添加邮件验证

This commit is contained in:
咕谷酱
2025-08-22 08:19:12 +08:00
parent 42f17d0c66
commit 3bee2421fa
19 changed files with 1594 additions and 22 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from datetime import UTC, datetime, timedelta
import re
from typing import Literal
from typing import Literal, Union
from app.auth import (
authenticate_user,
@@ -28,8 +28,13 @@ from app.models.oauth import (
TokenResponse,
UserRegistrationErrors,
)
from app.models.extended_auth import ExtendedTokenResponse
from app.models.score import GameMode
from app.service.login_log_service import LoginLogService
from app.service.email_verification_service import (
EmailVerificationService,
LoginSessionService
)
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import JSONResponse
@@ -198,7 +203,7 @@ async def register_user(
@router.post(
"/oauth/token",
response_model=TokenResponse,
response_model=Union[TokenResponse, ExtendedTokenResponse],
name="获取访问令牌",
description="OAuth 令牌端点,支持密码、刷新令牌和授权码三种授权方式。",
)
@@ -218,6 +223,7 @@ async def oauth_token(
None, description="刷新令牌(仅刷新令牌模式需要)"
),
redis: Redis = Depends(get_redis),
geoip: GeoIPHelper = Depends(get_geoip_helper),
):
scopes = scope.split(" ")
@@ -295,17 +301,68 @@ async def oauth_token(
# 确保用户对象与当前会话关联
await db.refresh(user)
# 记录成功的登录
# 获取用户信息和客户端信息
user_id = getattr(user, "id")
assert user_id is not None, "User ID should not be None after authentication"
await LoginLogService.record_login(
db=db,
user_id=user_id,
request=request,
login_success=True,
login_method="password",
notes=f"OAuth password grant for client {client_id}",
from app.dependencies.geoip import get_client_ip
ip_address = get_client_ip(request)
user_agent = request.headers.get("User-Agent", "")
# 获取国家代码
geo_info = geoip.lookup(ip_address)
country_code = geo_info.get("country_iso", "XX")
# 检查是否为新位置登录
is_new_location = await LoginSessionService.check_new_location(
db, user_id, ip_address, country_code
)
# 创建登录会话记录
login_session = await LoginSessionService.create_session(
db, redis, user_id, ip_address, user_agent, country_code, is_new_location
)
# 如果是新位置登录,需要邮件验证
if is_new_location and settings.enable_email_verification:
# 刷新用户对象以确保属性已加载
await db.refresh(user)
# 发送邮件验证码
verification_sent = await EmailVerificationService.send_verification_email(
db, redis, user_id, user.username, user.email, ip_address, user_agent
)
# 记录需要二次验证的登录尝试
await LoginLogService.record_login(
db=db,
user_id=user_id,
request=request,
login_success=True,
login_method="password_pending_verification",
notes=f"新位置登录,需要邮件验证 - IP: {ip_address}, 国家: {country_code}",
)
if not verification_sent:
# 邮件发送失败,记录错误
logger.error(f"[Auth] Failed to send email verification code for user {user_id}")
elif is_new_location and not settings.enable_email_verification:
# 新位置登录但邮件验证功能被禁用,直接标记会话为已验证
await LoginSessionService.mark_session_verified(db, user_id)
logger.debug(f"[Auth] New location login detected but email verification disabled, auto-verifying user {user_id}")
else:
# 不是新位置登录,正常登录
await LoginLogService.record_login(
db=db,
user_id=user_id,
request=request,
login_success=True,
login_method="password",
notes=f"正常登录 - IP: {ip_address}, 国家: {country_code}",
)
# 无论是否新位置登录都返回正常的token
# session_verified状态通过/me接口的session_verified字段来体现
# 生成令牌
access_token_expires = timedelta(minutes=settings.access_token_expire_minutes)