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")