完善邮件验证与密码重置安全流程

This commit is contained in:
Cheng Zhou
2026-06-30 09:44:24 +08:00
parent 6c2bcc59b2
commit b25055775e
27 changed files with 4437 additions and 526 deletions
+128
View File
@@ -0,0 +1,128 @@
from __future__ import annotations
from datetime import datetime
from typing import Literal, Optional
from pydantic import BaseModel, EmailStr, Field, field_validator
SmtpSecurityValue = Literal["NONE", "SSL", "STARTTLS"]
class EmailSettingsRead(BaseModel):
register_domain: str = "huapont.cn"
smtp_host: str = ""
smtp_port: int = 465
smtp_security: SmtpSecurityValue = "SSL"
smtp_username: str = ""
smtp_password_configured: bool = False
sender_email: str = ""
sender_name: Optional[str] = "CTMS 系统"
allowed_register_domain: str = "huapont.cn,qq.com"
verification_code_ttl_minutes: int = 10
send_cooldown_seconds: int = 60
max_verify_attempts: int = 5
updated_at: Optional[datetime] = None
class EmailSettingsUpdate(BaseModel):
smtp_host: str = Field(default="", max_length=255)
smtp_port: int = Field(ge=1, le=65535)
smtp_security: SmtpSecurityValue = "SSL"
smtp_username: str = Field(default="", max_length=255)
smtp_password: Optional[str] = Field(default=None, max_length=500)
sender_email: str = Field(default="", max_length=255)
sender_name: Optional[str] = Field(default="CTMS 系统", max_length=255)
allowed_register_domain: str = Field(default="", max_length=255)
verification_code_ttl_minutes: int = Field(default=10, ge=1, le=60)
send_cooldown_seconds: int = Field(default=60, ge=10, le=3600)
max_verify_attempts: int = Field(default=5, ge=1, le=20)
@field_validator("allowed_register_domain")
@classmethod
def normalize_domain(cls, value: str) -> str:
domains = []
for item in value.replace("", ",").split(","):
domain = item.strip().lower().lstrip("@")
if domain and domain not in domains:
domains.append(domain)
if not domains:
raise ValueError("至少配置一个允许注册邮箱域名")
return ",".join(domains)
class EmailTestRequest(BaseModel):
recipient_email: EmailStr
class EmailDomainCreateRequest(BaseModel):
register_domain: str = Field(min_length=1, max_length=255)
@field_validator("register_domain")
@classmethod
def normalize_domain(cls, value: str) -> str:
return value.strip().lower().lstrip("@")
class EmailSettingsListResponse(BaseModel):
items: list[EmailSettingsRead]
total: int
class RegisterEmailCodeSendRequest(BaseModel):
email: EmailStr
class RegisterEmailCodeVerifyRequest(BaseModel):
email: EmailStr
code: str = Field(min_length=4, max_length=12)
class PasswordResetLinkSendRequest(BaseModel):
email: EmailStr
class PasswordResetRequest(BaseModel):
email: EmailStr
code: str = Field(min_length=4, max_length=12)
password: str = Field(min_length=8, max_length=72)
@field_validator("password")
@classmethod
def validate_password_strength(cls, value: str) -> str:
has_letter = any(ch.isalpha() for ch in value)
has_number = any(ch.isdigit() for ch in value)
if not has_letter or not has_number:
raise ValueError("密码需至少 8 位且包含字母和数字")
return value
class PasswordResetCodeVerifyRequest(BaseModel):
email: EmailStr
code: str = Field(min_length=4, max_length=12)
class PasswordResetTokenRequest(BaseModel):
token: str = Field(min_length=20, max_length=200)
password: str = Field(min_length=8, max_length=72)
@field_validator("password")
@classmethod
def validate_password_strength(cls, value: str) -> str:
has_letter = any(ch.isalpha() for ch in value)
has_number = any(ch.isdigit() for ch in value)
if not has_letter or not has_number:
raise ValueError("密码需至少 8 位且包含字母和数字")
return value
class EmailCodeResponse(BaseModel):
message: str
class EmailCodeVerifyResponse(BaseModel):
verified: bool
class PasswordResetCodeVerifyResponse(BaseModel):
verified: bool
reset_token: str