Files
xiaoxia-saas/packages/application/auth/password_reset_use_case.py
xiaoxia 4208b5d940
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
fix(#781): 替换print为logging,统一日志输出规范 (#804)
2026-07-24 08:03:14 +08:00

163 lines
4.9 KiB
Python

"""
密码重置 Use Case
"""
import logging
import secrets
from datetime import datetime, timedelta, timezone
from typing import Optional
from packages.adapters.smtp import get_email_service
from packages.application.auth.password_hasher import password_hasher, password_validator
logger = logging.getLogger(__name__)
class RequestPasswordResetRequest:
"""请求密码重置"""
def __init__(self, email: str):
self.email = email.strip().lower()
class RequestPasswordResetUseCase:
"""请求密码重置用例"""
def __init__(
self,
user_repository,
base_url: str,
token_expire_hours: int = 1,
email_service=None,
):
"""
初始化请求密码重置用例
Args:
user_repository: 用户仓储
base_url: 应用基础 URL(用于生成重置链接),必须由调用方传入
token_expire_hours: 令牌过期时间(小时)
email_service: 邮件服务
"""
self.user_repository = user_repository
self.base_url = base_url
self.token_expire_hours = token_expire_hours
self.email_service = email_service or get_email_service()
def execute(self, request: RequestPasswordResetRequest) -> tuple[bool, Optional[str]]:
"""
执行密码重置请求
Args:
request: 重置请求
Returns:
(是否成功, 错误信息)
"""
try:
if not request.email:
return False, "Email is required"
# 查找用户
user = self.user_repository.find_by_email(request.email)
# 安全考虑:即使用户不存在,也返回成功(避免暴露用户存在性)
if not user:
return True, None
# 生成重置令牌
reset_token = secrets.token_urlsafe(32)
reset_url = f"{self.base_url}/reset-password?token={reset_token}"
# 设置令牌和过期时间
user.password_reset_token = reset_token
user.password_reset_expires_at = datetime.now(timezone.utc) + timedelta(hours=self.token_expire_hours)
# 保存用户
self.user_repository.save(user)
# 发送重置邮件
try:
success, error = self.email_service.send_password_reset_email(
to_email=user.email,
username=user.username or user.display_name,
reset_url=reset_url,
)
if not success:
logger.warning(f"Password reset email failed: {error}")
# 不返回错误,避免暴露用户存在性
except Exception as e:
logger.error(f"Email service error: {e}", exc_info=True)
return True, None
except Exception as e:
return False, f"Password reset request failed: {str(e)}"
class ResetPasswordRequest:
"""重置密码请求"""
def __init__(self, token: str, new_password: str):
self.token = token
self.new_password = new_password
class ResetPasswordUseCase:
"""重置密码用例"""
def __init__(self, user_repository):
self.user_repository = user_repository
def execute(self, request: ResetPasswordRequest) -> tuple[bool, Optional[str]]:
"""
执行密码重置
Args:
request: 重置请求
Returns:
(是否成功, 错误信息)
"""
try:
if not request.token:
return False, "Reset token is required"
if not request.new_password:
return False, "New password is required"
# 验证新密码强度
valid, error = password_validator.validate(request.new_password)
if not valid:
return False, error
# 查找用户
user = self.user_repository.find_by_password_reset_token(request.token)
if not user:
return False, "Invalid or expired reset token"
# 检查令牌是否过期
if user.password_reset_expires_at:
expires_at = user.password_reset_expires_at
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
if datetime.now(timezone.utc) > expires_at:
return False, "Reset token has expired"
# 哈希新密码
hashed_password = password_hasher.hash_password(request.new_password)
# 更新用户密码
user.password_hash = hashed_password
user.password_reset_token = None
user.password_reset_expires_at = None
# 保存用户
self.user_repository.save(user)
return True, None
except Exception as e:
return False, f"Password reset failed: {str(e)}"