注册码生成以及用户注册

This commit is contained in:
2025-11-17 18:03:13 +08:00
parent 6e332f248f
commit 32ff920921
11 changed files with 413 additions and 9 deletions

View File

@@ -42,6 +42,10 @@
<button id="loginBtn" type="submit">登录</button>
<div id="error" class="error"></div>
</form>
<div class="hint" style="text-align:center; margin-top:12px;">
还没有账号?
<a href="/accounts/register/" style="color:#2d8cf0; text-decoration:none;">去注册</a>
</div>
</div>
<script src="{% static 'accounts/login.js' %}"></script>

View 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>

View File

@@ -9,4 +9,6 @@ urlpatterns = [
path("session-key/", views.set_session_key, name="set_session_key"),
path("login/secure-submit/", views.secure_login_submit, name="secure_login_submit"),
path("logout/", views.logout, name="logout"),
path("register/", views.register_page, name="register"),
path("register/submit/", views.register_submit, name="register_submit"),
]

View File

@@ -13,6 +13,7 @@ from django.conf import settings
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 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"])
@@ -158,3 +159,53 @@ def logout(request):
pass
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/"})

View File

@@ -34,11 +34,12 @@ class UserDocument(Document):
"""用户数据文档映射"""
user_id = fields.LongField()
username = fields.KeywordField()
email = fields.KeywordField()
password_hash = fields.KeywordField()
password_salt = fields.KeywordField()
permission = fields.IntegerField() # 还是2种权限0为管理员1为用户区别在于0有全部权限1在数据管理页面有搜索框但是索引到的录入信息要根据其用户id查询其key若其中之一与用户的manage_key字段匹配就显示否则不显示
key = fields.IntegerField() #表示该用户的关键字举个例子学生A的key为"2024届人工智能1班","2024届""计算机与人工智能学院" 班导师B的key为"计算机与人工智能学院"
manage_key = fields.IntegerField() #表示该用户管理的关键字非管理员班导师B的manage_key为"2024届人工智能1班"
key = fields.KeywordField(multi=True) #表示该用户的关键字举个例子学生A的key为"2024届人工智能1班","2024届""计算机与人工智能学院" 班导师B的key为"计算机与人工智能学院"
manage_key = fields.KeywordField(multi=True) #表示该用户管理的关键字非管理员班导师B的manage_key为"2024届人工智能1班"
#那么学生A就可以在数据管理页面搜索到自己的获奖数据而班导师B就可以在数据管理页面搜索到所有人工智能1班的获奖数据。也就是说学生A和班导师B都其实只有用户权限
class Django:
@@ -49,6 +50,17 @@ class UserDocument(Document):
@GLOBAL_INDEX.doc_type
class GlobalDocument(Document):
type_list = fields.KeywordField()
keys_list = fields.KeywordField(multi=True)
class Django:
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

View File

@@ -5,12 +5,13 @@ Django版本的ES连接和操作模块
from elasticsearch import Elasticsearch
from elasticsearch_dsl import connections
import os
from .documents import AchievementDocument, UserDocument, GlobalDocument
from .documents import AchievementDocument, UserDocument, GlobalDocument, RegistrationCodeDocument
from accounts.crypto import hash_password_random_salt
from .indexes import ACHIEVEMENT_INDEX_NAME, USER_INDEX_NAME, GLOBAL_INDEX_NAME
import hashlib
import time
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
import uuid
import json
# 使用环境变量配置ES连接默认为本机
@@ -115,6 +116,87 @@ def ensure_type_in_list(type_name: str):
except Exception:
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):
"""
根据数据内容生成唯一ID用于去重
@@ -527,7 +609,10 @@ def write_user_data(user_data):
username=user_data.get('username'),
password_hash=pwd_hash_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()
print(f"用户数据写入成功: {user_data.get('username')}")

View File

@@ -1,5 +1,5 @@
INDEX_NAME = "wordsearch2666661"
USER_NAME = "users1111166"
USER_NAME = "users11111666789"
ACHIEVEMENT_INDEX_NAME = INDEX_NAME
USER_INDEX_NAME = USER_NAME
GLOBAL_INDEX_NAME = "global1111111121"
GLOBAL_INDEX_NAME = "global11111111"

View 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>

View File

@@ -32,6 +32,10 @@ urlpatterns = [
# 管理页面
path('manage/', views.manage_page, name='manage_page'),
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'),

View File

@@ -604,3 +604,67 @@ def user_manage(request):
user_id_qs = request.GET.get("user_id")
context = {"user_id": user_id_qs or session_user_id}
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})

View File

@@ -145,6 +145,7 @@
<a href="{% url 'elastic:manage_page' %}" onclick="return handleNavClick(this, '/elastic/manage/');">数据管理</a>
{% if is_admin %}
<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 %}
<button id="logoutBtn">退出登录</button>
<div id="logoutMsg"></div>