h5_auth.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """H5 登录相关 HTTP 接口。
  2. Pydantic 模型负责校验请求体,FastAPI 的 Depends 负责在接口执行前完成身份验证。
  3. """
  4. from typing import Annotated, Any
  5. from fastapi import APIRouter, Depends, Request, Response
  6. from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
  7. from pydantic import BaseModel, Field
  8. from app.api.responses import success_response
  9. from app.core.config import Settings
  10. from app.core.errors import AppError
  11. from app.domains.attribution.service import AttributionService
  12. from app.domains.identity.models import H5User
  13. from app.domains.identity.service import H5AuthService
  14. bearer_scheme = HTTPBearer(auto_error=False)
  15. H5_REFRESH_COOKIE = "zbt_h5_refresh"
  16. H5_REFRESH_COOKIE_PATH = "/api/v1/h5/auth"
  17. class MobileRequest(BaseModel):
  18. """请求验证码时的 JSON 请求体。"""
  19. mobile: str = Field(min_length=11, max_length=11)
  20. class H5LoginRequest(MobileRequest):
  21. code: str = Field(min_length=6, max_length=6)
  22. referral_code: str | None = Field(default=None, max_length=32)
  23. class RefreshRequest(BaseModel):
  24. refresh_token: str = Field(min_length=32)
  25. def create_h5_router(
  26. auth_service: H5AuthService,
  27. settings: Settings,
  28. attribution_service: AttributionService | None = None,
  29. ) -> APIRouter:
  30. """创建 H5 认证路由,并把认证服务放入各接口的闭包中。"""
  31. router = APIRouter(tags=["h5-auth"])
  32. def bearer_token(
  33. credentials: Annotated[
  34. HTTPAuthorizationCredentials | None,
  35. Depends(bearer_scheme),
  36. ],
  37. ) -> str:
  38. # Depends 会从 Authorization: Bearer <token> 请求头中解析凭证。
  39. if credentials is None:
  40. raise AppError("AUTH_REQUIRED", "请先登录", 401)
  41. return credentials.credentials
  42. def current_user(
  43. token: Annotated[str, Depends(bearer_token)],
  44. ) -> H5User:
  45. # 依赖可以继续依赖另一个依赖:先取 token,再查询当前用户。
  46. return auth_service.authenticate_access(token)
  47. @router.post("/h5/auth/request-code")
  48. def request_code(payload: MobileRequest, request: Request) -> dict[str, Any]:
  49. return success_response(request, auth_service.request_code(payload.mobile))
  50. def set_refresh_cookie(response: Response, refresh_token: str) -> None:
  51. response.set_cookie(
  52. key=H5_REFRESH_COOKIE,
  53. value=refresh_token,
  54. max_age=settings.refresh_token_days * 24 * 60 * 60,
  55. httponly=True,
  56. secure=settings.app_env == "production",
  57. samesite="lax",
  58. path=H5_REFRESH_COOKIE_PATH,
  59. )
  60. @router.post("/h5/auth/login")
  61. def login(
  62. payload: H5LoginRequest,
  63. request: Request,
  64. response: Response,
  65. ) -> dict[str, Any]:
  66. # payload 已由 FastAPI 转成 H5LoginRequest,无需手动解析 JSON。
  67. result = auth_service.login(payload.mobile, payload.code)
  68. if payload.referral_code and attribution_service is not None:
  69. user = auth_service.authenticate_access(str(result["tokens"]["access_token"]))
  70. result["attribution"] = attribution_service.capture_visit(
  71. user,
  72. payload.referral_code,
  73. )
  74. set_refresh_cookie(response, result["tokens"]["refresh_token"])
  75. return success_response(request, result)
  76. @router.post("/h5/auth/refresh")
  77. def refresh(
  78. request: Request,
  79. response: Response,
  80. payload: RefreshRequest | None = None,
  81. ) -> dict[str, Any]:
  82. refresh_token = (
  83. payload.refresh_token if payload is not None else request.cookies.get(H5_REFRESH_COOKIE)
  84. )
  85. if refresh_token is None:
  86. raise AppError("AUTH_REQUIRED", "缺少刷新凭证", 401)
  87. result = auth_service.refresh(refresh_token)
  88. set_refresh_cookie(response, result["tokens"]["refresh_token"])
  89. return success_response(request, result)
  90. @router.post("/h5/auth/logout")
  91. def logout(
  92. request: Request,
  93. response: Response,
  94. token: Annotated[str, Depends(bearer_token)],
  95. ) -> dict[str, Any]:
  96. auth_service.logout(token)
  97. response.delete_cookie(
  98. key=H5_REFRESH_COOKIE,
  99. path=H5_REFRESH_COOKIE_PATH,
  100. samesite="lax",
  101. )
  102. return success_response(request, {"logged_out": True})
  103. @router.get("/h5/me")
  104. def me(
  105. request: Request,
  106. user: Annotated[H5User, Depends(current_user)],
  107. ) -> dict[str, Any]:
  108. return success_response(
  109. request,
  110. {
  111. "id": user.id,
  112. "mobile_masked": user.mobile_masked,
  113. "display_name": user.display_name,
  114. "status": user.status,
  115. },
  116. )
  117. return router