70 lines
3.1 KiB
Python
70 lines
3.1 KiB
Python
"""
|
||||
|
|
密码重置路由。
|
|||
|
|
|
|||
|
|
接口(统一响应格式 {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="密码重置成功,请使用新密码登录")
|