refactor: 提取共享信封模块 EnvelopeMixin/ok
auth.py 的 _EnvelopeMixin/_ok 提取到 tracker/api/envelope.py(去下划线前缀), auth.py 改为 import 使用;collect.py 的 finalize_response 含 ACAO 回显逻辑 保留在自身视图,仅注释指向未来收敛。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -5,36 +5,13 @@ 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
|
||||
from tracker.api.envelope import EnvelopeMixin, ok
|
||||
|
||||
# 对齐 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):
|
||||
class LoginView(EnvelopeMixin, TokenObtainPairView):
|
||||
"""登录。JWT 取舍(M1 决策):access 12h / refresh 24h、无黑名单轮换——
|
||||
token 泄露唯一应急是换 SECRET_KEY(全体登出),README-deploy 写明。"""
|
||||
throttle_classes = [ScopedRateThrottle]
|
||||
@@ -43,27 +20,27 @@ class LoginView(_EnvelopeMixin, TokenObtainPairView):
|
||||
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 ok({"access": resp.data["access"], "refresh": resp.data["refresh"]})
|
||||
return resp
|
||||
|
||||
|
||||
class RefreshView(_EnvelopeMixin, TokenRefreshView):
|
||||
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 ok({"access": resp.data["access"]})
|
||||
return resp
|
||||
|
||||
|
||||
class MeView(_EnvelopeMixin, APIView):
|
||||
class MeView(EnvelopeMixin, APIView):
|
||||
def get(self, request):
|
||||
return _ok({"username": request.user.username,
|
||||
return ok({"username": request.user.username,
|
||||
"is_admin": request.user.is_staff})
|
||||
|
||||
|
||||
class UserCreateView(_EnvelopeMixin, APIView):
|
||||
class UserCreateView(EnvelopeMixin, APIView):
|
||||
permission_classes = [IsAdminUser] # is_staff
|
||||
|
||||
def post(self, request):
|
||||
@@ -80,4 +57,4 @@ class UserCreateView(_EnvelopeMixin, APIView):
|
||||
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")
|
||||
return ok({"username": username}, "created")
|
||||
|
||||
@@ -79,6 +79,9 @@ class CollectBatchView(APIView):
|
||||
def finalize_response(self, request, response, *args, **kwargs):
|
||||
"""统一收口:ACAO 回显 + 错误信封。
|
||||
|
||||
错误信封部分与 tracker.api.envelope.EnvelopeMixin 同构——collect 端含 ACAO 回显
|
||||
(浏览器 SDK 跨域需读错误体),暂保留在自身视图,未来收敛至共享模块。
|
||||
|
||||
429/ParseError 等由 DRF 异常处理器产出的响应(无 ACAO、body 是 {detail})在这里补齐——
|
||||
否则浏览器 SDK 跨域读不到错误体会误判网络错误而重试,限流时反而打爆限流器。
|
||||
dispatch 对 initial() 抛出的 Throttled/ParseError 同样走 finalize_response,此钩子全覆盖。
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from rest_framework.response import Response
|
||||
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user