""" 密码重置路由。 接口(统一响应格式 {success, data, message}): - POST /api/password/forgot 向注册邮箱发送重置验证码(未注册邮箱也返回同样提示,避免泄露) - POST /api/password/reset 校验验证码并重置密码 """ import os from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy.orm import Session from ..auth import hash_password from ..database import get_db from ..models import PURPOSE_RESET, User from ..schemas import ( ForgotPasswordRequest, ResetPasswordRequest, UnifiedResponse, VerifyCodeRequest, ) from ..security import is_valid_email from .email import issue_code, verify_code router = APIRouter(prefix="/api/password", tags=["password"]) # 密码最小长度:与注册接口保持一致,可通过 .env 的 PASSWORD_MIN_LENGTH 调整(默认 6) PASSWORD_MIN_LENGTH = int(os.getenv("PASSWORD_MIN_LENGTH") or "6") @router.post("/forgot", response_model=UnifiedResponse) def forgot_password( payload: ForgotPasswordRequest, request: Request, db: Session = Depends(get_db), ) -> UnifiedResponse: """忘记密码:向已注册邮箱发送重置验证码(限流逻辑与 send-code 一致)。""" email = payload.email.strip().lower() if not is_valid_email(email): raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="邮箱格式不正确") user = db.query(User).filter(User.email == email).first() if user is None: # 未注册邮箱也返回同样提示,避免泄露邮箱是否已注册 return UnifiedResponse(success=True, data=None, message="如该邮箱已注册,验证码已发送") issue_code(email, PURPOSE_RESET, db, request) return UnifiedResponse(success=True, data=None, message="验证码已发送,请查收邮件") @router.post("/reset", response_model=UnifiedResponse) def reset_password(payload: ResetPasswordRequest, db: Session = Depends(get_db)) -> UnifiedResponse: """重置密码:校验验证码后更新为新密码(验证码一次性使用,且有尝试次数限制)。""" email = payload.email.strip().lower() if len(payload.new_password) < PASSWORD_MIN_LENGTH: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f"新密码至少 {PASSWORD_MIN_LENGTH} 位") if len(payload.new_password) > 128: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="新密码不能超过 128 位") # 校验验证码(通过后验证码作废,一次性使用;尝试次数过多时抛 429) verify_code(VerifyCodeRequest(email=email, code=payload.code, purpose=PURPOSE_RESET), db) user = db.query(User).filter(User.email == email).first() if user is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") user.password_hash = hash_password(payload.new_password) # 自增令牌版本号:使该用户已签发的所有旧 JWT 立即失效(重置密码后需重新登录) user.token_version += 1 db.commit() return UnifiedResponse(success=True, data=None, message="密码重置成功,请使用新密码登录")