This commit is contained in:
@@ -15,13 +15,14 @@
|
|||||||
|
|
||||||
/* 主内容区 */
|
/* 主内容区 */
|
||||||
.main-content { margin-left: 220px; padding: 40px; }
|
.main-content { margin-left: 220px; padding: 40px; }
|
||||||
.profile-card { background: #fff; border-radius: 14px; box-shadow: 0 10px 24px rgba(31,35,40,0.08); padding: 30px; margin-bottom: 30px; }
|
.profile-card { background: #fff; border-radius: 14px; box-shadow: 0 10px 24px rgba(31,35,40,0.08); padding: 30px; margin-bottom: 40px; }
|
||||||
|
.rc-card { margin-top: 18px; }
|
||||||
.profile-header { display: flex; align-items: center; margin-bottom: 20px; border-bottom: 1px solid #eee; padding-bottom: 20px; }
|
.profile-header { display: flex; align-items: center; margin-bottom: 20px; border-bottom: 1px solid #eee; padding-bottom: 20px; }
|
||||||
.profile-info h2 { margin: 0; color: #1e1e2e; }
|
.profile-info h2 { margin: 0; color: #1e1e2e; }
|
||||||
.profile-info p { margin: 5px 0; color: #666; }
|
.profile-info p { margin: 5px 0; color: #666; }
|
||||||
.label { font-weight: bold; color: #333; margin-right: 10px; }
|
.label { font-weight: bold; color: #333; margin-right: 10px; }
|
||||||
|
|
||||||
.section-title { font-size: 20px; font-weight: bold; margin-bottom: 20px; color: #1e1e2e; }
|
.section-title { font-size: 20px; font-weight: bold; margin: 34px 0 24px; color: #1e1e2e; }
|
||||||
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
|
.image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 20px; }
|
||||||
.image-item { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05); transition: transform 0.2s; }
|
.image-item { background: #fff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05); transition: transform 0.2s; }
|
||||||
.image-item:hover { transform: translateY(-5px); }
|
.image-item:hover { transform: translateY(-5px); }
|
||||||
@@ -66,34 +67,13 @@
|
|||||||
<div class="profile-details">
|
<div class="profile-details">
|
||||||
<p><span class="label">用户名:</span> {{ profile_user.username }}</p>
|
<p><span class="label">用户名:</span> {{ profile_user.username }}</p>
|
||||||
<p><span class="label">用户ID:</span> {{ profile_user.user_id }}</p>
|
<p><span class="label">用户ID:</span> {{ profile_user.user_id }}</p>
|
||||||
|
<p><span class="label">注册码:</span> {{ profile_user.registration_code|default:"无" }}</p>
|
||||||
<p><span class="label">所属:</span> {{ profile_user.key|join:"、"|default:"未填写" }}</p>
|
<p><span class="label">所属:</span> {{ profile_user.key|join:"、"|default:"未填写" }}</p>
|
||||||
<p><span class="label">可管理级别:</span> {{ profile_user.manage_key|join:"、"|default:"无" }}</p>
|
<p><span class="label">可管理级别:</span> {{ profile_user.manage_key|join:"、"|default:"无" }}</p>
|
||||||
<p><span class="label">权限级别:</span> {{ permission_name }}</p>
|
<p><span class="label">权限级别:</span> {{ permission_name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if permission_name != "管理员" and not profile_user.manage_key %}
|
|
||||||
<div class="profile-card">
|
|
||||||
<div class="profile-header">
|
|
||||||
<div class="profile-info">
|
|
||||||
<h2>修改密码</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form id="pwdForm">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="newPassword">新密码</label>
|
|
||||||
<input type="password" id="newPassword" autocomplete="new-password" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="confirmPassword">确认密码</label>
|
|
||||||
<input type="password" id="confirmPassword" autocomplete="new-password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn">保存</button>
|
|
||||||
<div id="pwdMsg" class="msg"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="section-title">我的提交</div>
|
<div class="section-title">我的提交</div>
|
||||||
{% if achievements %}
|
{% if achievements %}
|
||||||
<div class="image-grid">
|
<div class="image-grid">
|
||||||
@@ -116,6 +96,50 @@
|
|||||||
<a href="{% url 'elastic:upload_page' %}" style="color: #2d8cf0; text-decoration: none;">去上传第一张图片吧!</a>
|
<a href="{% url 'elastic:upload_page' %}" style="color: #2d8cf0; text-decoration: none;">去上传第一张图片吧!</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="profile-card rc-card">
|
||||||
|
<div class="profile-header">
|
||||||
|
<div class="profile-info">
|
||||||
|
<h2>替换注册码</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="rcForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newRegCode">新注册码</label>
|
||||||
|
<input type="text" id="newRegCode" placeholder="输入新注册码后替换原有 key" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>预览</label>
|
||||||
|
<div id="rcPreview" style="background:#f8fafc; border:1px solid #e5e7eb; border-radius:10px; padding:10px 12px; font-size:13px; color:#334155;">
|
||||||
|
<div style="color:#64748b;">输入注册码后自动显示 key 预览</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">替换</button>
|
||||||
|
<div id="rcMsg" class="msg"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if permission_name != "管理员" and not profile_user.manage_key %}
|
||||||
|
<div class="profile-card">
|
||||||
|
<div class="profile-header">
|
||||||
|
<div class="profile-info">
|
||||||
|
<h2>修改密码</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form id="pwdForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newPassword">新密码</label>
|
||||||
|
<input type="password" id="newPassword" autocomplete="new-password" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirmPassword">确认密码</label>
|
||||||
|
<input type="password" id="confirmPassword" autocomplete="new-password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">保存</button>
|
||||||
|
<div id="pwdMsg" class="msg"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 图片放大模态框 -->
|
<!-- 图片放大模态框 -->
|
||||||
@@ -205,6 +229,88 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rcForm = document.getElementById('rcForm');
|
||||||
|
if (rcForm) {
|
||||||
|
let rcPreviewTimer = null;
|
||||||
|
let rcPreviewSeq = 0;
|
||||||
|
const rcInput = document.getElementById('newRegCode');
|
||||||
|
const rcPreview = document.getElementById('rcPreview');
|
||||||
|
|
||||||
|
async function refreshRcPreview(code) {
|
||||||
|
const seq = ++rcPreviewSeq;
|
||||||
|
if (!code) {
|
||||||
|
rcPreview.innerHTML = '<div style="color:#64748b;">输入注册码后自动显示 key 预览</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rcPreview.innerHTML = '<div style="color:#64748b;">正在查询...</div>';
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/accounts/profile/registration-code/preview/?code=${encodeURIComponent(code)}`, { method: 'GET', credentials: 'same-origin' });
|
||||||
|
const data = await resp.json();
|
||||||
|
if (seq !== rcPreviewSeq) return;
|
||||||
|
if (!(resp.ok && data && data.ok)) {
|
||||||
|
const msg = (data && data.message) ? data.message : '查询失败';
|
||||||
|
rcPreview.innerHTML = `<div style="color:#b91c1c;">${msg}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const keys = ((data.data || {}).keys || []).map(String).filter(Boolean);
|
||||||
|
const manageKeys = ((data.data || {}).manage_keys || []).map(String).filter(Boolean);
|
||||||
|
const keysText = keys.length ? keys.join('、') : '无';
|
||||||
|
const manageText = manageKeys.length ? manageKeys.join('、') : '无';
|
||||||
|
rcPreview.innerHTML = `<div><span style="font-weight:700;">key:</span>${keysText}</div><div style="margin-top:6px;"><span style="font-weight:700;">manage_key:</span>${manageText}</div>`;
|
||||||
|
} catch (e) {
|
||||||
|
if (seq !== rcPreviewSeq) return;
|
||||||
|
rcPreview.innerHTML = '<div style="color:#b91c1c;">查询失败</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rcInput) {
|
||||||
|
rcInput.addEventListener('input', () => {
|
||||||
|
const code = (rcInput.value || '').trim();
|
||||||
|
if (rcPreviewTimer) window.clearTimeout(rcPreviewTimer);
|
||||||
|
rcPreviewTimer = window.setTimeout(() => refreshRcPreview(code), 300);
|
||||||
|
});
|
||||||
|
refreshRcPreview((rcInput.value || '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
rcForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const msg = document.getElementById('rcMsg');
|
||||||
|
msg.textContent = '';
|
||||||
|
msg.className = 'msg';
|
||||||
|
const code = (document.getElementById('newRegCode').value || '').trim();
|
||||||
|
if (!code) {
|
||||||
|
msg.textContent = '请输入注册码';
|
||||||
|
msg.className = 'msg error';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm('确定要替换注册码吗?该操作会替换你当前的 key。')) return;
|
||||||
|
try {
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
const resp = await fetch('/accounts/profile/registration-code/replace/', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': csrftoken || ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ code })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (resp.ok && data.ok) {
|
||||||
|
msg.textContent = '替换成功';
|
||||||
|
msg.className = 'msg success';
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
msg.textContent = (data && data.message) ? data.message : '替换失败';
|
||||||
|
msg.className = 'msg error';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
msg.textContent = '替换失败';
|
||||||
|
msg.className = 'msg error';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -13,4 +13,10 @@ urlpatterns = [
|
|||||||
path("register/submit/", views.register_submit, name="register_submit"),
|
path("register/submit/", views.register_submit, name="register_submit"),
|
||||||
path("email/send-code/", views.send_email_code, name="send_email_code"),
|
path("email/send-code/", views.send_email_code, name="send_email_code"),
|
||||||
path("profile/", views.profile_page, name="profile"),
|
path("profile/", views.profile_page, name="profile"),
|
||||||
|
path("profile/registration-code/replace/", views.replace_registration_code_view, name="replace_registration_code"),
|
||||||
|
path("profile/registration-code/preview/", views.registration_code_preview_view, name="registration_code_preview"),
|
||||||
|
path("registration-code/request/submit/", views.submit_registration_code_request_view, name="submit_registration_code_request"),
|
||||||
|
path("registration-code/requests/", views.registration_code_requests_page, name="registration_code_requests_page"),
|
||||||
|
path("registration-code/requests/list/", views.list_registration_code_requests_view, name="list_registration_code_requests"),
|
||||||
|
path("registration-code/requests/decide/", views.decide_registration_code_request_view, name="decide_registration_code_request"),
|
||||||
]
|
]
|
||||||
@@ -15,7 +15,7 @@ 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, generate_rsa_private_pem_b64, public_spki_b64_from_private_pem_b64, rsa_oaep_decrypt_b64_with_private_pem
|
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, update_user_by_id, get_user_by_id
|
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, update_user_by_id, get_user_by_id, create_registration_code_manage_request, find_pending_registration_code_manage_request, list_registration_code_manage_requests, decide_registration_code_manage_request, get_registration_code_manage_request
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
@@ -259,6 +259,7 @@ def register_submit(request):
|
|||||||
"email": email,
|
"email": email,
|
||||||
"key": (rc.get("keys") if rc else []) or [],
|
"key": (rc.get("keys") if rc else []) or [],
|
||||||
"manage_key": (rc.get("manage_keys") if rc else []) or [],
|
"manage_key": (rc.get("manage_keys") if rc else []) or [],
|
||||||
|
"registration_code": (rc.get("code") if rc else None),
|
||||||
})
|
})
|
||||||
if not ok:
|
if not ok:
|
||||||
return JsonResponse({"ok": False, "message": "注册失败"}, status=500)
|
return JsonResponse({"ok": False, "message": "注册失败"}, status=500)
|
||||||
@@ -269,6 +270,169 @@ def register_submit(request):
|
|||||||
pass
|
pass
|
||||||
return JsonResponse({"ok": True, "redirect_url": "/accounts/login/"})
|
return JsonResponse({"ok": True, "redirect_url": "/accounts/login/"})
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@csrf_protect
|
||||||
|
def replace_registration_code_view(request):
|
||||||
|
session_user_id = request.session.get("user_id")
|
||||||
|
if session_user_id is None:
|
||||||
|
return JsonResponse({"ok": False, "message": "未登录"}, status=401)
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return HttpResponseBadRequest("Invalid JSON")
|
||||||
|
code = (payload.get("code") or "").strip()
|
||||||
|
if not code:
|
||||||
|
return JsonResponse({"ok": False, "message": "请输入注册码"}, status=400)
|
||||||
|
rc = get_registration_code(code)
|
||||||
|
if not rc:
|
||||||
|
return JsonResponse({"ok": False, "message": "注册码无效"}, status=400)
|
||||||
|
try:
|
||||||
|
exp = rc.get("expires_at")
|
||||||
|
now = __import__("datetime").datetime.now(__import__("datetime").timezone.utc)
|
||||||
|
if hasattr(exp, 'isoformat'):
|
||||||
|
exp_dt = exp
|
||||||
|
else:
|
||||||
|
exp_dt = __import__("datetime").datetime.fromisoformat(str(exp))
|
||||||
|
if exp_dt <= now:
|
||||||
|
return JsonResponse({"ok": False, "message": "注册码已过期"}, status=400)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
keys = list(rc.get("keys") or [])
|
||||||
|
manage_keys = list(rc.get("manage_keys") or [])
|
||||||
|
ok = update_user_by_id(session_user_id, key=keys, manage_key=manage_keys, registration_code=code)
|
||||||
|
if not ok:
|
||||||
|
return JsonResponse({"ok": False, "message": "替换失败"}, status=500)
|
||||||
|
return JsonResponse({"ok": True})
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def registration_code_preview_view(request):
|
||||||
|
session_user_id = request.session.get("user_id")
|
||||||
|
if session_user_id is None:
|
||||||
|
return JsonResponse({"ok": False, "message": "未登录"}, status=401)
|
||||||
|
code = (request.GET.get("code") or "").strip()
|
||||||
|
if not code:
|
||||||
|
return JsonResponse({"ok": False, "message": "请输入注册码"}, status=400)
|
||||||
|
rc = get_registration_code(code)
|
||||||
|
if not rc:
|
||||||
|
return JsonResponse({"ok": False, "message": "注册码无效"}, status=400)
|
||||||
|
try:
|
||||||
|
exp = rc.get("expires_at")
|
||||||
|
now = __import__("datetime").datetime.now(__import__("datetime").timezone.utc)
|
||||||
|
if hasattr(exp, 'isoformat'):
|
||||||
|
exp_dt = exp
|
||||||
|
else:
|
||||||
|
exp_dt = __import__("datetime").datetime.fromisoformat(str(exp))
|
||||||
|
if exp_dt <= now:
|
||||||
|
return JsonResponse({"ok": False, "message": "注册码已过期"}, status=400)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"data": {
|
||||||
|
"code": rc.get("code"),
|
||||||
|
"keys": list(rc.get("keys") or []),
|
||||||
|
"manage_keys": list(rc.get("manage_keys") or []),
|
||||||
|
"expires_at": rc.get("expires_at"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@csrf_protect
|
||||||
|
def submit_registration_code_request_view(request):
|
||||||
|
session_user_id = request.session.get("user_id")
|
||||||
|
if session_user_id is None:
|
||||||
|
return JsonResponse({"ok": False, "message": "未登录"}, status=401)
|
||||||
|
try:
|
||||||
|
perm = int(request.session.get("permission", 1))
|
||||||
|
except Exception:
|
||||||
|
perm = 1
|
||||||
|
if perm == 0:
|
||||||
|
return JsonResponse({"ok": False, "message": "无权限"}, status=403)
|
||||||
|
me = get_user_by_id(session_user_id) or {}
|
||||||
|
if (me.get("manage_key") or []) or int(me.get("can_manage_registration_codes") or 0) == 1:
|
||||||
|
return JsonResponse({"ok": False, "message": "无需申请"}, status=400)
|
||||||
|
if str(me.get("registration_code") or "").strip():
|
||||||
|
return JsonResponse({"ok": False, "message": "已有注册码,无法申请"}, status=400)
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return HttpResponseBadRequest("Invalid JSON")
|
||||||
|
reason = (payload.get("reason") or "").strip()
|
||||||
|
if not reason:
|
||||||
|
return JsonResponse({"ok": False, "message": "请填写申请理由"}, status=400)
|
||||||
|
pending = find_pending_registration_code_manage_request(session_user_id)
|
||||||
|
if pending:
|
||||||
|
return JsonResponse({"ok": True, "message": "已提交申请"})
|
||||||
|
rid = create_registration_code_manage_request(session_user_id, me.get("username"), reason)
|
||||||
|
if not rid:
|
||||||
|
return JsonResponse({"ok": False, "message": "提交失败"}, status=500)
|
||||||
|
return JsonResponse({"ok": True})
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
@ensure_csrf_cookie
|
||||||
|
def registration_code_requests_page(request):
|
||||||
|
session_user_id = request.session.get("user_id")
|
||||||
|
if session_user_id is None:
|
||||||
|
return redirect("/accounts/login/")
|
||||||
|
try:
|
||||||
|
perm = int(request.session.get("permission", 1))
|
||||||
|
except Exception:
|
||||||
|
perm = 1
|
||||||
|
if perm != 0:
|
||||||
|
return redirect("/main/home/")
|
||||||
|
me = get_user_by_id(session_user_id) or {}
|
||||||
|
return render(request, "accounts/registration_code_requests.html", {"username": me.get("username")})
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def list_registration_code_requests_view(request):
|
||||||
|
session_user_id = request.session.get("user_id")
|
||||||
|
if session_user_id is None:
|
||||||
|
return JsonResponse({"ok": False, "message": "未登录"}, status=401)
|
||||||
|
try:
|
||||||
|
perm = int(request.session.get("permission", 1))
|
||||||
|
except Exception:
|
||||||
|
perm = 1
|
||||||
|
if perm != 0:
|
||||||
|
return JsonResponse({"ok": False, "message": "无权限"}, status=403)
|
||||||
|
status = (request.GET.get("status") or "").strip() or None
|
||||||
|
data = list_registration_code_manage_requests(status=status)
|
||||||
|
return JsonResponse({"ok": True, "data": data})
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@csrf_protect
|
||||||
|
def decide_registration_code_request_view(request):
|
||||||
|
session_user_id = request.session.get("user_id")
|
||||||
|
if session_user_id is None:
|
||||||
|
return JsonResponse({"ok": False, "message": "未登录"}, status=401)
|
||||||
|
try:
|
||||||
|
perm = int(request.session.get("permission", 1))
|
||||||
|
except Exception:
|
||||||
|
perm = 1
|
||||||
|
if perm != 0:
|
||||||
|
return JsonResponse({"ok": False, "message": "无权限"}, status=403)
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return HttpResponseBadRequest("Invalid JSON")
|
||||||
|
request_id = (payload.get("request_id") or "").strip()
|
||||||
|
action = (payload.get("action") or "").strip().lower()
|
||||||
|
note = (payload.get("note") or "").strip()
|
||||||
|
if not request_id or action not in ("approve", "reject"):
|
||||||
|
return JsonResponse({"ok": False, "message": "参数错误"}, status=400)
|
||||||
|
req = get_registration_code_manage_request(request_id)
|
||||||
|
if not req:
|
||||||
|
return JsonResponse({"ok": False, "message": "申请不存在"}, status=404)
|
||||||
|
status = "approved" if action == "approve" else "rejected"
|
||||||
|
ok = decide_registration_code_manage_request(request_id, status=status, reviewed_by=session_user_id, reviewer_note=note)
|
||||||
|
if not ok:
|
||||||
|
return JsonResponse({"ok": False, "message": "操作失败"}, status=500)
|
||||||
|
if status == "approved":
|
||||||
|
uid = req.get("user_id")
|
||||||
|
update_user_by_id(uid, can_manage_registration_codes=1, registration_manage_keys=[])
|
||||||
|
return JsonResponse({"ok": True})
|
||||||
|
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@csrf_protect
|
@csrf_protect
|
||||||
def send_email_code(request):
|
def send_email_code(request):
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ class UserDocument(Document):
|
|||||||
user_id = fields.LongField()
|
user_id = fields.LongField()
|
||||||
username = fields.KeywordField()
|
username = fields.KeywordField()
|
||||||
email = fields.KeywordField()
|
email = fields.KeywordField()
|
||||||
|
registration_code = fields.KeywordField()
|
||||||
|
can_manage_registration_codes = fields.IntegerField()
|
||||||
|
registration_manage_keys = fields.KeywordField(multi=True)
|
||||||
password_hash = fields.KeywordField()
|
password_hash = fields.KeywordField()
|
||||||
password_salt = fields.KeywordField()
|
password_salt = fields.KeywordField()
|
||||||
permission = fields.IntegerField() # 还是2种权限,0为管理员,1为用户(区别在于0有全部权限,1在数据管理页面有搜索框,但是索引到的录入信息要根据其用户id查询其key,若其中之一与用户的manage_key字段匹配就显示否则不显示)
|
permission = fields.IntegerField() # 还是2种权限,0为管理员,1为用户(区别在于0有全部权限,1在数据管理页面有搜索框,但是索引到的录入信息要根据其用户id查询其key,若其中之一与用户的manage_key字段匹配就显示否则不显示)
|
||||||
|
|||||||
@@ -783,6 +783,9 @@ def write_user_data(user_data):
|
|||||||
password_salt=pwd_salt_b64,
|
password_salt=pwd_salt_b64,
|
||||||
permission=perm_val,
|
permission=perm_val,
|
||||||
email=user_data.get('email'),
|
email=user_data.get('email'),
|
||||||
|
registration_code=(user_data.get('registration_code') or None),
|
||||||
|
can_manage_registration_codes=int(user_data.get('can_manage_registration_codes') or 0),
|
||||||
|
registration_manage_keys=list(user_data.get('registration_manage_keys') or []),
|
||||||
key=list(user_data.get('key') or []),
|
key=list(user_data.get('key') or []),
|
||||||
manage_key=list(user_data.get('manage_key') or []),
|
manage_key=list(user_data.get('manage_key') or []),
|
||||||
)
|
)
|
||||||
@@ -836,6 +839,9 @@ def get_all_users():
|
|||||||
"username": hit.username,
|
"username": hit.username,
|
||||||
"permission": int(hit.permission),
|
"permission": int(hit.permission),
|
||||||
"email": getattr(hit, 'email', None),
|
"email": getattr(hit, 'email', None),
|
||||||
|
"registration_code": getattr(hit, 'registration_code', None),
|
||||||
|
"can_manage_registration_codes": int(getattr(hit, 'can_manage_registration_codes', 0) or 0),
|
||||||
|
"registration_manage_keys": list(getattr(hit, 'registration_manage_keys', []) or []),
|
||||||
"key": list(getattr(hit, 'key', []) or []),
|
"key": list(getattr(hit, 'key', []) or []),
|
||||||
"manage_key": list(getattr(hit, 'manage_key', []) or []),
|
"manage_key": list(getattr(hit, 'manage_key', []) or []),
|
||||||
})
|
})
|
||||||
@@ -857,6 +863,9 @@ def get_user_by_id(user_id):
|
|||||||
"username": hit.username,
|
"username": hit.username,
|
||||||
"permission": int(hit.permission),
|
"permission": int(hit.permission),
|
||||||
"email": getattr(hit, 'email', None),
|
"email": getattr(hit, 'email', None),
|
||||||
|
"registration_code": getattr(hit, 'registration_code', None),
|
||||||
|
"can_manage_registration_codes": int(getattr(hit, 'can_manage_registration_codes', 0) or 0),
|
||||||
|
"registration_manage_keys": list(getattr(hit, 'registration_manage_keys', []) or []),
|
||||||
"key": list(getattr(hit, 'key', []) or []),
|
"key": list(getattr(hit, 'key', []) or []),
|
||||||
"manage_key": list(getattr(hit, 'manage_key', []) or []),
|
"manage_key": list(getattr(hit, 'manage_key', []) or []),
|
||||||
}
|
}
|
||||||
@@ -880,7 +889,7 @@ def delete_user_by_id(user_id):
|
|||||||
print(f"删除用户失败: {str(e)}")
|
print(f"删除用户失败: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def update_user_by_id(user_id, username=None, permission=None, password=None, key=None):
|
def update_user_by_id(user_id, username=None, permission=None, password=None, key=None, manage_key=None, registration_code=None, can_manage_registration_codes=None, registration_manage_keys=None):
|
||||||
try:
|
try:
|
||||||
search = UserDocument.search()
|
search = UserDocument.search()
|
||||||
search = search.query("term", user_id=int(user_id))
|
search = search.query("term", user_id=int(user_id))
|
||||||
@@ -898,9 +907,118 @@ def update_user_by_id(user_id, username=None, permission=None, password=None, ke
|
|||||||
doc.password_salt = salt_b64
|
doc.password_salt = salt_b64
|
||||||
if key is not None:
|
if key is not None:
|
||||||
doc.key = list(key)
|
doc.key = list(key)
|
||||||
|
if manage_key is not None:
|
||||||
|
doc.manage_key = list(manage_key)
|
||||||
|
if registration_code is not None:
|
||||||
|
doc.registration_code = str(registration_code) if str(registration_code).strip() else None
|
||||||
|
if can_manage_registration_codes is not None:
|
||||||
|
try:
|
||||||
|
doc.can_manage_registration_codes = int(can_manage_registration_codes)
|
||||||
|
except Exception:
|
||||||
|
doc.can_manage_registration_codes = 0
|
||||||
|
if registration_manage_keys is not None:
|
||||||
|
doc.registration_manage_keys = list(registration_manage_keys)
|
||||||
doc.save()
|
doc.save()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"更新用户失败: {str(e)}")
|
print(f"更新用户失败: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _rc_request_now_iso():
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
def create_registration_code_manage_request(user_id: int, username: str, reason: str):
|
||||||
|
try:
|
||||||
|
rid = uuid.uuid4().hex
|
||||||
|
doc = {
|
||||||
|
"kind": "registration_code_manage_request",
|
||||||
|
"request_id": rid,
|
||||||
|
"user_id": int(user_id),
|
||||||
|
"username": str(username or ""),
|
||||||
|
"reason": str(reason or ""),
|
||||||
|
"status": "pending",
|
||||||
|
"created_at": _rc_request_now_iso(),
|
||||||
|
}
|
||||||
|
es.index(index=GLOBAL_INDEX_NAME, id=rid, body=doc)
|
||||||
|
return rid
|
||||||
|
except Exception as e:
|
||||||
|
print(f"创建注册码管理申请失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def find_pending_registration_code_manage_request(user_id: int):
|
||||||
|
try:
|
||||||
|
body = {
|
||||||
|
"size": 1,
|
||||||
|
"query": {
|
||||||
|
"bool": {
|
||||||
|
"must": [
|
||||||
|
{"term": {"kind": "registration_code_manage_request"}},
|
||||||
|
{"term": {"user_id": int(user_id)}},
|
||||||
|
{"term": {"status": "pending"}},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sort": [{"created_at": {"order": "desc"}}],
|
||||||
|
}
|
||||||
|
resp = es.search(index=GLOBAL_INDEX_NAME, body=body)
|
||||||
|
hits = (resp.get("hits") or {}).get("hits") or []
|
||||||
|
if not hits:
|
||||||
|
return None
|
||||||
|
h = hits[0]
|
||||||
|
src = h.get("_source") or {}
|
||||||
|
src["_id"] = h.get("_id")
|
||||||
|
return src
|
||||||
|
except Exception as e:
|
||||||
|
print(f"查询注册码管理申请失败: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_registration_code_manage_request(request_id: str):
|
||||||
|
try:
|
||||||
|
resp = es.get(index=GLOBAL_INDEX_NAME, id=str(request_id))
|
||||||
|
src = resp.get("_source") or {}
|
||||||
|
if (src.get("kind") or "") != "registration_code_manage_request":
|
||||||
|
return None
|
||||||
|
src["_id"] = resp.get("_id")
|
||||||
|
return src
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def list_registration_code_manage_requests(status: str = None, limit: int = 200):
|
||||||
|
try:
|
||||||
|
must = [{"term": {"kind": "registration_code_manage_request"}}]
|
||||||
|
if status:
|
||||||
|
must.append({"term": {"status": str(status)}})
|
||||||
|
body = {
|
||||||
|
"size": max(1, min(int(limit or 200), 500)),
|
||||||
|
"query": {"bool": {"must": must}},
|
||||||
|
"sort": [{"created_at": {"order": "desc"}}],
|
||||||
|
}
|
||||||
|
resp = es.search(index=GLOBAL_INDEX_NAME, body=body)
|
||||||
|
hits = (resp.get("hits") or {}).get("hits") or []
|
||||||
|
out = []
|
||||||
|
for h in hits:
|
||||||
|
src = h.get("_source") or {}
|
||||||
|
src["_id"] = h.get("_id")
|
||||||
|
out.append(src)
|
||||||
|
return out
|
||||||
|
except Exception as e:
|
||||||
|
print(f"列出注册码管理申请失败: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def decide_registration_code_manage_request(request_id: str, status: str, reviewed_by: int, reviewer_note: str = None):
|
||||||
|
try:
|
||||||
|
sid = str(status or "").strip().lower()
|
||||||
|
if sid not in ("approved", "rejected"):
|
||||||
|
return False
|
||||||
|
doc = {
|
||||||
|
"status": sid,
|
||||||
|
"reviewed_at": _rc_request_now_iso(),
|
||||||
|
"reviewed_by": int(reviewed_by),
|
||||||
|
"reviewer_note": str(reviewer_note or ""),
|
||||||
|
}
|
||||||
|
es.update(index=GLOBAL_INDEX_NAME, id=str(request_id), body={"doc": doc})
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"审批注册码管理申请失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|||||||
@@ -38,6 +38,16 @@
|
|||||||
</style>
|
</style>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<script>
|
<script>
|
||||||
|
const IS_ADMIN = {{ is_admin|yesno:"true,false" }};
|
||||||
|
const HAS_MANAGE_KEY = {{ has_manage_key|yesno:"true,false" }};
|
||||||
|
const CAN_MANAGE_REG = {{ can_manage_registration_codes|yesno:"true,false" }};
|
||||||
|
const MY_KEYS_RAW = JSON.parse('{{ my_keys_json|default:"[]"|escapejs }}');
|
||||||
|
const MY_KEYS_SET = new Set((Array.isArray(MY_KEYS_RAW) ? MY_KEYS_RAW : []).map(v => String(v || '').trim()).filter(Boolean));
|
||||||
|
const MY_MANAGE_KEYS_RAW = JSON.parse('{{ manage_keys_json|default:"[]"|escapejs }}');
|
||||||
|
const MY_MANAGE_KEYS_SET = new Set((Array.isArray(MY_MANAGE_KEYS_RAW) ? MY_MANAGE_KEYS_RAW : []).map(v => String(v || '').trim()).filter(Boolean));
|
||||||
|
const ALLOWED_MANAGE_KEYS_RAW = JSON.parse('{{ allowed_manage_keys_json|default:"[]"|escapejs }}');
|
||||||
|
const ALLOWED_MANAGE_KEYS_SET = new Set((Array.isArray(ALLOWED_MANAGE_KEYS_RAW) ? ALLOWED_MANAGE_KEYS_RAW : []).map(v => String(v || '').trim()).filter(Boolean));
|
||||||
|
|
||||||
function getCookie(name){const v=`; ${document.cookie}`;const p=v.split(`; ${name}=`);if(p.length===2) return p.pop().split(';').shift();}
|
function getCookie(name){const v=`; ${document.cookie}`;const p=v.split(`; ${name}=`);if(p.length===2) return p.pop().split(';').shift();}
|
||||||
async function loadKeys(){
|
async function loadKeys(){
|
||||||
const resp=await fetch('/elastic/registration-codes/keys/');
|
const resp=await fetch('/elastic/registration-codes/keys/');
|
||||||
@@ -48,8 +58,17 @@
|
|||||||
keySel.innerHTML=''; mkeySel.innerHTML='';
|
keySel.innerHTML=''; mkeySel.innerHTML='';
|
||||||
opts.forEach(k=>{
|
opts.forEach(k=>{
|
||||||
const o=document.createElement('option'); o.value=k; o.textContent=k; keySel.appendChild(o);
|
const o=document.createElement('option'); o.value=k; o.textContent=k; keySel.appendChild(o);
|
||||||
const o2=document.createElement('option'); o2.value=k; o2.textContent=k; mkeySel.appendChild(o2);
|
const o2=document.createElement('option'); o2.value=k; o2.textContent=k;
|
||||||
|
if ((!IS_ADMIN) && HAS_MANAGE_KEY) {
|
||||||
|
const v = String(k || '').trim();
|
||||||
|
if (v && !ALLOWED_MANAGE_KEYS_SET.has(v)) o2.disabled = true;
|
||||||
|
}
|
||||||
|
mkeySel.appendChild(o2);
|
||||||
});
|
});
|
||||||
|
if ((!IS_ADMIN) && HAS_MANAGE_KEY) {
|
||||||
|
Array.from(keySel.options).forEach(o => { if (MY_KEYS_SET.has(String(o.value || '').trim())) o.selected = true; });
|
||||||
|
Array.from(mkeySel.options).forEach(o => { o.selected = false; });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function addKey(){
|
async function addKey(){
|
||||||
const keyName=(document.getElementById('newKey').value||'').trim();
|
const keyName=(document.getElementById('newKey').value||'').trim();
|
||||||
@@ -58,7 +77,12 @@
|
|||||||
const resp=await fetch('/elastic/registration-codes/keys/add/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({key:keyName})});
|
const resp=await fetch('/elastic/registration-codes/keys/add/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({key:keyName})});
|
||||||
const data=await resp.json();
|
const data=await resp.json();
|
||||||
const msg=document.getElementById('msg');
|
const msg=document.getElementById('msg');
|
||||||
if(resp.ok && data.status==='success'){msg.textContent='新增key成功'; msg.className='notice success'; msg.style.display='block'; document.getElementById('newKey').value=''; loadKeys();}
|
if(resp.ok && data.status==='success'){
|
||||||
|
if ((!IS_ADMIN) && HAS_MANAGE_KEY) {
|
||||||
|
ALLOWED_MANAGE_KEYS_SET.add(keyName);
|
||||||
|
}
|
||||||
|
msg.textContent='新增key成功'; msg.className='notice success'; msg.style.display='block'; document.getElementById('newKey').value=''; loadKeys();
|
||||||
|
}
|
||||||
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';}
|
||||||
}
|
}
|
||||||
async function deleteSelectedKey(){
|
async function deleteSelectedKey(){
|
||||||
@@ -72,20 +96,28 @@
|
|||||||
alert('请先在下方列表中选择一个要删除的Key');
|
alert('请先在下方列表中选择一个要删除的Key');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ((!IS_ADMIN) && HAS_MANAGE_KEY) {
|
||||||
|
const v = String(selectedKey || '').trim();
|
||||||
|
if (!v || !ALLOWED_MANAGE_KEYS_SET.has(v)) {
|
||||||
|
const msg=document.getElementById('msg');
|
||||||
|
msg.textContent='只能删除自己新增的 key';
|
||||||
|
msg.className='notice error';
|
||||||
|
msg.style.display='block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
if(!confirm(`确定要全局删除Key \"${selectedKey}\" 吗?\n该操作将:\n1. 从全局可选Key列表中移除\n2. 从所有包含此Key的注册码中同步清除\n此操作不可恢复!`)) return;
|
if(!confirm(`确定要全局删除Key \"${selectedKey}\" 吗?\n该操作将:\n1. 从全局可选Key列表中移除\n2. 从所有包含此Key的注册码中同步清除\n此操作不可恢复!`)) return;
|
||||||
|
|
||||||
const ov=document.getElementById('overlay'); ov.style.display='flex';
|
const ov=document.getElementById('overlay'); ov.style.display='flex';
|
||||||
const csrftoken=getCookie('csrftoken');
|
const csrftoken=getCookie('csrftoken');
|
||||||
const resp=await fetch('/elastic/registration-codes/keys/remove/',{
|
const url = '/elastic/registration-codes/keys/remove/';
|
||||||
method:'POST',
|
const resp=await fetch(url,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({key:selectedKey})});
|
||||||
credentials:'same-origin',
|
|
||||||
headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},
|
|
||||||
body:JSON.stringify({key:selectedKey})
|
|
||||||
});
|
|
||||||
const data=await resp.json();
|
const data=await resp.json();
|
||||||
const msg=document.getElementById('msg');
|
const msg=document.getElementById('msg');
|
||||||
if(resp.ok && data.status==='success'){
|
if(resp.ok && data.status==='success'){
|
||||||
|
if ((!IS_ADMIN) && HAS_MANAGE_KEY) {
|
||||||
|
ALLOWED_MANAGE_KEYS_SET.delete(String(selectedKey||'').trim());
|
||||||
|
}
|
||||||
msg.textContent = data.message || '删除成功';
|
msg.textContent = data.message || '删除成功';
|
||||||
msg.className='notice success';
|
msg.className='notice success';
|
||||||
msg.style.display='block';
|
msg.style.display='block';
|
||||||
@@ -99,13 +131,50 @@
|
|||||||
ov.style.display='none';
|
ov.style.display='none';
|
||||||
}
|
}
|
||||||
function selectedValues(sel){return Array.from(sel.selectedOptions).map(o=>o.value);}
|
function selectedValues(sel){return Array.from(sel.selectedOptions).map(o=>o.value);}
|
||||||
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){
|
||||||
function clearSelection(id){ const sel=document.getElementById(id); Array.from(sel.options).forEach(o=>o.selected=false); }
|
sel.addEventListener('mousedown', function(e){
|
||||||
|
if(e.target && e.target.tagName==='OPTION'){
|
||||||
|
e.preventDefault();
|
||||||
|
const op=e.target;
|
||||||
|
if (op.disabled) return;
|
||||||
|
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; });
|
||||||
|
}
|
||||||
async function generateCode(){
|
async function generateCode(){
|
||||||
const ov=document.getElementById('overlay'); ov.style.display='flex';
|
const ov=document.getElementById('overlay'); ov.style.display='flex';
|
||||||
const csrftoken=getCookie('csrftoken');
|
const csrftoken=getCookie('csrftoken');
|
||||||
const keys=selectedValues(document.getElementById('keys'));
|
const keySel = document.getElementById('keys');
|
||||||
const manageKeys=selectedValues(document.getElementById('manageKeys'));
|
let keys=selectedValues(keySel);
|
||||||
|
if ((!IS_ADMIN) && HAS_MANAGE_KEY) {
|
||||||
|
const selected = new Set(keys.map(k=>String(k||'').trim()).filter(Boolean));
|
||||||
|
const missing = Array.from(MY_KEYS_SET).filter(k => !selected.has(k));
|
||||||
|
if (missing.length) {
|
||||||
|
const msg=document.getElementById('msg');
|
||||||
|
msg.textContent = `必须选择导师原有的 key:${missing.join('、')}`;
|
||||||
|
msg.className='notice error';
|
||||||
|
msg.style.display='block';
|
||||||
|
ov.style.display='none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let manageKeys=selectedValues(document.getElementById('manageKeys'));
|
||||||
|
if ((!IS_ADMIN) && HAS_MANAGE_KEY) {
|
||||||
|
const hasForbidden = manageKeys.some(k => !ALLOWED_MANAGE_KEYS_SET.has(String(k || '').trim()));
|
||||||
|
if (hasForbidden) {
|
||||||
|
const msg=document.getElementById('msg');
|
||||||
|
msg.textContent='manage_key 只能选择本页新增的 key';
|
||||||
|
msg.className='notice error';
|
||||||
|
msg.style.display='block';
|
||||||
|
ov.style.display='none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
const mode=document.getElementById('expireMode').value;
|
const mode=document.getElementById('expireMode').value;
|
||||||
let days=30; if(mode==='month') days=30; else if(mode==='fouryears') days=1460; else { const d=parseInt(document.getElementById('customDays').value||'30'); days=isNaN(d)?30:Math.max(1,d);}
|
let days=30; if(mode==='month') days=30; else if(mode==='fouryears') days=1460; else { const d=parseInt(document.getElementById('customDays').value||'30'); days=isNaN(d)?30:Math.max(1,d);}
|
||||||
const resp=await fetch('/elastic/registration-codes/generate/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({keys,manage_keys:manageKeys,expires_in_days:days})});
|
const resp=await fetch('/elastic/registration-codes/generate/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({keys,manage_keys:manageKeys,expires_in_days:days})});
|
||||||
@@ -138,7 +207,12 @@
|
|||||||
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 ''; }
|
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'; } }
|
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('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();});
|
document.addEventListener('DOMContentLoaded',()=>{
|
||||||
|
loadKeys();
|
||||||
|
enableToggleSelect(document.getElementById('keys'));
|
||||||
|
enableToggleSelect(document.getElementById('manageKeys'));
|
||||||
|
loadCodes();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -155,16 +229,20 @@
|
|||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="card fade-in">
|
<div class="card fade-in">
|
||||||
<h2>管理注册码</h2>
|
<h2>管理注册码</h2>
|
||||||
|
{% if is_admin or has_manage_key or can_manage_registration_codes %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<label>管理 Key</label>
|
<label>管理 Key</label>
|
||||||
<div style="display:flex; gap:8px;">
|
<div style="display:flex; gap:8px;">
|
||||||
<input id="newKey" type="text" placeholder="输入新的key进行新增,或在下方选择后删除" style="flex: 1;" />
|
<input id="newKey" type="text" placeholder="输入新的key进行新增,或在下方选择后删除" style="flex: 1;" />
|
||||||
<button class="btn btn-secondary" onclick="addKey()">新增 Key</button>
|
<button class="btn btn-secondary" onclick="addKey()">新增 Key</button>
|
||||||
|
{% if is_admin or has_manage_key %}
|
||||||
<button class="btn btn-danger" onclick="deleteSelectedKey()">删除选中 Key</button>
|
<button class="btn btn-danger" onclick="deleteSelectedKey()">删除选中 Key</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="row" style="margin-top:12px;">
|
<div class="row" style="margin-top:12px;">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<label>选择 keys</label>
|
<label>选择 keys</label>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@
|
|||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
margin: 10% auto;
|
margin: 6% auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
@@ -207,6 +207,71 @@
|
|||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.keys-box {
|
||||||
|
max-height: 140px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #111827;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-item input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-edit-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-keys {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #1f2937;
|
||||||
|
border: 1px solid #c7d2fe;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-tag button {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-tag.locked {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -306,7 +371,7 @@
|
|||||||
<label for="username">用户名</label>
|
<label for="username">用户名</label>
|
||||||
<input type="text" id="username" name="username" required>
|
<input type="text" id="username" name="username" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group" id="permissionGroup">
|
||||||
<label for="permission">权限</label>
|
<label for="permission">权限</label>
|
||||||
<select id="permission" name="permission" required>
|
<select id="permission" name="permission" required>
|
||||||
<option value="0">管理员</option>
|
<option value="0">管理员</option>
|
||||||
@@ -314,6 +379,28 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Key(从已有 Key 中选择)</label>
|
||||||
|
<div class="key-edit-row">
|
||||||
|
<select id="userKeySelect"></select>
|
||||||
|
<button type="button" id="addUserKeyBtn" class="btn btn-primary">添加</button>
|
||||||
|
<button type="button" id="clearUserKeyBtn" class="btn">清空</button>
|
||||||
|
</div>
|
||||||
|
<div id="userKeysSelected" class="selected-keys"></div>
|
||||||
|
<div id="userKeysReadonlyGroup" style="display:none; margin-top: 10px;">
|
||||||
|
<div style="font-weight: 600; color: #374151; font-size: 13px; margin-bottom: 6px;">导师Key(不可修改)</div>
|
||||||
|
<div id="userKeysReadonly" class="selected-keys"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="manageKeyGroup">
|
||||||
|
<label>Manage Key(从已有 Key 中选择)</label>
|
||||||
|
<div class="key-edit-row">
|
||||||
|
<select id="userManageKeySelect"></select>
|
||||||
|
<button type="button" id="addUserManageKeyBtn" class="btn btn-primary">添加</button>
|
||||||
|
<button type="button" id="clearUserManageKeyBtn" class="btn">清空</button>
|
||||||
|
</div>
|
||||||
|
<div id="userManageKeysSelected" class="selected-keys"></div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">密码</label>
|
<label for="password">密码</label>
|
||||||
<input type="password" id="password" name="password" required>
|
<input type="password" id="password" name="password" required>
|
||||||
@@ -340,6 +427,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const IS_ADMIN = {{ is_admin|yesno:"true,false" }};
|
||||||
|
const IS_TUTOR = {{ is_tutor|yesno:"true,false" }};
|
||||||
|
const MY_MANAGE_KEYS_RAW = JSON.parse('{{ manage_keys_json|default:"[]"|escapejs }}');
|
||||||
|
const MY_KEYS_RAW = JSON.parse('{{ my_keys_json|default:"[]"|escapejs }}');
|
||||||
|
let KEY_OPTIONS_CACHE = null;
|
||||||
|
let MODAL_SELECTED_KEYS = [];
|
||||||
|
let MODAL_SELECTED_MANAGE_KEYS = [];
|
||||||
|
|
||||||
// 获取CSRF令牌的函数
|
// 获取CSRF令牌的函数
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
const value = `; ${document.cookie}`;
|
const value = `; ${document.cookie}`;
|
||||||
@@ -430,10 +525,7 @@
|
|||||||
if (!select) return;
|
if (!select) return;
|
||||||
select.innerHTML = '<option value="">全部Key</option>';
|
select.innerHTML = '<option value="">全部Key</option>';
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/elastic/keys-for-filter/', { credentials: 'same-origin' });
|
const keys = await fetchKeyOptions();
|
||||||
const data = await resp.json();
|
|
||||||
if (data.status !== 'success') return;
|
|
||||||
const keys = data.data || [];
|
|
||||||
keys.forEach(k => {
|
keys.forEach(k => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = String(k || '').trim();
|
opt.value = String(k || '').trim();
|
||||||
@@ -448,22 +540,205 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeStr(v) {
|
||||||
|
return String(v || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const MY_MANAGE_KEYS = (Array.isArray(MY_MANAGE_KEYS_RAW) ? MY_MANAGE_KEYS_RAW : [])
|
||||||
|
.map(normalizeStr)
|
||||||
|
.filter(Boolean);
|
||||||
|
const MY_MANAGE_KEYS_SET = new Set(MY_MANAGE_KEYS);
|
||||||
|
const MY_KEYS = (Array.isArray(MY_KEYS_RAW) ? MY_KEYS_RAW : [])
|
||||||
|
.map(normalizeStr)
|
||||||
|
.filter(Boolean);
|
||||||
|
const MY_KEYS_SET = new Set(MY_KEYS);
|
||||||
|
|
||||||
|
async function fetchKeyOptions() {
|
||||||
|
if (Array.isArray(KEY_OPTIONS_CACHE)) return KEY_OPTIONS_CACHE;
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/elastic/keys-for-filter/', { credentials: 'same-origin' });
|
||||||
|
const data = await resp.json();
|
||||||
|
if (data.status !== 'success') return [];
|
||||||
|
const keys = (data.data || []).map(normalizeStr).filter(Boolean);
|
||||||
|
KEY_OPTIONS_CACHE = keys;
|
||||||
|
return keys;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectOptions(selectId, options) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select) return;
|
||||||
|
select.innerHTML = '<option value="">请选择Key</option>';
|
||||||
|
(options || []).forEach(k => {
|
||||||
|
const s = normalizeStr(k);
|
||||||
|
if (!s) return;
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = s;
|
||||||
|
opt.textContent = s;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectOptionsMixed(selectId, enabledOptions, disabledOptions) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select) return;
|
||||||
|
select.innerHTML = '<option value="">请选择Key</option>';
|
||||||
|
(enabledOptions || []).forEach(k => {
|
||||||
|
const s = normalizeStr(k);
|
||||||
|
if (!s) return;
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = s;
|
||||||
|
opt.textContent = s;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
(disabledOptions || []).forEach(k => {
|
||||||
|
const s = normalizeStr(k);
|
||||||
|
if (!s) return;
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = s;
|
||||||
|
opt.textContent = s;
|
||||||
|
opt.disabled = true;
|
||||||
|
select.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSelectedTags(containerId, selectedArr) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = '';
|
||||||
|
(selectedArr || []).forEach(k => {
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.className = 'key-tag';
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.textContent = k;
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.textContent = '×';
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const idx = selectedArr.indexOf(k);
|
||||||
|
if (idx >= 0) selectedArr.splice(idx, 1);
|
||||||
|
renderSelectedTags(containerId, selectedArr);
|
||||||
|
});
|
||||||
|
tag.appendChild(text);
|
||||||
|
tag.appendChild(btn);
|
||||||
|
container.appendChild(tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderReadonlyTags(containerId, keysArr) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!container) return;
|
||||||
|
container.innerHTML = '';
|
||||||
|
(keysArr || []).forEach(k => {
|
||||||
|
const tag = document.createElement('span');
|
||||||
|
tag.className = 'key-tag locked';
|
||||||
|
const text = document.createElement('span');
|
||||||
|
text.textContent = k;
|
||||||
|
tag.appendChild(text);
|
||||||
|
container.appendChild(tag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setReadonlyKeysVisible(visible) {
|
||||||
|
const group = document.getElementById('userKeysReadonlyGroup');
|
||||||
|
if (group) group.style.display = visible ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setKeyEditorDisabled(prefix, disabled) {
|
||||||
|
const select = document.getElementById(prefix + 'Select');
|
||||||
|
const addBtn = document.getElementById('add' + prefix.charAt(0).toUpperCase() + prefix.slice(1) + 'Btn');
|
||||||
|
const clearBtn = document.getElementById('clear' + prefix.charAt(0).toUpperCase() + prefix.slice(1) + 'Btn');
|
||||||
|
if (select) select.disabled = !!disabled;
|
||||||
|
if (addBtn) addBtn.disabled = !!disabled;
|
||||||
|
if (clearBtn) clearBtn.disabled = !!disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addFromSelect(selectId, selectedArr, renderId) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select) return;
|
||||||
|
const v = normalizeStr(select.value);
|
||||||
|
if (!v) return;
|
||||||
|
if (!selectedArr.includes(v)) selectedArr.push(v);
|
||||||
|
renderSelectedTags(renderId, selectedArr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelected(selectedArr, renderId) {
|
||||||
|
selectedArr.length = 0;
|
||||||
|
renderSelectedTags(renderId, selectedArr);
|
||||||
|
}
|
||||||
|
|
||||||
// 打开添加用户模态框
|
// 打开添加用户模态框
|
||||||
function openAddModal() {
|
async function openAddModal() {
|
||||||
document.getElementById('modalTitle').textContent = '添加用户';
|
document.getElementById('modalTitle').textContent = '添加用户';
|
||||||
document.getElementById('userForm').reset();
|
document.getElementById('userForm').reset();
|
||||||
document.getElementById('userId').value = '';
|
document.getElementById('userId').value = '';
|
||||||
|
document.getElementById('username').disabled = false;
|
||||||
|
document.getElementById('permission').disabled = false;
|
||||||
|
document.getElementById('permissionGroup').style.display = '';
|
||||||
|
document.getElementById('manageKeyGroup').style.display = '';
|
||||||
|
const options = await fetchKeyOptions();
|
||||||
|
if ((!IS_ADMIN) && IS_TUTOR) {
|
||||||
|
const enabled = (options || []).map(normalizeStr).filter(k => k && !MY_KEYS_SET.has(k));
|
||||||
|
setSelectOptionsMixed('userKeySelect', enabled, MY_KEYS);
|
||||||
|
} else {
|
||||||
|
setSelectOptions('userKeySelect', options);
|
||||||
|
}
|
||||||
|
setSelectOptions('userManageKeySelect', options);
|
||||||
|
MODAL_SELECTED_KEYS = [];
|
||||||
|
MODAL_SELECTED_MANAGE_KEYS = [];
|
||||||
|
renderSelectedTags('userKeysSelected', MODAL_SELECTED_KEYS);
|
||||||
|
renderSelectedTags('userManageKeysSelected', MODAL_SELECTED_MANAGE_KEYS);
|
||||||
|
setReadonlyKeysVisible(false);
|
||||||
|
renderReadonlyTags('userKeysReadonly', []);
|
||||||
|
setKeyEditorDisabled('userKey', false);
|
||||||
|
setKeyEditorDisabled('userManageKey', false);
|
||||||
document.getElementById('password').required = true;
|
document.getElementById('password').required = true;
|
||||||
document.getElementById('confirmPassword').required = true;
|
document.getElementById('confirmPassword').required = true;
|
||||||
document.getElementById('userModal').style.display = 'block';
|
document.getElementById('userModal').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开编辑用户模态框
|
// 打开编辑用户模态框
|
||||||
function openEditModal(user) {
|
async function openEditModal(user) {
|
||||||
document.getElementById('modalTitle').textContent = '编辑用户';
|
document.getElementById('modalTitle').textContent = '编辑用户';
|
||||||
document.getElementById('username').value = user.username;
|
document.getElementById('username').value = user.username;
|
||||||
document.getElementById('userId').value = user.user_id;
|
document.getElementById('userId').value = user.user_id;
|
||||||
document.getElementById('permission').value = user.permission;
|
document.getElementById('permission').value = user.permission;
|
||||||
|
const options = await fetchKeyOptions();
|
||||||
|
setSelectOptions('userManageKeySelect', options);
|
||||||
|
const allUserKeys = (Array.isArray(user.key) ? user.key : (user.key ? [user.key] : [])).map(normalizeStr).filter(Boolean);
|
||||||
|
const lockedKeys = allUserKeys.filter(k => MY_KEYS_SET.has(k));
|
||||||
|
if ((!IS_ADMIN) && IS_TUTOR) {
|
||||||
|
const enabled = (options || []).map(normalizeStr).filter(k => k && !MY_KEYS_SET.has(k));
|
||||||
|
setSelectOptionsMixed('userKeySelect', enabled, MY_KEYS);
|
||||||
|
} else {
|
||||||
|
setSelectOptions('userKeySelect', options);
|
||||||
|
}
|
||||||
|
MODAL_SELECTED_KEYS = IS_ADMIN ? allUserKeys : allUserKeys.filter(k => !MY_KEYS_SET.has(k));
|
||||||
|
MODAL_SELECTED_MANAGE_KEYS = (Array.isArray(user.manage_key) ? user.manage_key : (user.manage_key ? [user.manage_key] : [])).map(normalizeStr).filter(Boolean);
|
||||||
|
MODAL_SELECTED_KEYS = Array.from(new Set(MODAL_SELECTED_KEYS));
|
||||||
|
MODAL_SELECTED_MANAGE_KEYS = Array.from(new Set(MODAL_SELECTED_MANAGE_KEYS));
|
||||||
|
renderSelectedTags('userKeysSelected', MODAL_SELECTED_KEYS);
|
||||||
|
renderSelectedTags('userManageKeysSelected', MODAL_SELECTED_MANAGE_KEYS);
|
||||||
|
setReadonlyKeysVisible((!IS_ADMIN) && IS_TUTOR && lockedKeys.length > 0);
|
||||||
|
renderReadonlyTags('userKeysReadonly', ((!IS_ADMIN) && IS_TUTOR) ? Array.from(new Set(lockedKeys)) : []);
|
||||||
|
|
||||||
|
if (IS_ADMIN) {
|
||||||
|
document.getElementById('username').disabled = false;
|
||||||
|
document.getElementById('permission').disabled = false;
|
||||||
|
document.getElementById('permissionGroup').style.display = '';
|
||||||
|
document.getElementById('manageKeyGroup').style.display = '';
|
||||||
|
setKeyEditorDisabled('userKey', false);
|
||||||
|
setKeyEditorDisabled('userManageKey', false);
|
||||||
|
} else {
|
||||||
|
document.getElementById('username').disabled = true;
|
||||||
|
document.getElementById('permission').disabled = true;
|
||||||
|
document.getElementById('permissionGroup').style.display = 'none';
|
||||||
|
document.getElementById('manageKeyGroup').style.display = 'none';
|
||||||
|
setKeyEditorDisabled('userKey', !IS_TUTOR);
|
||||||
|
setKeyEditorDisabled('userManageKey', true);
|
||||||
|
}
|
||||||
document.getElementById('password').required = false;
|
document.getElementById('password').required = false;
|
||||||
document.getElementById('confirmPassword').required = false;
|
document.getElementById('confirmPassword').required = false;
|
||||||
document.getElementById('userModal').style.display = 'block';
|
document.getElementById('userModal').style.display = 'block';
|
||||||
@@ -499,10 +774,15 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {};
|
||||||
username: username,
|
if (IS_ADMIN) {
|
||||||
permission: parseInt(permission)
|
data.username = username;
|
||||||
};
|
data.permission = parseInt(permission);
|
||||||
|
data.key = MODAL_SELECTED_KEYS;
|
||||||
|
data.manage_key = MODAL_SELECTED_MANAGE_KEYS;
|
||||||
|
} else {
|
||||||
|
data.key = MODAL_SELECTED_KEYS;
|
||||||
|
}
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
data.password = password;
|
data.password = password;
|
||||||
@@ -539,7 +819,9 @@
|
|||||||
if (result.status === 'success') {
|
if (result.status === 'success') {
|
||||||
showNotification(userId ? '用户更新成功' : '用户添加成功');
|
showNotification(userId ? '用户更新成功' : '用户添加成功');
|
||||||
document.getElementById('userModal').style.display = 'none';
|
document.getElementById('userModal').style.display = 'none';
|
||||||
loadUsers();
|
const searchTerm = (document.getElementById('searchInput') || {}).value || '';
|
||||||
|
const key = (document.getElementById('keyFilter') || {}).value || '';
|
||||||
|
loadUsers(searchTerm, key);
|
||||||
} else {
|
} else {
|
||||||
showNotification(result.message || '操作失败', false);
|
showNotification(result.message || '操作失败', false);
|
||||||
}
|
}
|
||||||
@@ -624,6 +906,31 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addUserKeyBtn = document.getElementById('addUserKeyBtn');
|
||||||
|
if (addUserKeyBtn) {
|
||||||
|
addUserKeyBtn.addEventListener('click', function() {
|
||||||
|
addFromSelect('userKeySelect', MODAL_SELECTED_KEYS, 'userKeysSelected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const clearUserKeyBtn = document.getElementById('clearUserKeyBtn');
|
||||||
|
if (clearUserKeyBtn) {
|
||||||
|
clearUserKeyBtn.addEventListener('click', function() {
|
||||||
|
clearSelected(MODAL_SELECTED_KEYS, 'userKeysSelected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const addUserManageKeyBtn = document.getElementById('addUserManageKeyBtn');
|
||||||
|
if (addUserManageKeyBtn) {
|
||||||
|
addUserManageKeyBtn.addEventListener('click', function() {
|
||||||
|
addFromSelect('userManageKeySelect', MODAL_SELECTED_MANAGE_KEYS, 'userManageKeysSelected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const clearUserManageKeyBtn = document.getElementById('clearUserManageKeyBtn');
|
||||||
|
if (clearUserManageKeyBtn) {
|
||||||
|
clearUserManageKeyBtn.addEventListener('click', function() {
|
||||||
|
clearSelected(MODAL_SELECTED_MANAGE_KEYS, 'userManageKeysSelected');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 点击模态框外部关闭模态框
|
// 点击模态框外部关闭模态框
|
||||||
window.addEventListener('click', function(event) {
|
window.addEventListener('click', function(event) {
|
||||||
const modals = document.querySelectorAll('.modal');
|
const modals = document.querySelectorAll('.modal');
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ 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/keys/remove/', views.remove_key_view, name='remove_key'),
|
path('registration-codes/keys/remove/', views.remove_key_view, name='remove_key'),
|
||||||
|
path('registration-codes/keys/unallow/', views.unallow_tutor_added_key_view, name='unallow_tutor_added_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/list/', views.list_registration_codes_view, name='list_registration_codes'),
|
||||||
path('registration-codes/revoke/', views.revoke_registration_code_view, name='revoke_registration_code'),
|
path('registration-codes/revoke/', views.revoke_registration_code_view, name='revoke_registration_code'),
|
||||||
|
|||||||
267
elastic/views.py
267
elastic/views.py
@@ -489,6 +489,8 @@ def update_user_by_id_view(request, user_id):
|
|||||||
new_username = (payload.get("username") or "").strip()
|
new_username = (payload.get("username") or "").strip()
|
||||||
new_permission = payload.get("permission")
|
new_permission = payload.get("permission")
|
||||||
new_password = (payload.get("password") or "").strip()
|
new_password = (payload.get("password") or "").strip()
|
||||||
|
raw_keys = payload.get("key", None)
|
||||||
|
raw_manage_keys = payload.get("manage_key", None)
|
||||||
if new_password and len(new_password) < 6:
|
if new_password and len(new_password) < 6:
|
||||||
return JsonResponse({"status": "error", "message": "密码长度至少为6位"}, status=400)
|
return JsonResponse({"status": "error", "message": "密码长度至少为6位"}, status=400)
|
||||||
|
|
||||||
@@ -498,6 +500,28 @@ def update_user_by_id_view(request, user_id):
|
|||||||
requester_mgr = set(requester.get("manage_key") or [])
|
requester_mgr = set(requester.get("manage_key") or [])
|
||||||
target_keys = set(target.get("key") or [])
|
target_keys = set(target.get("key") or [])
|
||||||
|
|
||||||
|
def normalize_keys(v):
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
if isinstance(v, list):
|
||||||
|
parts = v
|
||||||
|
elif isinstance(v, str):
|
||||||
|
parts = re.split(r"[,,;;、\r\n]+", v)
|
||||||
|
else:
|
||||||
|
parts = [v]
|
||||||
|
out = []
|
||||||
|
seen = set()
|
||||||
|
for p in parts:
|
||||||
|
s = str(p).strip().strip(";")
|
||||||
|
if not s or s in seen:
|
||||||
|
continue
|
||||||
|
seen.add(s)
|
||||||
|
out.append(s)
|
||||||
|
return out
|
||||||
|
|
||||||
|
new_keys = normalize_keys(raw_keys)
|
||||||
|
new_manage_keys = normalize_keys(raw_manage_keys)
|
||||||
|
|
||||||
if is_admin:
|
if is_admin:
|
||||||
if new_username:
|
if new_username:
|
||||||
other = get_user_by_username(new_username)
|
other = get_user_by_username(new_username)
|
||||||
@@ -508,6 +532,8 @@ def update_user_by_id_view(request, user_id):
|
|||||||
username=new_username if new_username else None,
|
username=new_username if new_username else None,
|
||||||
permission=int(new_permission) if new_permission is not None else None,
|
permission=int(new_permission) if new_permission is not None else None,
|
||||||
password=new_password if new_password else None,
|
password=new_password if new_password else None,
|
||||||
|
key=new_keys,
|
||||||
|
manage_key=new_manage_keys,
|
||||||
)
|
)
|
||||||
return JsonResponse({"status": "success"}) if ok else JsonResponse({"status": "error", "message": "用户更新失败"}, status=500)
|
return JsonResponse({"status": "success"}) if ok else JsonResponse({"status": "error", "message": "用户更新失败"}, status=500)
|
||||||
|
|
||||||
@@ -518,9 +544,25 @@ def update_user_by_id_view(request, user_id):
|
|||||||
return JsonResponse({"status": "success"}) if ok else JsonResponse({"status": "error", "message": "用户更新失败"}, status=500)
|
return JsonResponse({"status": "success"}) if ok else JsonResponse({"status": "error", "message": "用户更新失败"}, status=500)
|
||||||
|
|
||||||
if requester_mgr and (target_keys & requester_mgr):
|
if requester_mgr and (target_keys & requester_mgr):
|
||||||
if not new_password or new_username or new_permission is not None:
|
if new_username or new_permission is not None or new_manage_keys is not None:
|
||||||
return JsonResponse({"status": "error", "message": "导师仅允许修改密码"}, status=403)
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
ok = es_update_user_by_id(user_id, password=new_password)
|
if not new_password and new_keys is None:
|
||||||
|
return JsonResponse({"status": "error", "message": "缺少更新内容"}, status=400)
|
||||||
|
merged_keys = None
|
||||||
|
if new_keys is not None:
|
||||||
|
try:
|
||||||
|
new_keys_set = set(new_keys)
|
||||||
|
except Exception:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
requester_locked = set(normalize_keys(requester.get("key")) or [])
|
||||||
|
if new_keys_set & requester_locked:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
existing_keys = [str(v).strip() for v in list(target.get("key") or []) if str(v).strip()]
|
||||||
|
preserved = [k for k in existing_keys if k in requester_locked]
|
||||||
|
merged_keys = preserved + [str(v).strip() for v in list(new_keys) if str(v).strip() and str(v).strip() not in requester_locked]
|
||||||
|
seen = set()
|
||||||
|
merged_keys = [k for k in merged_keys if not (k in seen or seen.add(k))]
|
||||||
|
ok = es_update_user_by_id(user_id, password=new_password if new_password else None, key=merged_keys)
|
||||||
return JsonResponse({"status": "success"}) if ok else JsonResponse({"status": "error", "message": "用户更新失败"}, status=500)
|
return JsonResponse({"status": "success"}) if ok else JsonResponse({"status": "error", "message": "用户更新失败"}, status=500)
|
||||||
|
|
||||||
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
@@ -1019,17 +1061,39 @@ def analytics_recent_view(request):
|
|||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@csrf_protect
|
@csrf_protect
|
||||||
def remove_key_view(request):
|
def remove_key_view(request):
|
||||||
|
if request.session.get("user_id") is None:
|
||||||
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8"))
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
key_to_remove = payload.get("key")
|
key_to_remove = payload.get("key")
|
||||||
|
|
||||||
if not key_to_remove:
|
if not key_to_remove:
|
||||||
return JsonResponse({"status": "error", "message": "缺少key参数"}, status=400)
|
return JsonResponse({"status": "error", "message": "缺少key参数"}, status=400)
|
||||||
|
key_to_remove = str(key_to_remove).strip()
|
||||||
|
if not key_to_remove:
|
||||||
|
return JsonResponse({"status": "error", "message": "缺少key参数"}, status=400)
|
||||||
|
|
||||||
|
if not is_admin:
|
||||||
|
me = get_user_by_id(request.session.get("user_id")) or {}
|
||||||
|
if not (me.get("manage_key") or []):
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
allowed = {str(x).strip() for x in list(request.session.get("tutor_added_manage_keys") or []) if str(x).strip()}
|
||||||
|
if key_to_remove not in allowed:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
|
||||||
from .es_connect import delete_key_globally
|
from .es_connect import delete_key_globally
|
||||||
ok, count = delete_key_globally(key_to_remove)
|
ok, count = delete_key_globally(key_to_remove)
|
||||||
|
|
||||||
if ok:
|
if ok:
|
||||||
|
if not is_admin:
|
||||||
|
cur = [str(x).strip() for x in list(request.session.get("tutor_added_manage_keys") or []) if str(x).strip()]
|
||||||
|
cur = [k for k in cur if k != key_to_remove]
|
||||||
|
request.session["tutor_added_manage_keys"] = cur
|
||||||
|
try:
|
||||||
|
request.session.modified = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return JsonResponse({"status": "success", "message": f"已成功全局删除 Key '{key_to_remove}',并同步清理了 {count} 个注册码。"})
|
return JsonResponse({"status": "success", "message": f"已成功全局删除 Key '{key_to_remove}',并同步清理了 {count} 个注册码。"})
|
||||||
else:
|
else:
|
||||||
return JsonResponse({"status": "error", "message": "删除失败"}, status=500)
|
return JsonResponse({"status": "error", "message": "删除失败"}, status=500)
|
||||||
@@ -1039,6 +1103,35 @@ def remove_key_view(request):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@csrf_protect
|
||||||
|
def unallow_tutor_added_key_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)
|
||||||
|
me = get_user_by_id(request.session.get("user_id")) or {}
|
||||||
|
if not (me.get("manage_key") or []):
|
||||||
|
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)
|
||||||
|
key_name = (payload.get("key") or "").strip()
|
||||||
|
if not key_name:
|
||||||
|
return JsonResponse({"status": "error", "message": "key不能为空"}, status=400)
|
||||||
|
cur = list(request.session.get("tutor_added_manage_keys") or [])
|
||||||
|
cur = [str(x).strip() for x in cur if str(x).strip()]
|
||||||
|
if key_name not in cur:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
cur = [k for k in cur if k != key_name]
|
||||||
|
request.session["tutor_added_manage_keys"] = cur
|
||||||
|
try:
|
||||||
|
request.session.modified = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return JsonResponse({"status": "success"})
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
def user_manage(request):
|
def user_manage(request):
|
||||||
@@ -1049,6 +1142,15 @@ def user_manage(request):
|
|||||||
is_admin = int(request.session.get("permission", 1)) == 0
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
me = get_user_by_id(session_user_id) or {}
|
me = get_user_by_id(session_user_id) or {}
|
||||||
has_manage = bool(me.get("manage_key"))
|
has_manage = bool(me.get("manage_key"))
|
||||||
|
manage_keys = list(me.get("manage_key") or [])
|
||||||
|
raw_my_keys = me.get("key") or []
|
||||||
|
if isinstance(raw_my_keys, list):
|
||||||
|
my_keys = raw_my_keys
|
||||||
|
elif isinstance(raw_my_keys, str):
|
||||||
|
my_keys = re.split(r"[,,;;、\r\n]+", raw_my_keys)
|
||||||
|
else:
|
||||||
|
my_keys = [raw_my_keys]
|
||||||
|
my_keys = [str(x).strip() for x in my_keys if str(x).strip()]
|
||||||
user_id_qs = request.GET.get("user_id")
|
user_id_qs = request.GET.get("user_id")
|
||||||
context = {
|
context = {
|
||||||
"user_id": user_id_qs or session_user_id,
|
"user_id": user_id_qs or session_user_id,
|
||||||
@@ -1056,6 +1158,8 @@ def user_manage(request):
|
|||||||
"is_admin": is_admin,
|
"is_admin": is_admin,
|
||||||
"is_tutor": (not is_admin) and has_manage,
|
"is_tutor": (not is_admin) and has_manage,
|
||||||
"is_student": (not is_admin) and (not has_manage),
|
"is_student": (not is_admin) and (not has_manage),
|
||||||
|
"manage_keys_json": json.dumps(manage_keys, ensure_ascii=False),
|
||||||
|
"my_keys_json": json.dumps(my_keys, ensure_ascii=False),
|
||||||
}
|
}
|
||||||
return render(request, "elastic/users.html", context)
|
return render(request, "elastic/users.html", context)
|
||||||
|
|
||||||
@@ -1066,14 +1170,36 @@ def registration_code_manage_page(request):
|
|||||||
if session_user_id is None:
|
if session_user_id is None:
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
return redirect("/accounts/login/")
|
return redirect("/accounts/login/")
|
||||||
if int(request.session.get("permission", 1)) != 0:
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
|
me = get_user_by_id(session_user_id) or {}
|
||||||
|
has_manage = bool(me.get("manage_key"))
|
||||||
|
can_manage_reg = int(me.get("can_manage_registration_codes") or 0) == 1
|
||||||
|
if (not is_admin) and (not has_manage) and (not can_manage_reg):
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
return redirect("/main/home/")
|
return redirect("/main/home/")
|
||||||
user_id_qs = request.GET.get("user_id")
|
user_id_qs = request.GET.get("user_id")
|
||||||
me = get_user_by_id(session_user_id) or {}
|
raw_my_keys = me.get("key") or []
|
||||||
|
if isinstance(raw_my_keys, list):
|
||||||
|
my_keys = raw_my_keys
|
||||||
|
elif isinstance(raw_my_keys, str):
|
||||||
|
my_keys = re.split(r"[,,;;、\r\n]+", raw_my_keys)
|
||||||
|
else:
|
||||||
|
my_keys = [raw_my_keys]
|
||||||
|
my_keys = [str(x).strip() for x in my_keys if str(x).strip()]
|
||||||
|
if can_manage_reg and (not has_manage) and (not is_admin):
|
||||||
|
allowed_manage_keys = [str(x).strip() for x in list(me.get("registration_manage_keys") or []) if str(x).strip()]
|
||||||
|
else:
|
||||||
|
allowed_manage_keys = list(request.session.get("tutor_added_manage_keys") or [])
|
||||||
|
allowed_manage_keys = [str(x).strip() for x in allowed_manage_keys if str(x).strip()]
|
||||||
context = {
|
context = {
|
||||||
"user_id": user_id_qs or session_user_id,
|
"user_id": user_id_qs or session_user_id,
|
||||||
"username": me.get("username"),
|
"username": me.get("username"),
|
||||||
|
"is_admin": is_admin,
|
||||||
|
"has_manage_key": has_manage,
|
||||||
|
"can_manage_registration_codes": can_manage_reg,
|
||||||
|
"my_keys_json": json.dumps(my_keys, ensure_ascii=False),
|
||||||
|
"manage_keys_json": json.dumps(list(me.get("manage_key") or []), ensure_ascii=False),
|
||||||
|
"allowed_manage_keys_json": json.dumps(allowed_manage_keys, ensure_ascii=False),
|
||||||
}
|
}
|
||||||
return render(request, "elastic/registration_codes.html", context)
|
return render(request, "elastic/registration_codes.html", context)
|
||||||
|
|
||||||
@@ -1081,8 +1207,16 @@ def registration_code_manage_page(request):
|
|||||||
def get_keys_list_view(request):
|
def get_keys_list_view(request):
|
||||||
if request.session.get("user_id") is None:
|
if request.session.get("user_id") is None:
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
if int(request.session.get("permission", 1)) != 0:
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
|
if not is_admin:
|
||||||
|
me = get_user_by_id(request.session.get("user_id")) or {}
|
||||||
|
has_manage = bool(me.get("manage_key") or [])
|
||||||
|
can_manage_reg = int(me.get("can_manage_registration_codes") or 0) == 1
|
||||||
|
if (not has_manage) and (not can_manage_reg):
|
||||||
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
if can_manage_reg and (not has_manage):
|
||||||
|
lst = [str(x).strip() for x in list(me.get("registration_manage_keys") or []) if str(x).strip()]
|
||||||
|
return JsonResponse({"status": "success", "data": lst})
|
||||||
lst = get_keys_list()
|
lst = get_keys_list()
|
||||||
return JsonResponse({"status": "success", "data": lst})
|
return JsonResponse({"status": "success", "data": lst})
|
||||||
|
|
||||||
@@ -1091,7 +1225,12 @@ def get_keys_list_view(request):
|
|||||||
def add_key_view(request):
|
def add_key_view(request):
|
||||||
if request.session.get("user_id") is None:
|
if request.session.get("user_id") is None:
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
if int(request.session.get("permission", 1)) != 0:
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
|
if not is_admin:
|
||||||
|
me = get_user_by_id(request.session.get("user_id")) or {}
|
||||||
|
has_manage = bool(me.get("manage_key") or [])
|
||||||
|
can_manage_reg = int(me.get("can_manage_registration_codes") or 0) == 1
|
||||||
|
if (not has_manage) and (not can_manage_reg):
|
||||||
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8"))
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
@@ -1100,9 +1239,29 @@ def add_key_view(request):
|
|||||||
key_name = (payload.get("key") or "").strip()
|
key_name = (payload.get("key") or "").strip()
|
||||||
if not key_name:
|
if not key_name:
|
||||||
return JsonResponse({"status": "error", "message": "key不能为空"}, status=400)
|
return JsonResponse({"status": "error", "message": "key不能为空"}, status=400)
|
||||||
|
cur_global = set(get_keys_list() or [])
|
||||||
|
if key_name in cur_global:
|
||||||
|
return JsonResponse({"status": "error", "message": "key已存在"}, status=409)
|
||||||
ok = ensure_key_in_list(key_name)
|
ok = ensure_key_in_list(key_name)
|
||||||
if not ok:
|
if not ok:
|
||||||
return JsonResponse({"status": "error", "message": "key已存在或写入失败"}, status=409)
|
return JsonResponse({"status": "error", "message": "写入失败"}, status=500)
|
||||||
|
if not is_admin:
|
||||||
|
uid = request.session.get("user_id")
|
||||||
|
if has_manage:
|
||||||
|
cur = list(request.session.get("tutor_added_manage_keys") or [])
|
||||||
|
cur = [str(x).strip() for x in cur if str(x).strip()]
|
||||||
|
if key_name not in cur:
|
||||||
|
cur.append(key_name)
|
||||||
|
request.session["tutor_added_manage_keys"] = cur
|
||||||
|
try:
|
||||||
|
request.session.modified = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
elif can_manage_reg:
|
||||||
|
cur = [str(x).strip() for x in list((me or {}).get("registration_manage_keys") or []) if str(x).strip()]
|
||||||
|
if key_name not in cur:
|
||||||
|
cur.append(key_name)
|
||||||
|
es_update_user_by_id(uid, registration_manage_keys=cur)
|
||||||
return JsonResponse({"status": "success"})
|
return JsonResponse({"status": "success"})
|
||||||
|
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
@@ -1110,7 +1269,13 @@ def add_key_view(request):
|
|||||||
def generate_registration_code_view(request):
|
def generate_registration_code_view(request):
|
||||||
if request.session.get("user_id") is None:
|
if request.session.get("user_id") is None:
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
if int(request.session.get("permission", 1)) != 0:
|
uid = request.session.get("user_id")
|
||||||
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
|
if not is_admin:
|
||||||
|
me = get_user_by_id(uid) or {}
|
||||||
|
has_manage = bool(me.get("manage_key") or [])
|
||||||
|
can_manage_reg = int(me.get("can_manage_registration_codes") or 0) == 1
|
||||||
|
if (not has_manage) and (not can_manage_reg):
|
||||||
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8"))
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
@@ -1118,11 +1283,75 @@ def generate_registration_code_view(request):
|
|||||||
return JsonResponse({"status": "error", "message": "JSON无效"}, status=400)
|
return JsonResponse({"status": "error", "message": "JSON无效"}, status=400)
|
||||||
keys = list(payload.get("keys") or [])
|
keys = list(payload.get("keys") or [])
|
||||||
manage_keys = list(payload.get("manage_keys") or [])
|
manage_keys = list(payload.get("manage_keys") or [])
|
||||||
|
if not is_admin:
|
||||||
|
if has_manage:
|
||||||
|
my_keys = []
|
||||||
|
try:
|
||||||
|
my_keys = [str(x).strip() for x in list((me or {}).get("key") or []) if str(x).strip()]
|
||||||
|
except Exception:
|
||||||
|
my_keys = []
|
||||||
|
allowed_manage = set()
|
||||||
|
try:
|
||||||
|
allowed_manage = {str(x).strip() for x in list(request.session.get("tutor_added_manage_keys") or []) if str(x).strip()}
|
||||||
|
except Exception:
|
||||||
|
allowed_manage = set()
|
||||||
|
normalized_keys = []
|
||||||
|
seen = set()
|
||||||
|
for v in list(keys or []):
|
||||||
|
s = str(v).strip()
|
||||||
|
if not s or s in seen:
|
||||||
|
continue
|
||||||
|
seen.add(s)
|
||||||
|
normalized_keys.append(s)
|
||||||
|
missing = [k for k in my_keys if k not in seen]
|
||||||
|
if missing:
|
||||||
|
return JsonResponse(
|
||||||
|
{"status": "error", "message": f"必须选择导师原有的 key:{'、'.join(missing)}"},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
keys = normalized_keys
|
||||||
|
clean_manage = []
|
||||||
|
manage_seen = set()
|
||||||
|
for v in list(manage_keys or []):
|
||||||
|
s = str(v).strip()
|
||||||
|
if not s or s in manage_seen:
|
||||||
|
continue
|
||||||
|
if s not in allowed_manage:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
manage_seen.add(s)
|
||||||
|
clean_manage.append(s)
|
||||||
|
manage_keys = clean_manage
|
||||||
|
else:
|
||||||
|
allowed = {str(x).strip() for x in list((me or {}).get("registration_manage_keys") or []) if str(x).strip()}
|
||||||
|
norm_keys = []
|
||||||
|
seen = set()
|
||||||
|
for v in list(keys or []):
|
||||||
|
s = str(v).strip()
|
||||||
|
if not s or s in seen:
|
||||||
|
continue
|
||||||
|
if s not in allowed:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
seen.add(s)
|
||||||
|
norm_keys.append(s)
|
||||||
|
if not norm_keys:
|
||||||
|
return JsonResponse({"status": "error", "message": "至少选择一个 key"}, status=400)
|
||||||
|
keys = norm_keys
|
||||||
|
clean_manage = []
|
||||||
|
manage_seen = set()
|
||||||
|
for v in list(manage_keys or []):
|
||||||
|
s = str(v).strip()
|
||||||
|
if not s or s in manage_seen:
|
||||||
|
continue
|
||||||
|
if s not in allowed:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
manage_seen.add(s)
|
||||||
|
clean_manage.append(s)
|
||||||
|
manage_keys = clean_manage
|
||||||
try:
|
try:
|
||||||
days = int(payload.get("expires_in_days", 30))
|
days = int(payload.get("expires_in_days", 30))
|
||||||
except Exception:
|
except Exception:
|
||||||
days = 30
|
days = 30
|
||||||
result = generate_registration_code(keys=keys, manage_keys=manage_keys, expires_in_days=days, created_by=request.session.get("user_id"))
|
result = generate_registration_code(keys=keys, manage_keys=manage_keys, expires_in_days=days, created_by=uid)
|
||||||
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})
|
||||||
@@ -1131,9 +1360,15 @@ def generate_registration_code_view(request):
|
|||||||
def list_registration_codes_view(request):
|
def list_registration_codes_view(request):
|
||||||
if request.session.get("user_id") is None:
|
if request.session.get("user_id") is None:
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
if int(request.session.get("permission", 1)) != 0:
|
uid = request.session.get("user_id")
|
||||||
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
|
if not is_admin:
|
||||||
|
me = get_user_by_id(uid) or {}
|
||||||
|
if (not (me.get("manage_key") or [])) and (int(me.get("can_manage_registration_codes") or 0) != 1):
|
||||||
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
data = list_registration_codes()
|
data = list_registration_codes()
|
||||||
|
if not is_admin:
|
||||||
|
data = [it for it in (data or []) if str(it.get("created_by")) == str(uid)]
|
||||||
return JsonResponse({"status": "success", "data": data})
|
return JsonResponse({"status": "success", "data": data})
|
||||||
|
|
||||||
|
|
||||||
@@ -1198,7 +1433,11 @@ def keys_for_filter_view(request):
|
|||||||
def revoke_registration_code_view(request):
|
def revoke_registration_code_view(request):
|
||||||
if request.session.get("user_id") is None:
|
if request.session.get("user_id") is None:
|
||||||
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
if int(request.session.get("permission", 1)) != 0:
|
uid = request.session.get("user_id")
|
||||||
|
is_admin = int(request.session.get("permission", 1)) == 0
|
||||||
|
if not is_admin:
|
||||||
|
me = get_user_by_id(uid) or {}
|
||||||
|
if (not (me.get("manage_key") or [])) and (int(me.get("can_manage_registration_codes") or 0) != 1):
|
||||||
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
try:
|
try:
|
||||||
payload = json.loads(request.body.decode("utf-8"))
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
@@ -1207,6 +1446,10 @@ def revoke_registration_code_view(request):
|
|||||||
code = (payload.get("code") or "").strip()
|
code = (payload.get("code") or "").strip()
|
||||||
if not code:
|
if not code:
|
||||||
return JsonResponse({"status": "error", "message": "缺少code"}, status=400)
|
return JsonResponse({"status": "error", "message": "缺少code"}, status=400)
|
||||||
|
if not is_admin:
|
||||||
|
info = get_registration_code(code)
|
||||||
|
if not info or str(info.get("created_by")) != str(uid):
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
ok = revoke_registration_code(code)
|
ok = revoke_registration_code(code)
|
||||||
if not ok:
|
if not ok:
|
||||||
return JsonResponse({"status": "error", "message": "作废失败"}, status=500)
|
return JsonResponse({"status": "error", "message": "作废失败"}, status=500)
|
||||||
|
|||||||
@@ -48,9 +48,15 @@
|
|||||||
<a href="{% url 'elastic:user_manage' %}" onclick="return handleNavClick(this, '/elastic/user_manage/');">用户管理</a>
|
<a href="{% url 'elastic:user_manage' %}" onclick="return handleNavClick(this, '/elastic/user_manage/');">用户管理</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="/accounts/profile/">个人中心</a>
|
<a href="/accounts/profile/">个人中心</a>
|
||||||
{% if is_admin %}
|
{% if is_admin or has_manage_key or can_manage_registration_codes %}
|
||||||
<a href="{% url 'elastic:registration_code_manage_page' %}" onclick="return handleNavClick(this, '/elastic/registration-codes/manage/');">注册码管理</a>
|
<a href="{% url 'elastic:registration_code_manage_page' %}" onclick="return handleNavClick(this, '/elastic/registration-codes/manage/');">注册码管理</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if is_admin %}
|
||||||
|
<a href="{% url 'accounts:registration_code_requests_page' %}">注册码申请管理</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if not is_admin and not has_manage_key and not can_manage_registration_codes and not has_registration_code %}
|
||||||
|
<a id="applyRegBtn" href="javascript:void(0)">申请注册码管理</a>
|
||||||
|
{% endif %}
|
||||||
<a id="logoutBtn">退出登录</a>
|
<a id="logoutBtn">退出登录</a>
|
||||||
<div id="logoutMsg"></div>
|
<div id="logoutMsg"></div>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@@ -89,6 +95,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="applyRegModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.45); z-index:3000; align-items:center; justify-content:center;">
|
||||||
|
<div class="card" style="width:min(560px, calc(100vw - 40px));">
|
||||||
|
<div class="header">
|
||||||
|
<h3 style="margin:0;">申请注册码管理权限</h3>
|
||||||
|
<button id="applyRegClose" class="btn" type="button" style="background:#e5e7eb;">关闭</button>
|
||||||
|
</div>
|
||||||
|
<div class="muted" style="margin-bottom:10px;">填写申请理由,管理员同意后可进入“注册码管理”页面。</div>
|
||||||
|
<div style="margin-top:10px;">
|
||||||
|
<label for="applyReason" style="display:block; margin-bottom:6px; font-weight:600;">申请理由</label>
|
||||||
|
<textarea id="applyReason" rows="5" style="width:100%; padding:10px 12px; border:1px solid #d1d5db; border-radius:10px; box-sizing:border-box; resize: vertical;"></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="applyRegMsg" class="muted" style="margin-top:10px;"></div>
|
||||||
|
<div style="display:flex; gap:10px; justify-content:flex-end; margin-top:14px;">
|
||||||
|
<button id="applyRegSubmit" class="btn btn-primary" type="button">提交申请</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// 获取CSRF令牌的函数
|
// 获取CSRF令牌的函数
|
||||||
function getCookie(name) {
|
function getCookie(name) {
|
||||||
@@ -155,6 +179,68 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const applyRegBtn = document.getElementById('applyRegBtn');
|
||||||
|
const applyRegModal = document.getElementById('applyRegModal');
|
||||||
|
const applyRegClose = document.getElementById('applyRegClose');
|
||||||
|
const applyRegSubmit = document.getElementById('applyRegSubmit');
|
||||||
|
const applyRegMsg = document.getElementById('applyRegMsg');
|
||||||
|
const applyReason = document.getElementById('applyReason');
|
||||||
|
|
||||||
|
function openApplyRegModal() {
|
||||||
|
if (!applyRegModal) return;
|
||||||
|
applyRegMsg.textContent = '';
|
||||||
|
applyReason.value = '';
|
||||||
|
applyRegModal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
function closeApplyRegModal() {
|
||||||
|
if (!applyRegModal) return;
|
||||||
|
applyRegModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (applyRegBtn) applyRegBtn.addEventListener('click', openApplyRegModal);
|
||||||
|
if (applyRegClose) applyRegClose.addEventListener('click', closeApplyRegModal);
|
||||||
|
if (applyRegModal) {
|
||||||
|
applyRegModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === applyRegModal) closeApplyRegModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (applyRegSubmit) {
|
||||||
|
applyRegSubmit.addEventListener('click', async () => {
|
||||||
|
const reason = (applyReason.value || '').trim();
|
||||||
|
if (!reason) {
|
||||||
|
applyRegMsg.textContent = '请填写申请理由';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyRegMsg.textContent = '提交中...';
|
||||||
|
const csrftoken = getCookie('csrftoken');
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/accounts/registration-code/request/submit/', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': csrftoken || ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ reason })
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
if (resp.ok && data.ok) {
|
||||||
|
applyRegMsg.textContent = '已提交申请,请等待管理员审核';
|
||||||
|
if (applyRegBtn) {
|
||||||
|
applyRegBtn.textContent = '已提交申请';
|
||||||
|
applyRegBtn.disabled = true;
|
||||||
|
applyRegBtn.style.opacity = '0.6';
|
||||||
|
applyRegBtn.style.cursor = 'not-allowed';
|
||||||
|
}
|
||||||
|
setTimeout(() => closeApplyRegModal(), 800);
|
||||||
|
} else {
|
||||||
|
applyRegMsg.textContent = (data && data.message) ? data.message : '提交失败';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
applyRegMsg.textContent = '提交失败';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function fetchJSON(url){ return fetch(url, {credentials:'same-origin'}).then(r=>r.json()); }
|
function fetchJSON(url){ return fetch(url, {credentials:'same-origin'}).then(r=>r.json()); }
|
||||||
function qs(params){ const u = new URLSearchParams(params); return u.toString(); }
|
function qs(params){ const u = new URLSearchParams(params); return u.toString(); }
|
||||||
|
|||||||
@@ -25,10 +25,14 @@ def home(request):
|
|||||||
except Exception:
|
except Exception:
|
||||||
perm = 1
|
perm = 1
|
||||||
has_manage_key = bool((u or {}).get("manage_key") or [])
|
has_manage_key = bool((u or {}).get("manage_key") or [])
|
||||||
|
can_manage_registration_codes = bool(int((u or {}).get("can_manage_registration_codes") or 0) == 1)
|
||||||
|
has_registration_code = bool(str((u or {}).get("registration_code") or "").strip())
|
||||||
context = {
|
context = {
|
||||||
"user_id": uid,
|
"user_id": uid,
|
||||||
"username": (u or {}).get("username"),
|
"username": (u or {}).get("username"),
|
||||||
"is_admin": (int(perm) == 0),
|
"is_admin": (int(perm) == 0),
|
||||||
"has_manage_key": has_manage_key,
|
"has_manage_key": has_manage_key,
|
||||||
|
"can_manage_registration_codes": can_manage_registration_codes,
|
||||||
|
"has_registration_code": has_registration_code,
|
||||||
}
|
}
|
||||||
return render(request, "main/home.html", context)
|
return render(request, "main/home.html", context)
|
||||||
|
|||||||
Reference in New Issue
Block a user