129 lines
3.9 KiB
Python
129 lines
3.9 KiB
Python
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
|