修复邮件验证部分问题

This commit is contained in:
咕谷酱
2025-09-23 22:13:15 +08:00
parent 7d6eeae073
commit 99d6af1c1f
10 changed files with 770 additions and 31 deletions

View File

@@ -12,11 +12,13 @@ from typing import Literal
from app.config import settings
from app.database.verification import EmailVerification, LoginSession
from app.log import logger
from app.service.client_detection_service import ClientDetectionService, ClientInfo
from app.service.device_trust_service import DeviceTrustService
from app.service.email_queue import email_queue # 导入邮件队列
from app.utils import utcnow
from redis.asyncio import Redis
from sqlmodel import col, exists, select
from sqlmodel import exists, select
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -56,7 +58,7 @@ class EmailVerificationService:
line-height: 1.6;
}}
.header {{
background: linear-gradient(135deg, #ff66aa, #ff9966);
background: #ED8EA6;
color: white;
padding: 20px;
text-align: center;
@@ -69,7 +71,7 @@ class EmailVerificationService:
}}
.code {{
background: #fff;
border: 2px solid #ff66aa;
border: 2px solid #ED8EA6;
border-radius: 8px;
padding: 15px;
text-align: center;
@@ -201,7 +203,7 @@ This email was sent automatically, please do not reply.
existing_result = await db.exec(
select(EmailVerification).where(
EmailVerification.user_id == user_id,
col(EmailVerification.is_used).is_(False),
EmailVerification.is_used == False, # noqa: E712
EmailVerification.expires_at > utcnow(),
)
)
@@ -247,14 +249,37 @@ This email was sent automatically, please do not reply.
email: str,
ip_address: str | None = None,
user_agent: str | None = None,
client_id: int | None = None,
country_code: str | None = None,
) -> bool:
"""发送验证邮件"""
"""发送验证邮件(带智能检测)"""
try:
# 检查是否启用邮件验证功能
if not settings.enable_email_verification:
logger.debug(f"[Email Verification] Email verification is disabled, skipping for user {user_id}")
return True # 返回成功,但不执行验证流程
# 检测客户端信息
client_info = ClientDetectionService.detect_client(user_agent, client_id)
logger.info(
f"[Email Verification] Detected client for user {user_id}: "
f"{ClientDetectionService.format_client_display_name(client_info)}"
)
# 检查是否需要验证
needs_verification, reason = await DeviceTrustService.should_require_verification(
redis=redis,
user_id=user_id,
device_fingerprint=client_info.device_fingerprint,
country_code=country_code,
client_info=client_info,
is_new_location=True, # 这里需要从调用方传入
)
if not needs_verification:
logger.info(f"[Email Verification] Skipping verification for user {user_id}: {reason}")
return True
# 创建验证记录
(
_,
@@ -279,6 +304,107 @@ This email was sent automatically, please do not reply.
logger.error(f"[Email Verification] Exception during sending verification email: {e}")
return False
@staticmethod
async def send_smart_verification_email(
db: AsyncSession,
redis: Redis,
user_id: int,
username: str,
email: str,
ip_address: str | None = None,
user_agent: str | None = None,
client_id: int | None = None,
country_code: str | None = None,
is_new_location: bool = False,
) -> tuple[bool, str, ClientInfo | None]:
"""
智能邮件验证发送
Args:
db: 数据库会话
redis: Redis 连接
user_id: 用户 ID
username: 用户名
email: 邮箱地址
ip_address: IP 地址
user_agent: 用户代理
client_id: 客户端 ID
country_code: 国家代码
is_new_location: 是否为新位置登录
Returns:
tuple[bool, str, ClientInfo | None]: (是否成功, 消息, 客户端信息)
"""
try:
# 检查是否启用邮件验证功能
if not settings.enable_email_verification:
logger.debug(f"[Smart Verification] Email verification is disabled, skipping for user {user_id}")
return True, "邮件验证功能已禁用", None
# 检查是否启用智能验证
if not settings.enable_smart_verification:
logger.debug(
f"[Smart Verification] Smart verification is disabled, using legacy logic for user {user_id}"
)
# 回退到传统验证逻辑
verification, code = await EmailVerificationService.create_verification_record(
db, redis, user_id, email, ip_address, user_agent
)
success = await EmailVerificationService.send_verification_email_via_queue(
email, code, username, user_id
)
return success, "使用传统验证逻辑发送邮件" if success else "传统验证邮件发送失败", None
# 检测客户端信息
client_info = ClientDetectionService.detect_client(user_agent, client_id)
client_display_name = ClientDetectionService.format_client_display_name(client_info)
logger.info(f"[Smart Verification] Detected client for user {user_id}: {client_display_name}")
# 检查是否需要验证
needs_verification, reason = await DeviceTrustService.should_require_verification(
redis=redis,
user_id=user_id,
device_fingerprint=client_info.device_fingerprint,
country_code=country_code,
client_info=client_info,
is_new_location=is_new_location,
)
if not needs_verification:
logger.info(f"[Smart Verification] Skipping verification for user {user_id}: {reason}")
# 即使不需要验证,也要更新设备信任信息
if client_info.device_fingerprint:
await DeviceTrustService.trust_device(redis, user_id, client_info.device_fingerprint, client_info)
if country_code:
await DeviceTrustService.trust_location(redis, user_id, country_code)
return True, f"跳过验证: {reason}", client_info
# 创建验证记录
verification, code = await EmailVerificationService.create_verification_record(
db, redis, user_id, email, ip_address, user_agent
)
_ = verification # 避免未使用变量警告
# 使用邮件队列发送验证邮件
success = await EmailVerificationService.send_verification_email_via_queue(email, code, username, user_id)
if success:
logger.info(
f"[Smart Verification] Successfully sent verification email to {email} "
f"for user {username} using {client_display_name}"
)
return True, "验证邮件已发送", client_info
else:
logger.error(f"[Smart Verification] Failed to send verification email: {email} (user: {username})")
return False, "验证邮件发送失败", client_info
except Exception as e:
logger.error(f"[Smart Verification] Exception during smart verification: {e}")
return False, f"验证过程中发生错误: {e!s}", None
@staticmethod
async def verify_email_code(
db: AsyncSession,
@@ -286,8 +412,11 @@ This email was sent automatically, please do not reply.
user_id: int,
code: str,
ip_address: str | None = None,
user_agent: str | None = None,
client_id: int | None = None,
country_code: str | None = None,
) -> tuple[bool, str]:
"""验证邮箱验证码"""
"""验证邮箱验证码(带智能信任更新)"""
try:
# 检查是否启用邮件验证功能
if not settings.enable_email_verification:
@@ -305,7 +434,7 @@ This email was sent automatically, please do not reply.
EmailVerification.id == int(verification_id),
EmailVerification.user_id == user_id,
EmailVerification.verification_code == code,
col(EmailVerification.is_used).is_(False),
EmailVerification.is_used == False, # noqa: E712
EmailVerification.expires_at > utcnow(),
)
)
@@ -323,6 +452,16 @@ This email was sent automatically, please do not reply.
# 删除 Redis 记录
await redis.delete(f"email_verification:{user_id}:{code}")
# 检测客户端信息并更新信任状态
client_info = ClientDetectionService.detect_client(user_agent, client_id)
await DeviceTrustService.mark_verification_successful(
redis=redis,
user_id=user_id,
device_fingerprint=client_info.device_fingerprint,
country_code=country_code,
client_info=client_info,
)
logger.info(f"[Email Verification] User {user_id} verification code verified successfully")
return True, "验证成功"
@@ -342,6 +481,8 @@ This email was sent automatically, please do not reply.
) -> tuple[bool, str]:
"""重新发送验证码"""
try:
# 避免未使用参数警告
_ = user_agent
# 检查是否启用邮件验证功能
if not settings.enable_email_verification:
logger.debug(f"[Email Verification] Email verification is disabled, skipping resend for user {user_id}")
@@ -465,7 +606,7 @@ class LoginSessionService:
result = await db.exec(
select(LoginSession).where(
LoginSession.user_id == user_id,
col(LoginSession.is_verified).is_(False),
LoginSession.is_verified == False, # noqa: E712
LoginSession.expires_at > utcnow(),
LoginSession.token_id == token_id,
)
@@ -497,7 +638,7 @@ class LoginSessionService:
await db.exec(
select(exists()).where(
LoginSession.user_id == user_id,
col(LoginSession.is_verified).is_(False),
LoginSession.is_verified == False, # noqa: E712
LoginSession.expires_at > utcnow(),
LoginSession.token_id == token_id,
)