注册码生成以及用户注册
This commit is contained in:
@@ -42,6 +42,10 @@
|
|||||||
<button id="loginBtn" type="submit">登录</button>
|
<button id="loginBtn" type="submit">登录</button>
|
||||||
<div id="error" class="error"></div>
|
<div id="error" class="error"></div>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="hint" style="text-align:center; margin-top:12px;">
|
||||||
|
还没有账号?
|
||||||
|
<a href="/accounts/register/" style="color:#2d8cf0; text-decoration:none;">去注册</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="{% static 'accounts/login.js' %}"></script>
|
<script src="{% static 'accounts/login.js' %}"></script>
|
||||||
|
|||||||
62
accounts/templates/accounts/register.html
Normal file
62
accounts/templates/accounts/register.html
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>用户注册</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; background: #f5f6fa; }
|
||||||
|
.container { max-width: 400px; margin: 10vh auto; padding: 24px; background: #fff; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
|
||||||
|
h1 { font-size: 20px; margin: 0 0 16px; }
|
||||||
|
label { display:block; margin: 12px 0 6px; color:#333; }
|
||||||
|
input { width:100%; padding:10px 12px; border:1px solid #dcdde1; border-radius:6px; }
|
||||||
|
button { width:100%; margin-top:16px; padding:10px 12px; background:#2d8cf0; color:#fff; border:none; border-radius:6px; cursor:pointer; }
|
||||||
|
button:disabled { background:#9bbcf0; cursor:not-allowed; }
|
||||||
|
.error { color:#d93025; margin-top:10px; min-height:20px; }
|
||||||
|
.hint { color:#888; font-size:12px; margin-top:10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>注册新用户</h1>
|
||||||
|
<form id="regForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label for="code">注册码</label>
|
||||||
|
<input id="code" name="code" type="text" required />
|
||||||
|
<label for="email">邮箱</label>
|
||||||
|
<input id="email" name="email" type="email" required />
|
||||||
|
<label for="username">用户名</label>
|
||||||
|
<input id="username" name="username" type="text" required />
|
||||||
|
<label for="password">密码</label>
|
||||||
|
<input id="password" name="password" type="password" required />
|
||||||
|
<label for="confirm">确认密码</label>
|
||||||
|
<input id="confirm" name="confirm" type="password" required />
|
||||||
|
<button id="regBtn" type="submit">注册</button>
|
||||||
|
<div id="error" class="error"></div>
|
||||||
|
</form>
|
||||||
|
<div class="hint">仅允许持有管理员提供注册码的学生注册</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function getCookie(name){const v=`; ${document.cookie}`;const p=v.split(`; ${name}=`);if(p.length===2) return p.pop().split(';').shift();}
|
||||||
|
document.getElementById('regForm').addEventListener('submit',async(e)=>{
|
||||||
|
e.preventDefault();
|
||||||
|
const err=document.getElementById('error'); err.textContent='';
|
||||||
|
const code=(document.getElementById('code').value||'').trim();
|
||||||
|
const email=(document.getElementById('email').value||'').trim();
|
||||||
|
const username=(document.getElementById('username').value||'').trim();
|
||||||
|
const password=document.getElementById('password').value||'';
|
||||||
|
const confirm=document.getElementById('confirm').value||'';
|
||||||
|
if(!code||!email||!username||!password){err.textContent='请填写所有字段';return;}
|
||||||
|
if(password!==confirm){err.textContent='两次密码不一致';return;}
|
||||||
|
const btn=document.getElementById('regBtn'); btn.disabled=true;
|
||||||
|
try{
|
||||||
|
const csrftoken=getCookie('csrftoken');
|
||||||
|
const resp=await fetch('/accounts/register/submit/',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-CSRFToken':csrftoken||''},body:JSON.stringify({code,email,username,password})});
|
||||||
|
const data=await resp.json();
|
||||||
|
if(!resp.ok||!data.ok){throw new Error(data.message||'注册失败');}
|
||||||
|
window.location.href=data.redirect_url;
|
||||||
|
}catch(e){err.textContent=e.message||'发生错误';}
|
||||||
|
finally{btn.disabled=false;}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -9,4 +9,6 @@ urlpatterns = [
|
|||||||
path("session-key/", views.set_session_key, name="set_session_key"),
|
path("session-key/", views.set_session_key, name="set_session_key"),
|
||||||
path("login/secure-submit/", views.secure_login_submit, name="secure_login_submit"),
|
path("login/secure-submit/", views.secure_login_submit, name="secure_login_submit"),
|
||||||
path("logout/", views.logout, name="logout"),
|
path("logout/", views.logout, name="logout"),
|
||||||
|
path("register/", views.register_page, name="register"),
|
||||||
|
path("register/submit/", views.register_submit, name="register_submit"),
|
||||||
]
|
]
|
||||||
@@ -13,6 +13,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
|
from .crypto import get_public_key_spki_b64, rsa_oaep_decrypt_b64, aes_gcm_decrypt_b64, verify_password
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
@@ -157,4 +158,54 @@ def logout(request):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
@ensure_csrf_cookie
|
||||||
|
def register_page(request):
|
||||||
|
return render(request, "accounts/register.html")
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@csrf_protect
|
||||||
|
def register_submit(request):
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return HttpResponseBadRequest("Invalid JSON")
|
||||||
|
code = (payload.get("code") or "").strip()
|
||||||
|
email = (payload.get("email") or "").strip()
|
||||||
|
username = (payload.get("username") or "").strip()
|
||||||
|
password = (payload.get("password") or "")
|
||||||
|
if not code or not email or not username or not password:
|
||||||
|
return HttpResponseBadRequest("Missing fields")
|
||||||
|
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
|
||||||
|
existing = es_get_user_by_username(username)
|
||||||
|
if existing:
|
||||||
|
return JsonResponse({"ok": False, "message": "用户名已存在"}, status=409)
|
||||||
|
users = es_get_all_users()
|
||||||
|
next_id = (max([int(u.get("user_id", 0)) for u in users]) + 1) if users else 1
|
||||||
|
ok = write_user_data({
|
||||||
|
"user_id": next_id,
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"permission": 1,
|
||||||
|
"email": email,
|
||||||
|
"key": rc.get("keys") or [],
|
||||||
|
"manage_key": rc.get("manage_keys") or [],
|
||||||
|
})
|
||||||
|
if not ok:
|
||||||
|
return JsonResponse({"ok": False, "message": "注册失败"}, status=500)
|
||||||
|
return JsonResponse({"ok": True, "redirect_url": "/accounts/login/"})
|
||||||
@@ -34,13 +34,14 @@ class UserDocument(Document):
|
|||||||
"""用户数据文档映射"""
|
"""用户数据文档映射"""
|
||||||
user_id = fields.LongField()
|
user_id = fields.LongField()
|
||||||
username = fields.KeywordField()
|
username = fields.KeywordField()
|
||||||
|
email = fields.KeywordField()
|
||||||
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字段匹配就显示否则不显示)
|
||||||
key = fields.IntegerField() #表示该用户的关键字,举个例子:学生A的key为"2024届人工智能1班","2024届","计算机与人工智能学院" 班导师B的key为"计算机与人工智能学院"
|
key = fields.KeywordField(multi=True) #表示该用户的关键字,举个例子:学生A的key为"2024届人工智能1班","2024届","计算机与人工智能学院" 班导师B的key为"计算机与人工智能学院"
|
||||||
manage_key = fields.IntegerField() #表示该用户管理的关键字(非管理员)班导师B的manage_key为"2024届人工智能1班"
|
manage_key = fields.KeywordField(multi=True) #表示该用户管理的关键字(非管理员)班导师B的manage_key为"2024届人工智能1班"
|
||||||
#那么学生A就可以在数据管理页面搜索到自己的获奖数据,而班导师B就可以在数据管理页面搜索到所有人工智能1班的获奖数据。也就是说学生A和班导师B都其实只有用户权限
|
#那么学生A就可以在数据管理页面搜索到自己的获奖数据,而班导师B就可以在数据管理页面搜索到所有人工智能1班的获奖数据。也就是说学生A和班导师B都其实只有用户权限
|
||||||
|
|
||||||
class Django:
|
class Django:
|
||||||
model = User
|
model = User
|
||||||
# fields列表应该只包含需要特殊处理的字段,或者可以完全省略
|
# fields列表应该只包含需要特殊处理的字段,或者可以完全省略
|
||||||
@@ -49,6 +50,17 @@ class UserDocument(Document):
|
|||||||
@GLOBAL_INDEX.doc_type
|
@GLOBAL_INDEX.doc_type
|
||||||
class GlobalDocument(Document):
|
class GlobalDocument(Document):
|
||||||
type_list = fields.KeywordField()
|
type_list = fields.KeywordField()
|
||||||
|
keys_list = fields.KeywordField(multi=True)
|
||||||
|
|
||||||
class Django:
|
class Django:
|
||||||
model = ElasticNews
|
model = ElasticNews
|
||||||
|
|
||||||
|
@GLOBAL_INDEX.doc_type
|
||||||
|
class RegistrationCodeDocument(Document):
|
||||||
|
code = fields.KeywordField() #具体值
|
||||||
|
keys = fields.KeywordField(multi=True) #对应的key
|
||||||
|
manage_keys = fields.KeywordField(multi=True) #对应的manage_key
|
||||||
|
expires_at = fields.DateField() #过期时间
|
||||||
|
created_by = fields.LongField() #创建者id
|
||||||
|
class Django:
|
||||||
|
model = ElasticNews
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ Django版本的ES连接和操作模块
|
|||||||
from elasticsearch import Elasticsearch
|
from elasticsearch import Elasticsearch
|
||||||
from elasticsearch_dsl import connections
|
from elasticsearch_dsl import connections
|
||||||
import os
|
import os
|
||||||
from .documents import AchievementDocument, UserDocument, GlobalDocument
|
from .documents import AchievementDocument, UserDocument, GlobalDocument, RegistrationCodeDocument
|
||||||
from accounts.crypto import hash_password_random_salt
|
from accounts.crypto import hash_password_random_salt
|
||||||
from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME, GLOBAL_INDEX_NAME
|
from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME, GLOBAL_INDEX_NAME
|
||||||
import hashlib
|
import hashlib
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone, timedelta
|
||||||
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# 使用环境变量配置ES连接,默认为本机
|
# 使用环境变量配置ES连接,默认为本机
|
||||||
@@ -115,6 +116,87 @@ def ensure_type_in_list(type_name: str):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_keys_list():
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
doc = GlobalDocument.get(id='keys')
|
||||||
|
cur = list(doc.keys_list or [])
|
||||||
|
except Exception:
|
||||||
|
cur = []
|
||||||
|
doc = GlobalDocument(keys_list=cur)
|
||||||
|
doc.meta.id = 'keys'
|
||||||
|
doc.save()
|
||||||
|
return [str(t).strip().strip(';') for t in cur]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def ensure_key_in_list(key_name: str):
|
||||||
|
if not key_name:
|
||||||
|
return False
|
||||||
|
norm = str(key_name).strip().strip(';')
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
doc = GlobalDocument.get(id='keys')
|
||||||
|
cur = list(doc.keys_list or [])
|
||||||
|
except Exception:
|
||||||
|
cur = []
|
||||||
|
doc = GlobalDocument(keys_list=cur)
|
||||||
|
doc.meta.id = 'keys'
|
||||||
|
cur_sanitized = {str(t).strip().strip(';') for t in cur}
|
||||||
|
if norm not in cur_sanitized:
|
||||||
|
cur.append(norm)
|
||||||
|
doc.keys_list = cur
|
||||||
|
doc.save()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def generate_registration_code(keys=None, manage_keys=None, expires_in_days: int = 30, created_by: int = None):
|
||||||
|
try:
|
||||||
|
keys = list(keys or [])
|
||||||
|
manage_keys = list(manage_keys or [])
|
||||||
|
for k in list(keys):
|
||||||
|
ensure_key_in_list(k)
|
||||||
|
for mk in list(manage_keys):
|
||||||
|
ensure_key_in_list(mk)
|
||||||
|
code = uuid.uuid4().hex + str(int(time.time()))[-6:]
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
expires = now + timedelta(days=max(1, int(expires_in_days or 30)))
|
||||||
|
doc = RegistrationCodeDocument(
|
||||||
|
code=code,
|
||||||
|
keys=keys,
|
||||||
|
manage_keys=manage_keys,
|
||||||
|
created_at=now.isoformat(),
|
||||||
|
expires_at=expires.isoformat(),
|
||||||
|
created_by=created_by,
|
||||||
|
)
|
||||||
|
doc.meta.id = code
|
||||||
|
doc.save()
|
||||||
|
return {
|
||||||
|
"code": code,
|
||||||
|
"keys": keys,
|
||||||
|
"manage_keys": manage_keys,
|
||||||
|
"created_at": now.isoformat(),
|
||||||
|
"expires_at": expires.isoformat(),
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_registration_code(code: str):
|
||||||
|
try:
|
||||||
|
doc = RegistrationCodeDocument.get(id=str(code))
|
||||||
|
return {
|
||||||
|
"code": getattr(doc, 'code', str(code)),
|
||||||
|
"keys": list(getattr(doc, 'keys', []) or []),
|
||||||
|
"manage_keys": list(getattr(doc, 'manage_keys', []) or []),
|
||||||
|
"created_at": getattr(doc, 'created_at', None),
|
||||||
|
"expires_at": getattr(doc, 'expires_at', None),
|
||||||
|
"created_by": getattr(doc, 'created_by', None),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def get_doc_id(data):
|
def get_doc_id(data):
|
||||||
"""
|
"""
|
||||||
根据数据内容生成唯一ID(用于去重)
|
根据数据内容生成唯一ID(用于去重)
|
||||||
@@ -527,7 +609,10 @@ def write_user_data(user_data):
|
|||||||
username=user_data.get('username'),
|
username=user_data.get('username'),
|
||||||
password_hash=pwd_hash_b64,
|
password_hash=pwd_hash_b64,
|
||||||
password_salt=pwd_salt_b64,
|
password_salt=pwd_salt_b64,
|
||||||
permission=perm_val
|
permission=perm_val,
|
||||||
|
email=user_data.get('email'),
|
||||||
|
key=list(user_data.get('key') or []),
|
||||||
|
manage_key=list(user_data.get('manage_key') or []),
|
||||||
)
|
)
|
||||||
user.save()
|
user.save()
|
||||||
print(f"用户数据写入成功: {user_data.get('username')}")
|
print(f"用户数据写入成功: {user_data.get('username')}")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
INDEX_NAME = "wordsearch2666661"
|
INDEX_NAME = "wordsearch2666661"
|
||||||
USER_NAME = "users1111166"
|
USER_NAME = "users11111666789"
|
||||||
ACHIEVEMENT_INDEX_NAME = INDEX_NAME
|
ACHIEVEMENT_INDEX_NAME = INDEX_NAME
|
||||||
USER_INDEX_NAME = USER_NAME
|
USER_INDEX_NAME = USER_NAME
|
||||||
GLOBAL_INDEX_NAME = "global1111111121"
|
GLOBAL_INDEX_NAME = "global11111111"
|
||||||
|
|||||||
119
elastic/templates/elastic/registration_codes.html
Normal file
119
elastic/templates/elastic/registration_codes.html
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>注册码管理</title>
|
||||||
|
<style>
|
||||||
|
body { margin:0; font-family: system-ui,-apple-system, Segoe UI, Roboto, sans-serif; background:#fafafa; }
|
||||||
|
.sidebar { position:fixed; top:0; left:0; width:180px; height:100vh; background:#1e1e2e; color:#fff; padding:20px; box-shadow:2px 0 5px rgba(0,0,0,0.1); z-index:1000; display:flex; flex-direction:column; align-items:center; }
|
||||||
|
.sidebar h3 { margin:0; font-size:18px; color:#add8e6; text-align:center; }
|
||||||
|
.navigation-links { width:100%; margin-top:60px; }
|
||||||
|
.sidebar a, .sidebar button { display:block; color:#8be9fd; text-decoration:none; margin:10px 0; font-size:16px; padding:15px; border-radius:4px; background:transparent; border:none; cursor:pointer; width:calc(100% - 40px); text-align:left; transition:all .2s ease; }
|
||||||
|
.sidebar a:hover, .sidebar button:hover { color:#ff79c6; background-color:rgba(139,233,253,.2); }
|
||||||
|
.main { margin-left:200px; padding:20px; color:#333; }
|
||||||
|
.card { background:#fff; border-radius:14px; box-shadow:0 10px 24px rgba(31,35,40,.08); padding:20px; margin-bottom:20px; }
|
||||||
|
.row { display:flex; gap:16px; }
|
||||||
|
.col { flex:1; }
|
||||||
|
label { display:block; margin-bottom:6px; font-weight:600; }
|
||||||
|
input[type=text], input[type=number], select { width:100%; padding:8px 12px; border:1px solid #d1d5db; border-radius:6px; box-sizing:border-box; }
|
||||||
|
.btn { padding:8px 12px; border:none; border-radius:8px; cursor:pointer; margin:0 4px; }
|
||||||
|
.btn-primary { background:#4f46e5; color:#fff; }
|
||||||
|
.btn-secondary { background:#64748b; color:#fff; }
|
||||||
|
.notice { padding:10px; border-radius:6px; margin-top:10px; display:none; }
|
||||||
|
.notice.success { background:#d4edda; color:#155724; border:1px solid #c3e6cb; }
|
||||||
|
.notice.error { background:#f8d7da; color:#721c24; border:1px solid #f5c6cb; }
|
||||||
|
.code-box { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; padding:12px; border:1px solid #e5e7eb; border-radius:8px; background:#fafafa; margin-top:10px; }
|
||||||
|
</style>
|
||||||
|
{% csrf_token %}
|
||||||
|
<script>
|
||||||
|
function getCookie(name){const v=`; ${document.cookie}`;const p=v.split(`; ${name}=`);if(p.length===2) return p.pop().split(';').shift();}
|
||||||
|
async function loadKeys(){
|
||||||
|
const resp=await fetch('/elastic/registration-codes/keys/');
|
||||||
|
const data=await resp.json();
|
||||||
|
const opts=(data.data||[]);
|
||||||
|
const keySel=document.getElementById('keys');
|
||||||
|
const mkeySel=document.getElementById('manageKeys');
|
||||||
|
keySel.innerHTML=''; mkeySel.innerHTML='';
|
||||||
|
opts.forEach(k=>{
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function addKey(){
|
||||||
|
const keyName=(document.getElementById('newKey').value||'').trim();
|
||||||
|
if(!keyName) return;
|
||||||
|
const csrftoken=getCookie('csrftoken');
|
||||||
|
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 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();}
|
||||||
|
else{msg.textContent=data.message||'新增失败'; msg.className='notice error'; msg.style.display='block';}
|
||||||
|
}
|
||||||
|
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 clearSelection(id){ const sel=document.getElementById(id); Array.from(sel.options).forEach(o=>o.selected=false); }
|
||||||
|
async function generateCode(){
|
||||||
|
const csrftoken=getCookie('csrftoken');
|
||||||
|
const keys=selectedValues(document.getElementById('keys'));
|
||||||
|
const manageKeys=selectedValues(document.getElementById('manageKeys'));
|
||||||
|
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);}
|
||||||
|
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 data=await resp.json();
|
||||||
|
const out=document.getElementById('codeOut');
|
||||||
|
const msg=document.getElementById('msg');
|
||||||
|
if(resp.ok && data.status==='success'){out.textContent=data.data.code; msg.textContent='生成成功'; msg.className='notice success'; msg.style.display='block';}
|
||||||
|
else{msg.textContent=data.message||'生成失败'; msg.className='notice error'; msg.style.display='block';}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded',()=>{loadKeys(); enableToggleSelect(document.getElementById('keys')); enableToggleSelect(document.getElementById('manageKeys'));});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="sidebar">
|
||||||
|
<h3>用户ID:{{ user_id }}</h3>
|
||||||
|
<div class="navigation-links">
|
||||||
|
<a href="{% url 'main:home' %}">主页</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="card">
|
||||||
|
<h2>管理注册码</h2>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<label>新增key</label>
|
||||||
|
<input id="newKey" type="text" placeholder="输入新的key" />
|
||||||
|
<button class="btn btn-secondary" onclick="addKey()">新增</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="margin-top:12px;">
|
||||||
|
<div class="col">
|
||||||
|
<label>选择keys</label>
|
||||||
|
<select id="keys" multiple size="10"></select>
|
||||||
|
<div style="margin-top:8px;"><button class="btn btn-secondary" onclick="clearSelection('keys')">清空选择</button></div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<label>选择manage_keys</label>
|
||||||
|
<select id="manageKeys" multiple size="10"></select>
|
||||||
|
<div style="margin-top:8px;"><button class="btn btn-secondary" onclick="clearSelection('manageKeys')">清空选择</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row" style="margin-top:12px;">
|
||||||
|
<div class="col">
|
||||||
|
<label>有效期</label>
|
||||||
|
<select id="expireMode">
|
||||||
|
<option value="month">一个月</option>
|
||||||
|
<option value="fouryears">四年</option>
|
||||||
|
<option value="custom">自定义天数</option>
|
||||||
|
</select>
|
||||||
|
<input id="customDays" type="number" min="1" placeholder="自定义天数" />
|
||||||
|
</div>
|
||||||
|
<div class="col" style="display:flex; align-items:flex-end;">
|
||||||
|
<button class="btn btn-primary" onclick="generateCode()">生成注册码</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="msg" class="notice"></div>
|
||||||
|
<div class="code-box" id="codeOut"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -32,6 +32,10 @@ urlpatterns = [
|
|||||||
# 管理页面
|
# 管理页面
|
||||||
path('manage/', views.manage_page, name='manage_page'),
|
path('manage/', views.manage_page, name='manage_page'),
|
||||||
path('user_manage/', views.user_manage, name='user_manage'),
|
path('user_manage/', views.user_manage, name='user_manage'),
|
||||||
|
path('registration-codes/manage/', views.registration_code_manage_page, name='registration_code_manage_page'),
|
||||||
|
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/generate/', views.generate_registration_code_view, name='generate_registration_code'),
|
||||||
|
|
||||||
# 分析接口
|
# 分析接口
|
||||||
path('analytics/trend/', views.analytics_trend_view, name='analytics_trend'),
|
path('analytics/trend/', views.analytics_trend_view, name='analytics_trend'),
|
||||||
|
|||||||
@@ -604,3 +604,67 @@ def user_manage(request):
|
|||||||
user_id_qs = request.GET.get("user_id")
|
user_id_qs = request.GET.get("user_id")
|
||||||
context = {"user_id": user_id_qs or session_user_id}
|
context = {"user_id": user_id_qs or session_user_id}
|
||||||
return render(request, "elastic/users.html", context)
|
return render(request, "elastic/users.html", context)
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
@ensure_csrf_cookie
|
||||||
|
def registration_code_manage_page(request):
|
||||||
|
session_user_id = request.session.get("user_id")
|
||||||
|
if session_user_id is None:
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
return redirect("/accounts/login/")
|
||||||
|
if int(request.session.get("permission", 1)) != 0:
|
||||||
|
from django.shortcuts import redirect
|
||||||
|
return redirect("/main/home/")
|
||||||
|
user_id_qs = request.GET.get("user_id")
|
||||||
|
context = {"user_id": user_id_qs or session_user_id}
|
||||||
|
return render(request, "elastic/registration_codes.html", context)
|
||||||
|
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def get_keys_list_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)
|
||||||
|
lst = get_keys_list()
|
||||||
|
return JsonResponse({"status": "success", "data": lst})
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@csrf_protect
|
||||||
|
def add_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)
|
||||||
|
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)
|
||||||
|
ok = ensure_key_in_list(key_name)
|
||||||
|
if not ok:
|
||||||
|
return JsonResponse({"status": "error", "message": "key已存在或写入失败"}, status=409)
|
||||||
|
return JsonResponse({"status": "success"})
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@csrf_protect
|
||||||
|
def generate_registration_code_view(request):
|
||||||
|
if request.session.get("user_id") is None:
|
||||||
|
return JsonResponse({"status": "error", "message": "未登录"}, status=401)
|
||||||
|
if int(request.session.get("permission", 1)) != 0:
|
||||||
|
return JsonResponse({"status": "error", "message": "无权限"}, status=403)
|
||||||
|
try:
|
||||||
|
payload = json.loads(request.body.decode("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return JsonResponse({"status": "error", "message": "JSON无效"}, status=400)
|
||||||
|
keys = list(payload.get("keys") or [])
|
||||||
|
manage_keys = list(payload.get("manage_keys") or [])
|
||||||
|
try:
|
||||||
|
days = int(payload.get("expires_in_days", 30))
|
||||||
|
except Exception:
|
||||||
|
days = 30
|
||||||
|
result = generate_registration_code(keys=keys, manage_keys=manage_keys, expires_in_days=days, created_by=request.session.get("user_id"))
|
||||||
|
if not result:
|
||||||
|
return JsonResponse({"status": "error", "message": "生成失败"}, status=500)
|
||||||
|
return JsonResponse({"status": "success", "data": result})
|
||||||
|
|||||||
@@ -145,6 +145,7 @@
|
|||||||
<a href="{% url 'elastic:manage_page' %}" onclick="return handleNavClick(this, '/elastic/manage/');">数据管理</a>
|
<a href="{% url 'elastic:manage_page' %}" onclick="return handleNavClick(this, '/elastic/manage/');">数据管理</a>
|
||||||
{% if is_admin %}
|
{% if is_admin %}
|
||||||
<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>
|
||||||
|
<a href="{% url 'elastic:registration_code_manage_page' %}" onclick="return handleNavClick(this, '/elastic/registration-codes/manage/');">注册码管理</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button id="logoutBtn">退出登录</button>
|
<button id="logoutBtn">退出登录</button>
|
||||||
<div id="logoutMsg"></div>
|
<div id="logoutMsg"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user