Initial commit: MyBlog full stack blog
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
密码重置路由。
|
||||
|
||||
接口(统一响应格式 {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="密码重置成功,请使用新密码登录")
|
||||
Reference in New Issue
Block a user