mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-05 23:56:36 +08:00
big update
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import math
|
||||
import os
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, timezone
|
||||
from typing import Tuple
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
@@ -30,8 +31,18 @@ from app.utils.email_utils import send_html_email
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
REGISTER_CODE_EXPIRE_MINUTES = int(os.getenv("REGISTER_CODE_EXPIRE_MINUTES", "10"))
|
||||
LOGIN_CODE_EXPIRE_MINUTES = int(os.getenv("LOGIN_CODE_EXPIRE_MINUTES", "10"))
|
||||
DEFAULT_REGISTER_CODE_EXPIRE_MINUTES = 10
|
||||
DEFAULT_LOGIN_CODE_EXPIRE_MINUTES = 10
|
||||
DEFAULT_CODE_SEND_COOLDOWN_SECONDS = 60
|
||||
REGISTER_CODE_EXPIRE_MINUTES = int(
|
||||
os.getenv("REGISTER_CODE_EXPIRE_MINUTES", str(DEFAULT_REGISTER_CODE_EXPIRE_MINUTES))
|
||||
)
|
||||
LOGIN_CODE_EXPIRE_MINUTES = int(
|
||||
os.getenv("LOGIN_CODE_EXPIRE_MINUTES", str(DEFAULT_LOGIN_CODE_EXPIRE_MINUTES))
|
||||
)
|
||||
CODE_SEND_COOLDOWN_SECONDS = int(
|
||||
os.getenv("CODE_SEND_COOLDOWN_SECONDS", str(DEFAULT_CODE_SEND_COOLDOWN_SECONDS))
|
||||
)
|
||||
|
||||
|
||||
def _normalize_email(email: str) -> str:
|
||||
@@ -78,6 +89,41 @@ def _create_code_record(
|
||||
return code_record, code
|
||||
|
||||
|
||||
def _enforce_code_send_cooldown(db: Session, email: str, purpose: VerificationPurpose) -> None:
|
||||
"""
|
||||
防抖:限制同一邮箱同一用途验证码的发送频率,避免用户短时间连续点击。
|
||||
"""
|
||||
if CODE_SEND_COOLDOWN_SECONDS <= 0:
|
||||
return
|
||||
|
||||
latest_record = (
|
||||
db.query(EmailVerificationCode)
|
||||
.filter(
|
||||
EmailVerificationCode.email == email,
|
||||
EmailVerificationCode.purpose == purpose,
|
||||
)
|
||||
.order_by(EmailVerificationCode.created_at.desc())
|
||||
.first()
|
||||
)
|
||||
if not latest_record:
|
||||
return
|
||||
|
||||
now = utcnow()
|
||||
record_time = latest_record.created_at
|
||||
if record_time.tzinfo is None:
|
||||
record_time = record_time.replace(tzinfo=timezone.utc)
|
||||
elapsed_seconds = (now - record_time).total_seconds()
|
||||
if elapsed_seconds >= CODE_SEND_COOLDOWN_SECONDS:
|
||||
return
|
||||
|
||||
retry_after_seconds = max(1, math.ceil(CODE_SEND_COOLDOWN_SECONDS - elapsed_seconds))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
detail=f"Please wait {retry_after_seconds}s before requesting another verification code",
|
||||
headers={"Retry-After": str(retry_after_seconds)},
|
||||
)
|
||||
|
||||
|
||||
def _build_auth_response(user: AppUser) -> AuthTokenResponse:
|
||||
token, expires_in = create_access_token(user_id=user.id, email=user.email)
|
||||
return AuthTokenResponse(
|
||||
@@ -95,6 +141,7 @@ async def send_register_code(payload: RegisterCodeSendRequest, db: Session = Dep
|
||||
if existing_user:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email is already registered")
|
||||
|
||||
_enforce_code_send_cooldown(db, email, VerificationPurpose.REGISTER)
|
||||
_invalidate_unused_codes(db, email, VerificationPurpose.REGISTER)
|
||||
code_record, code = _create_code_record(
|
||||
db,
|
||||
@@ -128,6 +175,7 @@ async def send_login_code(payload: LoginCodeSendRequest, db: Session = Depends(g
|
||||
if not user:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Email is not registered")
|
||||
|
||||
_enforce_code_send_cooldown(db, email, VerificationPurpose.LOGIN)
|
||||
_invalidate_unused_codes(db, email, VerificationPurpose.LOGIN)
|
||||
code_record, code = _create_code_record(
|
||||
db,
|
||||
|
||||
Reference in New Issue
Block a user