Merge remote-tracking branch 'origin/Django' into Django
This commit is contained in:
@@ -90,4 +90,26 @@ def verify_password(password_plain: str, salt_b64: str, hash_b64: str) -> bool:
|
|||||||
actual = hash_password_with_salt(password_plain, salt)
|
actual = hash_password_with_salt(password_plain, salt)
|
||||||
return hmac.compare_digest(actual, expected)
|
return hmac.compare_digest(actual, expected)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def generate_rsa_private_pem_b64() -> str:
|
||||||
|
if rsa is None or serialization is None:
|
||||||
|
raise RuntimeError("cryptography library is required for RSA operations")
|
||||||
|
priv = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||||
|
pem = priv.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption())
|
||||||
|
return base64.b64encode(pem).decode('ascii')
|
||||||
|
|
||||||
|
def public_spki_b64_from_private_pem_b64(private_pem_b64: str) -> str:
|
||||||
|
if serialization is None:
|
||||||
|
raise RuntimeError("cryptography library is required for RSA operations")
|
||||||
|
priv = serialization.load_pem_private_key(base64.b64decode(private_pem_b64), password=None)
|
||||||
|
pub = priv.public_key()
|
||||||
|
spki = pub.public_bytes(encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo)
|
||||||
|
return base64.b64encode(spki).decode('ascii')
|
||||||
|
|
||||||
|
def rsa_oaep_decrypt_b64_with_private_pem(private_pem_b64: str, ciphertext_b64: str) -> bytes:
|
||||||
|
if serialization is None or padding is None or hashes is None:
|
||||||
|
raise RuntimeError("cryptography library is required for RSA operations")
|
||||||
|
priv = serialization.load_pem_private_key(base64.b64decode(private_pem_b64), password=None)
|
||||||
|
ct = base64.b64decode(ciphertext_b64)
|
||||||
|
return priv.decrypt(ct, padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()), algorithm=hashes.SHA256(), label=None))
|
||||||
@@ -77,8 +77,20 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|||||||
const setKeyResp = await fetch('/accounts/session-key/', {
|
const setKeyResp = await fetch('/accounts/session-key/', {
|
||||||
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ encrypted_key: encAesKeyB64 })
|
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ encrypted_key: encAesKeyB64 })
|
||||||
});
|
});
|
||||||
const setKeyJson = await setKeyResp.json();
|
const setKeySnapshot = await (async () => {
|
||||||
if (!setKeyResp.ok || !setKeyJson.ok) throw new Error('设置会话密钥失败');
|
const clone = setKeyResp.clone();
|
||||||
|
const txt = await clone.text();
|
||||||
|
let parsed = null;
|
||||||
|
try { parsed = await setKeyResp.json(); } catch (_) {}
|
||||||
|
return { txt, parsed };
|
||||||
|
})();
|
||||||
|
if (!setKeySnapshot.parsed) {
|
||||||
|
const msg = (setKeySnapshot.txt || '').trim();
|
||||||
|
const mapped = msg.toLowerCase().includes('decrypt error') ? '会话密钥解密失败,请刷新页面后重试' : (msg || '设置会话密钥失败');
|
||||||
|
throw new Error(mapped);
|
||||||
|
}
|
||||||
|
const setKeyJson = setKeySnapshot.parsed;
|
||||||
|
if (!setKeyResp.ok || !setKeyJson.ok) throw new Error(setKeyJson.message || '设置会话密钥失败');
|
||||||
|
|
||||||
const aesKey = await importAesKey(aesKeyRaw);
|
const aesKey = await importAesKey(aesKeyRaw);
|
||||||
const iv = new Uint8Array(12); window.crypto.getRandomValues(iv);
|
const iv = new Uint8Array(12); window.crypto.getRandomValues(iv);
|
||||||
@@ -92,7 +104,19 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|||||||
const submitResp = await fetch('/accounts/login/secure-submit/', {
|
const submitResp = await fetch('/accounts/login/secure-submit/', {
|
||||||
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ iv: ivB64, ciphertext: ctB64 })
|
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrftoken || '' }, body: JSON.stringify({ iv: ivB64, ciphertext: ctB64 })
|
||||||
});
|
});
|
||||||
const submitJson = await submitResp.json();
|
const submitSnapshot = await (async () => {
|
||||||
|
const clone = submitResp.clone();
|
||||||
|
const txt = await clone.text();
|
||||||
|
let parsed = null;
|
||||||
|
try { parsed = await submitResp.json(); } catch (_) {}
|
||||||
|
return { txt, parsed };
|
||||||
|
})();
|
||||||
|
if (!submitSnapshot.parsed) {
|
||||||
|
const msg = (submitSnapshot.txt || '').trim();
|
||||||
|
const mapped = msg.toLowerCase().includes('decrypt error') ? '解密失败,请刷新页面后重试' : (msg || '服务器响应异常');
|
||||||
|
throw new Error(mapped);
|
||||||
|
}
|
||||||
|
const submitJson = submitSnapshot.parsed;
|
||||||
if (!submitResp.ok || !submitJson.ok) {
|
if (!submitResp.ok || !submitJson.ok) {
|
||||||
if (submitJson && submitJson.captcha_required) { needCaptcha = true; await loadCaptcha(); }
|
if (submitJson && submitJson.captcha_required) { needCaptcha = true; await loadCaptcha(); }
|
||||||
throw new Error(submitJson.message || '登录失败');
|
throw new Error(submitJson.message || '登录失败');
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from .es_client import get_user_by_username
|
from .es_client import get_user_by_username
|
||||||
from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password
|
from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password, generate_rsa_private_pem_b64, public_spki_b64_from_private_pem_b64, rsa_oaep_decrypt_b64_with_private_pem
|
||||||
from elastic.es_connect import get_registration_code, get_user_by_username as es_get_user_by_username, get_all_users as es_get_all_users, write_user_data
|
from elastic.es_connect import get_registration_code, get_user_by_username as es_get_user_by_username, get_all_users as es_get_all_users, write_user_data
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +25,11 @@ def login_page(request):
|
|||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def pubkey(request):
|
def pubkey(request):
|
||||||
pk_b64 = get_public_key_spki_b64()
|
pem_b64 = request.session.get("rsa_private_pem_b64")
|
||||||
|
if not pem_b64:
|
||||||
|
pem_b64 = generate_rsa_private_pem_b64()
|
||||||
|
request.session["rsa_private_pem_b64"] = pem_b64
|
||||||
|
pk_b64 = public_spki_b64_from_private_pem_b64(pem_b64)
|
||||||
return JsonResponse({"public_key_spki": pk_b64})
|
return JsonResponse({"public_key_spki": pk_b64})
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
@@ -56,7 +60,10 @@ def set_session_key(request):
|
|||||||
if not enc_key_b64:
|
if not enc_key_b64:
|
||||||
return HttpResponseBadRequest("Missing fields")
|
return HttpResponseBadRequest("Missing fields")
|
||||||
try:
|
try:
|
||||||
key_bytes = rsa_oaep_decrypt_b64(enc_key_b64)
|
pem_b64 = request.session.get("rsa_private_pem_b64")
|
||||||
|
if not pem_b64:
|
||||||
|
return HttpResponseBadRequest("Decrypt error")
|
||||||
|
key_bytes = rsa_oaep_decrypt_b64_with_private_pem(pem_b64, enc_key_b64)
|
||||||
except Exception:
|
except Exception:
|
||||||
return HttpResponseBadRequest("Decrypt error")
|
return HttpResponseBadRequest("Decrypt error")
|
||||||
request.session["session_enc_key_b64"] = base64.b64encode(key_bytes).decode("ascii")
|
request.session["session_enc_key_b64"] = base64.b64encode(key_bytes).decode("ascii")
|
||||||
@@ -110,6 +117,8 @@ def secure_login_submit(request):
|
|||||||
request.session["permission"] = 1
|
request.session["permission"] = 1
|
||||||
if "session_enc_key_b64" in request.session:
|
if "session_enc_key_b64" in request.session:
|
||||||
del request.session["session_enc_key_b64"]
|
del request.session["session_enc_key_b64"]
|
||||||
|
if "rsa_private_pem_b64" in request.session:
|
||||||
|
del request.session["rsa_private_pem_b64"]
|
||||||
if "login_failed_once" in request.session:
|
if "login_failed_once" in request.session:
|
||||||
del request.session["login_failed_once"]
|
del request.session["login_failed_once"]
|
||||||
if "captcha_code" in request.session:
|
if "captcha_code" in request.session:
|
||||||
|
|||||||
@@ -197,6 +197,55 @@ def get_registration_code(code: str):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def list_registration_codes():
|
||||||
|
try:
|
||||||
|
search = RegistrationCodeDocument.search()
|
||||||
|
body = {
|
||||||
|
"sort": [{"created_at": {"order": "desc"}}],
|
||||||
|
"query": {"exists": {"field": "code"}}
|
||||||
|
}
|
||||||
|
search = search.update_from_dict(body)
|
||||||
|
resp = search.execute()
|
||||||
|
out = []
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
for hit in resp:
|
||||||
|
try:
|
||||||
|
if not getattr(hit, 'code', None):
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
exp = getattr(hit, 'expires_at', None)
|
||||||
|
try:
|
||||||
|
if hasattr(exp, 'isoformat'):
|
||||||
|
exp_dt = exp
|
||||||
|
else:
|
||||||
|
exp_dt = datetime.fromisoformat(str(exp))
|
||||||
|
except Exception:
|
||||||
|
exp_dt = None
|
||||||
|
active = bool(exp_dt and exp_dt > now)
|
||||||
|
out.append({
|
||||||
|
"code": getattr(hit, 'code', ''),
|
||||||
|
"keys": list(getattr(hit, 'keys', []) or []),
|
||||||
|
"manage_keys": list(getattr(hit, 'manage_keys', []) or []),
|
||||||
|
"created_at": getattr(hit, 'created_at', None),
|
||||||
|
"expires_at": getattr(hit, 'expires_at', None),
|
||||||
|
"created_by": getattr(hit, 'created_by', None),
|
||||||
|
"active": active,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def revoke_registration_code(code: str):
|
||||||
|
try:
|
||||||
|
doc = RegistrationCodeDocument.get(id=str(code))
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
doc.expires_at = now
|
||||||
|
doc.save()
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def get_doc_id(data):
|
def get_doc_id(data):
|
||||||
"""
|
"""
|
||||||
根据数据内容生成唯一ID(用于去重)
|
根据数据内容生成唯一ID(用于去重)
|
||||||
|
|||||||
@@ -23,6 +23,14 @@
|
|||||||
.notice.success { background:#d4edda; color:#155724; border:1px solid #c3e6cb; }
|
.notice.success { background:#d4edda; color:#155724; border:1px solid #c3e6cb; }
|
||||||
.notice.error { background:#f8d7da; color:#721c24; border:1px solid #f5c6cb; }
|
.notice.error { background:#f8d7da; color:#721c24; border:1px solid #f5c6cb; }
|
||||||
.code-box { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; padding:12px; border:1px solid #e5e7eb; border-radius:8px; background:#fafafa; margin-top:10px; }
|
.code-box { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; padding:12px; border:1px solid #e5e7eb; border-radius:8px; background:#fafafa; margin-top:10px; }
|
||||||
|
.overlay { position:fixed; inset:0; background:rgba(0,0,0,0.25); display:flex; align-items:center; justify-content:center; z-index:2000; }
|
||||||
|
.spinner { width:42px; height:42px; border:4px solid #cbd5e1; border-top-color:#4f46e5; border-radius:50%; animation:spin 0.8s linear infinite; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.fade-in { animation: fadeUp 0.25s ease-out; }
|
||||||
|
@keyframes fadeUp { from { opacity:0; transform: translateY(6px); } to { opacity:1; transform: translateY(0); } }
|
||||||
|
table tr:hover { background-color:#f3f4f6; transition: background-color 0.2s ease; }
|
||||||
|
.btn { transition: transform 0.1s ease, box-shadow 0.2s ease; }
|
||||||
|
.btn:hover { transform: translateY(-1px); box-shadow:0 6px 16px rgba(31,35,40,0.12); }
|
||||||
</style>
|
</style>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<script>
|
<script>
|
||||||
@@ -53,6 +61,7 @@
|
|||||||
function enableToggleSelect(sel){ sel.addEventListener('mousedown',function(e){ if(e.target && e.target.tagName==='OPTION'){ e.preventDefault(); const op=e.target; op.selected=!op.selected; this.dispatchEvent(new Event('change',{bubbles:true})); } }); }
|
function enableToggleSelect(sel){ sel.addEventListener('mousedown',function(e){ if(e.target && e.target.tagName==='OPTION'){ e.preventDefault(); const op=e.target; op.selected=!op.selected; this.dispatchEvent(new Event('change',{bubbles:true})); } }); }
|
||||||
function clearSelection(id){ const sel=document.getElementById(id); Array.from(sel.options).forEach(o=>o.selected=false); }
|
function clearSelection(id){ const sel=document.getElementById(id); Array.from(sel.options).forEach(o=>o.selected=false); }
|
||||||
async function generateCode(){
|
async function generateCode(){
|
||||||
|
const ov=document.getElementById('overlay'); ov.style.display='flex';
|
||||||
const csrftoken=getCookie('csrftoken');
|
const csrftoken=getCookie('csrftoken');
|
||||||
const keys=selectedValues(document.getElementById('keys'));
|
const keys=selectedValues(document.getElementById('keys'));
|
||||||
const manageKeys=selectedValues(document.getElementById('manageKeys'));
|
const manageKeys=selectedValues(document.getElementById('manageKeys'));
|
||||||
@@ -64,22 +73,46 @@
|
|||||||
const msg=document.getElementById('msg');
|
const msg=document.getElementById('msg');
|
||||||
if(resp.ok && data.status==='success'){out.textContent=data.data.code; msg.textContent='生成成功'; msg.className='notice success'; msg.style.display='block';}
|
if(resp.ok && data.status==='success'){out.textContent=data.data.code; msg.textContent='生成成功'; msg.className='notice success'; msg.style.display='block';}
|
||||||
else{msg.textContent=data.message||'生成失败'; msg.className='notice error'; msg.style.display='block';}
|
else{msg.textContent=data.message||'生成失败'; msg.className='notice error'; msg.style.display='block';}
|
||||||
|
ov.style.display='none';
|
||||||
}
|
}
|
||||||
document.addEventListener('DOMContentLoaded',()=>{loadKeys(); enableToggleSelect(document.getElementById('keys')); enableToggleSelect(document.getElementById('manageKeys'));});
|
async function loadCodes(){
|
||||||
|
const ov=document.getElementById('overlay'); ov.style.display='flex';
|
||||||
|
const resp=await fetch('/elastic/registration-codes/list/');
|
||||||
|
const data=await resp.json();
|
||||||
|
const tbody=document.getElementById('codesBody');
|
||||||
|
if(!tbody) return;
|
||||||
|
tbody.innerHTML='';
|
||||||
|
if(resp.ok && data.status==='success'){
|
||||||
|
(data.data||[]).forEach(it=>{
|
||||||
|
const tr=document.createElement('tr');
|
||||||
|
const status = it.active? '有效' : '失效';
|
||||||
|
const ka = Array.isArray(it.keys)? it.keys.join('、') : '';
|
||||||
|
const mka = Array.isArray(it.manage_keys)? it.manage_keys.join('、') : '';
|
||||||
|
tr.innerHTML = `<td>${it.code||''}</td><td>${ka}</td><td>${mka}</td><td>${formatDate(it.created_at)}</td><td>${formatDate(it.expires_at)}</td><td>${status}</td><td>${it.active? '<button class=\"btn btn-secondary\" data-code=\"'+it.code+'\">作废</button>':''}</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ov.style.display='none';
|
||||||
|
}
|
||||||
|
function formatDate(t){ if(!t) return ''; try{ const d = new Date(t); if(String(d)!='Invalid Date'){ const p=n=>String(n).padStart(2,'0'); return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;} }catch(e){} return ''; }
|
||||||
|
async function revokeCode(code){ const csrftoken=getCookie('csrftoken'); const resp=await fetch('/elastic/registration-codes/revoke/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({code})}); const msg=document.getElementById('msg'); const data=await resp.json(); if(resp.ok && data.status==='success'){ msg.textContent='已作废'; msg.className='notice success'; msg.style.display='block'; loadCodes(); } else { msg.textContent=data.message||'作废失败'; msg.className='notice error'; msg.style.display='block'; } }
|
||||||
|
document.addEventListener('click',function(e){ const btn=e.target; if(btn && btn.matches('button[data-code]')){ revokeCode(btn.getAttribute('data-code')); }});
|
||||||
|
document.addEventListener('DOMContentLoaded',()=>{loadKeys(); enableToggleSelect(document.getElementById('keys')); enableToggleSelect(document.getElementById('manageKeys')); loadCodes();});
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="overlay" class="overlay" style="display:none"><div class="spinner"></div></div>
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<h3>你好,{{ username|default:"访客" }}</h3>
|
<h3>用户ID:{{ user_id }}</h3>
|
||||||
<div class="navigation-links">
|
<div class="navigation-links">
|
||||||
<a href="{% url 'main:home' %}">返回主页</a>
|
<a href="{% url 'main:home' %}">主页</a>
|
||||||
<a id="logoutBtn">退出登录</a>
|
<a id="logoutBtn">退出登录</a>
|
||||||
<div id="logoutMsg"></div>
|
<div id="logoutMsg"></div>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="card">
|
<div class="card fade-in">
|
||||||
<h2>管理注册码</h2>
|
<h2>管理注册码</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@@ -116,6 +149,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="msg" class="notice"></div>
|
<div id="msg" class="notice"></div>
|
||||||
<div class="code-box" id="codeOut"></div>
|
<div class="code-box" id="codeOut"></div>
|
||||||
|
<div class="row" style="margin-top:12px;">
|
||||||
|
<div class="col">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
|
<h3>已生成的注册码</h3>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-secondary" onclick="loadCodes()">刷新列表</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table style="width:100%; border-collapse:collapse; margin-top:10px;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">code</th>
|
||||||
|
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">keys</th>
|
||||||
|
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">manage_keys</th>
|
||||||
|
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">创建时间</th>
|
||||||
|
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">过期时间</th>
|
||||||
|
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">状态</th>
|
||||||
|
<th style="text-align:left; border-bottom:1px solid #e5e7eb; padding:8px;">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="codesBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ urlpatterns = [
|
|||||||
path('registration-codes/keys/', views.get_keys_list_view, name='get_keys_list'),
|
path('registration-codes/keys/', views.get_keys_list_view, name='get_keys_list'),
|
||||||
path('registration-codes/keys/add/', views.add_key_view, name='add_key'),
|
path('registration-codes/keys/add/', views.add_key_view, name='add_key'),
|
||||||
path('registration-codes/generate/', views.generate_registration_code_view, name='generate_registration_code'),
|
path('registration-codes/generate/', views.generate_registration_code_view, name='generate_registration_code'),
|
||||||
|
path('registration-codes/list/', views.list_registration_codes_view, name='list_registration_codes'),
|
||||||
|
path('registration-codes/revoke/', views.revoke_registration_code_view, name='revoke_registration_code'),
|
||||||
|
|
||||||
# 分析接口
|
# 分析接口
|
||||||
path('analytics/trend/', views.analytics_trend_view, name='analytics_trend'),
|
path('analytics/trend/', views.analytics_trend_view, name='analytics_trend'),
|
||||||
|
|||||||
@@ -668,3 +668,31 @@ def generate_registration_code_view(request):
|
|||||||
if not result:
|
if not result:
|
||||||
return JsonResponse({"status": "error", "message": "生成失败"}, status=500)
|
return JsonResponse({"status": "error", "message": "生成失败"}, status=500)
|
||||||
return JsonResponse({"status": "success", "data": result})
|
return JsonResponse({"status": "success", "data": result})
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def list_registration_codes_view(request):
|
||||||
|
if request.session.get("user_id") is None:
|
||||||
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
|
if int(request.session.get("permission", 1)) != 0:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
data = list_registration_codes()
|
||||||
|
return JsonResponse({"status": "success", "data": data})
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@csrf_protect
|
||||||
|
def revoke_registration_code_view(request):
|
||||||
|
if request.session.get("user_id") is None:
|
||||||
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
|
if int(request.session.get("permission", 1)) != 0:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return JsonResponse({"status": "error", "message": "JSON无效"}, status=400)
|
||||||
|
code = (payload.get("code") or "").strip()
|
||||||
|
if not code:
|
||||||
|
return JsonResponse({"status": "error", "message": "缺少code"}, status=400)
|
||||||
|
ok = revoke_registration_code(code)
|
||||||
|
if not ok:
|
||||||
|
return JsonResponse({"status": "error", "message": "作废失败"}, status=500)
|
||||||
|
return JsonResponse({"status": "success"})
|
||||||
|
|||||||
Reference in New Issue
Block a user