sms_auth.py 3.9 KB

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