Archived
refactor: register 端点拆解(84 行 / 圈 34 → 58 行 / 圈 20 / 嵌套 1)
为什么:注册是站点唯一的建号入口,84 行里五条「建号前失败」路径各自 手写一遍 restore_verify_code(有的还带失败日志),防刷双闸与三种建号 异常的口径全埋在分支里,改任何一条都要通读全函数核对还原时序。 拆出(顺序 = 原执行顺序,逐条语义对应): - _validate_registration_input:入参四道闸(协议/用户名/密码/邮箱), 按序返回第一条错误文案 - _reject_registration:统一拒绝出口——还原已消费验证码(可选失败 日志);五处 restore 收敛为一处,docstring 写明「为何必须还原」 - _register_credits:赠送闸(同设备重复注册归零;顺带删除 credits<0 死分支——_safe_register_credits 已保证非负) - _issue_registration_session:成功收尾(login_seq 递增 + 日志 + Cookie) 先写测试后动代码:新增 tests/test_register_flow.py 9 例,在未改动 实现上一次跑绿。验证码由 check_verify_code DELETE 原子消费,测试用 「失败路径后 email_codes 行仍在」做 restore 实证断言(不 mock 还原); 其中用户名重名路径用端到端方式验证——同一验证码不重发即可二次注册。 覆盖此前零测试的路径:IP 闸、设备赠送闸、IntegrityError/ValueError/ 读回 None 三种建号异常映射、非预期异常 500 泛化文案。 门禁:ruff 全过 · pyright 0/0 · pytest 339 passed(330→339)。
This commit is contained in:
1 parent
e00ac14460
commit
5ad2152acc
2 files changed
+269
-50
No files matched your search
+81
-50
@@ -138,11 +138,71 @@ async def forgot_password(request: Request):
|
||||
return error('重置失败,请稍后重试', 500)
|
||||
|
||||
|
||||
@router.post('/auth/register')
|
||||
async def register(request: Request):
|
||||
"""注册:验证码校验 → 用户名/邮箱查重 → 防刷双闸 → 建号赠送 → 建立会话。"""
|
||||
def _validate_registration_input(username: str, password: str, email: str,
|
||||
agree: str) -> str | None:
|
||||
"""注册入参四道闸,按序返回第一条错误文案(None = 全部通过)。"""
|
||||
from web.auth_state import re_username_ok
|
||||
|
||||
if agree.strip() != '1':
|
||||
return '请先阅读并同意《用户协议》与《隐私政策》'
|
||||
if not (3 <= len(username) <= 20 and re_username_ok(username)):
|
||||
return '用户名需 3-20 位,仅支持中文、字母、数字、下划线,且不能纯下划线'
|
||||
if len(password) < 8 or len(password) > 128:
|
||||
return '密码长度需在 8-128 位之间'
|
||||
if len(email) > 254:
|
||||
return '邮箱格式不正确'
|
||||
return None
|
||||
|
||||
|
||||
async def _reject_registration(message: str, *, email: str, code: str, ttl: int,
|
||||
username: str = '', ip: str = '',
|
||||
log_detail: str | None = None) -> JSONResponse:
|
||||
"""注册流程后续步骤失败的统一拒绝:还原已消费的验证码(可选失败日志)。
|
||||
|
||||
验证码在 check_verify_code 里已被 DELETE 原子消费,凡走到建号/查重/防刷
|
||||
之后才失败的路径都必须还原,否则用户被迫重新收码。
|
||||
"""
|
||||
await asyncio.to_thread(auth.restore_verify_code, email, code, ttl, 'register')
|
||||
if log_detail:
|
||||
await asyncio.to_thread(auth.add_log, 'fail', username, ip, '注册', log_detail)
|
||||
return error(message)
|
||||
|
||||
|
||||
async def _register_credits(device_used: bool) -> int:
|
||||
"""注册赠送次数:同设备重复注册账号照建,但不再赠送。"""
|
||||
if device_used:
|
||||
return 0
|
||||
return await _safe_register_credits()
|
||||
|
||||
|
||||
async def _issue_registration_session(user, credits: int, username: str,
|
||||
ip: str) -> JSONResponse:
|
||||
"""注册成功收尾:login_seq 递增 + 成功日志 + 签发会话 Cookie。"""
|
||||
uid = int(user['id'])
|
||||
now = int(time.time())
|
||||
await db.execute('UPDATE users SET last_active=?, last_login=?, '
|
||||
'login_seq=COALESCE(login_seq,0)+1 WHERE id=?', (now, now, uid))
|
||||
fresh = await repo.get_user_by_id(uid) or {}
|
||||
seq = int(fresh.get('login_seq') or 1)
|
||||
credits = min(credits, max(0, int(fresh.get('duration_seconds') or 0)))
|
||||
await asyncio.to_thread(auth.add_log, 'success', username, ip, '注册账号',
|
||||
f'赠送 {credits} 次' if credits else '未赠送次数')
|
||||
secure = os.environ.get('ENABLE_SECURE_COOKIE', '').strip().lower() in ('1', 'true', 'yes')
|
||||
return _login_cookie_response({
|
||||
'user': _user_public(fresh),
|
||||
'duration_text': auth.format_credits(int(fresh.get('duration_seconds') or 0)),
|
||||
'redirect': 'admin' if fresh.get('role') == 'admin' else 'user',
|
||||
}, '注册成功' + (f',已赠送 {credits} 次下载次数' if credits else ''), uid, seq, secure)
|
||||
|
||||
|
||||
@router.post('/auth/register')
|
||||
async def register(request: Request):
|
||||
"""注册:入参校验 → 验证码 → 查重/防刷 → 建号 → 会话。
|
||||
|
||||
五条「建号前失败」路径共用 _reject_registration(还原验证码),
|
||||
顺序与历史实现逐条对应:用户名重名 / 邮箱重名 / IP 超限(+日志)/
|
||||
建号 IntegrityError / 建号 ValueError(+日志)/ 读回 None(+日志)。
|
||||
"""
|
||||
data = await _request_data(request)
|
||||
username = str(data.get('username') or '').strip()
|
||||
password = str(data.get('password') or '')
|
||||
@@ -151,38 +211,28 @@ async def register(request: Request):
|
||||
device_id = str(data.get('device_id') or '').strip()[:64]
|
||||
ip = ratelimit.client_ip(request)
|
||||
|
||||
if str(data.get('agree') or '').strip() != '1':
|
||||
return error('请先阅读并同意《用户协议》与《隐私政策》')
|
||||
if not (3 <= len(username) <= 20 and re_username_ok(username)):
|
||||
return error('用户名需 3-20 位,仅支持中文、字母、数字、下划线,且不能纯下划线')
|
||||
if len(password) < 8 or len(password) > 128:
|
||||
return error('密码长度需在 8-128 位之间')
|
||||
if len(email) > 254:
|
||||
return error('邮箱格式不正确')
|
||||
bad_input = _validate_registration_input(
|
||||
username, password, email, str(data.get('agree') or ''))
|
||||
if bad_input:
|
||||
return error(bad_input)
|
||||
|
||||
try:
|
||||
code_ttl = await asyncio.to_thread(auth.check_verify_code, email, code, 'register')
|
||||
if not code_ttl:
|
||||
return error('邮箱验证码错误或已过期')
|
||||
rej = {'email': email, 'code': code, 'ttl': code_ttl}
|
||||
if await asyncio.to_thread(auth.get_user_by_name, username):
|
||||
await asyncio.to_thread(auth.restore_verify_code, email, code, code_ttl, 'register')
|
||||
return error('用户名已被注册')
|
||||
return await _reject_registration('用户名已被注册', **rej)
|
||||
if email and await asyncio.to_thread(auth.get_user_by_email, email):
|
||||
await asyncio.to_thread(auth.restore_verify_code, email, code, code_ttl, 'register')
|
||||
return error('该邮箱已被注册,请更换邮箱或联系管理员')
|
||||
return await _reject_registration('该邮箱已被注册,请更换邮箱或联系管理员', **rej)
|
||||
|
||||
device_used_flag = bool(device_id) and await asyncio.to_thread(auth.device_used, device_id)
|
||||
if ip and await asyncio.to_thread(auth.register_ip_exceeded, ip):
|
||||
await asyncio.to_thread(auth.restore_verify_code, email, code, code_ttl, 'register')
|
||||
await asyncio.to_thread(auth.add_log, 'fail', username, ip, '注册', '同 IP 24h 注册次数超限')
|
||||
return error('当前网络注册过于频繁,请 24 小时后再试或联系管理员')
|
||||
|
||||
credits = await _safe_register_credits()
|
||||
if credits < 0:
|
||||
credits = 0
|
||||
if device_used_flag:
|
||||
credits = 0 # 同设备重复注册:账号照建,不再赠送
|
||||
return await _reject_registration(
|
||||
'当前网络注册过于频繁,请 24 小时后再试或联系管理员',
|
||||
username=username, ip=ip, log_detail='同 IP 24h 注册次数超限', **rej)
|
||||
|
||||
credits = await _register_credits(device_used_flag)
|
||||
try:
|
||||
user = await asyncio.to_thread(
|
||||
auth.create_user, username, password, email=email, credits=credits,
|
||||
@@ -191,35 +241,16 @@ async def register(request: Request):
|
||||
ua=str(request.headers.get('user-agent') or ''),
|
||||
ip_limit=3, device_once=True))
|
||||
except sqlite3.IntegrityError:
|
||||
await asyncio.to_thread(auth.restore_verify_code, email, code, code_ttl, 'register')
|
||||
return error('用户名或邮箱已被注册,请换一个再试')
|
||||
return await _reject_registration('用户名或邮箱已被注册,请换一个再试', **rej)
|
||||
except ValueError as e:
|
||||
await asyncio.to_thread(auth.restore_verify_code, email, code, code_ttl, 'register')
|
||||
await asyncio.to_thread(auth.add_log, 'fail', username, ip, '注册', str(e))
|
||||
return error(str(e))
|
||||
return await _reject_registration(str(e), username=username, ip=ip,
|
||||
log_detail=str(e), **rej)
|
||||
|
||||
# 建号事务已提交却读不回(并发删除/库损坏):按注册失败处理并把验证码还回去,
|
||||
# 不让 user['id'] 直接 TypeError 崩成 500
|
||||
# 建号事务已提交却读不回(并发删除/库损坏):按注册失败处理
|
||||
if user is None:
|
||||
await asyncio.to_thread(auth.restore_verify_code, email, code, code_ttl, 'register')
|
||||
await asyncio.to_thread(auth.add_log, 'fail', username, ip, '注册', '建号后读取用户失败')
|
||||
return error('注册失败,请稍后重试')
|
||||
return await _reject_registration('注册失败,请稍后重试', username=username,
|
||||
ip=ip, log_detail='建号后读取用户失败', **rej)
|
||||
|
||||
# 建立会话(login_seq 递增 + 签发 Cookie)
|
||||
uid = int(user['id'])
|
||||
now = int(time.time())
|
||||
await db.execute('UPDATE users SET last_active=?, last_login=?, '
|
||||
'login_seq=COALESCE(login_seq,0)+1 WHERE id=?', (now, now, uid))
|
||||
fresh = await repo.get_user_by_id(uid) or {}
|
||||
seq = int(fresh.get('login_seq') or 1)
|
||||
credits = min(credits, max(0, int(fresh.get('duration_seconds') or 0)))
|
||||
await asyncio.to_thread(auth.add_log, 'success', username, ip, '注册账号',
|
||||
f'赠送 {credits} 次' if credits else '未赠送次数')
|
||||
secure = os.environ.get('ENABLE_SECURE_COOKIE', '').strip().lower() in ('1', 'true', 'yes')
|
||||
return _login_cookie_response({
|
||||
'user': _user_public(fresh),
|
||||
'duration_text': auth.format_credits(int(fresh.get('duration_seconds') or 0)),
|
||||
'redirect': 'admin' if fresh.get('role') == 'admin' else 'user',
|
||||
}, '注册成功' + (f',已赠送 {credits} 次下载次数' if credits else ''), uid, seq, secure)
|
||||
return await _issue_registration_session(user, credits, username, ip)
|
||||
except Exception:
|
||||
return error('注册失败,请稍后重试', 500)
|
||||
Reference in new issue
Block a user