sms_auth.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. from typing import Any
  2. from fastapi import APIRouter, Depends, HTTPException, Body, Request
  3. from sqlalchemy.orm import Session
  4. from datetime import timedelta
  5. from app.api.v1 import deps
  6. from app.core import security
  7. from app.core.config import settings
  8. from app.core.utils import get_client_ip
  9. from app.core.cache import redis_client
  10. from app.models.user import User, UserStatus
  11. from app.services.sms_service import SmsService
  12. from app.services.system_config_service import SystemConfigService
  13. from app.services.login_log_service import LoginLogService
  14. from app.models.login_log import LoginMethod, AuthType
  15. from app.schemas.login_log import LoginLogCreate
  16. from app.schemas.token import Token
  17. router = APIRouter()
  18. @router.post("/send-code", summary="发送短信验证码")
  19. def send_sms_code(
  20. mobile: str = Body(..., embed=True),
  21. platform: str = Body(..., embed=True, description="pc or mobile"),
  22. db: Session = Depends(deps.get_db),
  23. ) -> Any:
  24. """
  25. 发送短信验证码。
  26. 需检查系统配置是否允许对应平台的短信登录。
  27. """
  28. # Check config
  29. config_key = "sms_login_pc_enabled" if platform == "pc" else "sms_login_mobile_enabled"
  30. is_enabled = SystemConfigService.get_config(db, config_key)
  31. if is_enabled != "true":
  32. raise HTTPException(status_code=403, detail="当前平台未开启短信登录功能")
  33. # Send code
  34. try:
  35. SmsService.send_code(mobile)
  36. except Exception as e:
  37. # Pass through HTTPExceptions from service
  38. if isinstance(e, HTTPException):
  39. raise e
  40. # Log unexpected errors
  41. print(f"Error sending SMS: {e}")
  42. raise HTTPException(status_code=500, detail="发送验证码失败")
  43. return {"message": "验证码已发送"}
  44. @router.post("/login", response_model=Token, summary="短信验证码登录")
  45. def login_with_sms(
  46. request: Request,
  47. mobile: str = Body(..., embed=True),
  48. code: str = Body(..., embed=True),
  49. platform: str = Body("pc", embed=True),
  50. db: Session = Depends(deps.get_db),
  51. ) -> Any:
  52. """
  53. 使用手机号和验证码登录。
  54. """
  55. # 1. Check Config (Double check)
  56. config_key = "sms_login_pc_enabled" if platform == "pc" else "sms_login_mobile_enabled"
  57. is_enabled = SystemConfigService.get_config(db, config_key)
  58. if is_enabled != "true":
  59. raise HTTPException(status_code=403, detail="当前平台未开启短信登录功能")
  60. # 2. Verify Code
  61. # Redis key from SmsService: SMS:{mobile}
  62. key = f"SMS:{mobile}"
  63. stored_code = redis_client.get(key)
  64. if not stored_code or stored_code != code:
  65. raise HTTPException(status_code=400, detail="验证码错误或已过期")
  66. # 3. Find User
  67. user = db.query(User).filter(User.mobile == mobile, User.is_deleted == 0).first()
  68. # Log preparation
  69. log_create = LoginLogCreate(
  70. mobile=mobile,
  71. ip_address=get_client_ip(request),
  72. login_method=LoginMethod.UNIFIED_PAGE,
  73. auth_type=AuthType.SMS,
  74. user_agent=request.headers.get("user-agent")
  75. )
  76. if not user:
  77. log_create.is_success = 0
  78. log_create.failure_reason = "用户不存在"
  79. LoginLogService.create_log(db, log_create)
  80. raise HTTPException(status_code=404, detail="用户不存在")
  81. log_create.user_id = user.id
  82. if user.status != UserStatus.ACTIVE:
  83. log_create.is_success = 0
  84. log_create.failure_reason = "用户已禁用"
  85. LoginLogService.create_log(db, log_create)
  86. raise HTTPException(status_code=400, detail="用户已禁用")
  87. # 4. Generate Token
  88. access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
  89. access_token = security.create_access_token(
  90. subject=user.id,
  91. expires_delta=access_token_expires
  92. )
  93. # Clear Code
  94. redis_client.delete(key)
  95. # Log Success
  96. LoginLogService.create_log(db, log_create)
  97. return {
  98. "access_token": access_token,
  99. "token_type": "bearer",
  100. }