Files
fangke/server/tracker/api/auth.py
T
2026-09-15 14:18:37 +08:00

84 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from django.contrib.auth import get_user_model
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAdminUser
from rest_framework.throttling import ScopedRateThrottle
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
# 对齐 Django UnicodeUsernameValidator(\w 默认 Unicode)——create_user 不跑校验器,入口自防
_USERNAME_RE = re.compile(r"[\w.@+-]+")
def _ok(data, message="ok"):
return Response({"code": 200, "data": data, "message": message})
class _EnvelopeMixin:
"""错误信封统一收口(Task7 同款裁剪,无 ACAO——管理台与 API 同源,跨域由 nginx 层面不存在)。
simplejwt 认证失败 401 / 权限 403 / 限流 429 等由 DRF 异常处理器产出的响应
body 是 {detail},管理台前端(Task17)按 {code,data,message} 信封对接——
在此统一转换,视图自身的 _ok/4xx 响应不受影响(只动含 detail 的 dict)。
"""
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(request, response, *args, **kwargs)
if response.status_code >= 400 and isinstance(response.data, dict):
if "detail" in response.data:
response.data = {"code": response.status_code, "data": None,
"message": response.data["detail"]}
elif "code" not in response.data: # simplejwt 序列化器字段错误 {"password": [...]} 等
response.data = {"code": response.status_code, "data": response.data,
"message": "invalid_input"}
return response
class LoginView(_EnvelopeMixin, TokenObtainPairView):
"""登录。JWT 取舍(M1 决策):access 12h / refresh 24h、无黑名单轮换——
token 泄露唯一应急是换 SECRET_KEY(全体登出),README-deploy 写明。"""
throttle_classes = [ScopedRateThrottle]
throttle_scope = "login" # 10/min/IP 防爆破(settings 已配)
def post(self, request, *args, **kwargs):
resp = super().post(request, *args, **kwargs) # 失败由 simplejwt 返回 401(信封由 mixin 补)
if resp.status_code == 200:
return _ok({"access": resp.data["access"], "refresh": resp.data["refresh"]})
return resp
class RefreshView(_EnvelopeMixin, TokenRefreshView):
permission_classes = [AllowAny] # TokenRefreshView 自带 AllowAny,显式声明表意
def post(self, request, *args, **kwargs):
resp = super().post(request, *args, **kwargs)
if resp.status_code == 200: # 成功同样走信封——与 LoginView 对齐,前端只需一种解析路径
return _ok({"access": resp.data["access"]})
return resp
class MeView(_EnvelopeMixin, APIView):
def get(self, request):
return _ok({"username": request.user.username,
"is_admin": request.user.is_staff})
class UserCreateView(_EnvelopeMixin, APIView):
permission_classes = [IsAdminUser] # is_staff
def post(self, request):
data = request.data if isinstance(request.data, dict) else {} # JSON 数组等畸形 body → 空表(.get 会 500)
username, password = data.get("username"), data.get("password")
if not isinstance(username, str) or not isinstance(password, str): # 123.strip() / len(None) 会 500
return Response({"code": 400, "data": None, "message": "invalid_input"}, status=400)
username = username.strip()
# 字符集对齐 UnicodeUsernameValidator(create_user 不跑校验器);长度上限防 MySQL DataError(sqlite 不报错)
if (not username or len(username) > 150 or not _USERNAME_RE.fullmatch(username)
or len(password) < 8):
return Response({"code": 400, "data": None, "message": "invalid_input"}, status=400)
User = get_user_model()
if User.objects.filter(username=username).exists():
return Response({"code": 409, "data": None, "message": "username_exists"}, status=409)
User.objects.create_user(username=username, password=password, is_staff=False)
return _ok({"username": username}, "created")