148 lines
4.4 KiB
Python
148 lines
4.4 KiB
Python
"""
|
|
密码重置 Use Case
|
|
"""
|
|
|
|
import secrets
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Optional
|
|
|
|
from packages.adapters.smtp import get_email_service
|
|
from packages.domain.auth import password_hasher, password_validator
|
|
|
|
|
|
class RequestPasswordResetRequest:
|
|
"""请求密码重置"""
|
|
|
|
def __init__(self, email: str):
|
|
self.email = email.strip().lower()
|
|
|
|
|
|
class RequestPasswordResetUseCase:
|
|
"""请求密码重置用例"""
|
|
|
|
def __init__(
|
|
self,
|
|
user_repository,
|
|
base_url: str = "http://localhost:3000",
|
|
token_expire_hours: int = 1,
|
|
email_service=None,
|
|
):
|
|
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:
|
|
print(f"Failed to send password reset email: {error}")
|
|
# 不返回错误,避免暴露用户存在性
|
|
except Exception as e:
|
|
print(f"Email service error: {e}")
|
|
|
|
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:
|
|
if datetime.now(timezone.utc) > user.password_reset_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)}"
|