Files

70 lines
3.1 KiB
Python
Raw Permalink Normal View History

2026-08-22 22:28:41 +08:00
"""
密码重置路由。
接口(统一响应格式 {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="密码重置成功,请使用新密码登录")