feature:新增邮箱注册登录功能
This commit is contained in:
+531
-209
File diff suppressed because it is too large
Load Diff
+152
-1
@@ -18,12 +18,14 @@ from app.services.cover_generation_service import cover_generation_service
|
||||
from app.schemas.settings import (
|
||||
SettingsCreate, SettingsUpdate, SettingsResponse,
|
||||
APIKeyPreset, APIKeyPresetConfig, PresetCreateRequest,
|
||||
PresetUpdateRequest, PresetResponse, PresetListResponse
|
||||
PresetUpdateRequest, PresetResponse, PresetListResponse,
|
||||
SystemSMTPSettingsResponse, SystemSMTPSettingsUpdate, SMTPTestRequest
|
||||
)
|
||||
from app.user_manager import User
|
||||
from app.logger import get_logger
|
||||
from app.config import settings as app_settings, PROJECT_ROOT
|
||||
from app.services.ai_service import AIService, create_user_ai_service, create_user_ai_service_with_mcp, normalize_provider
|
||||
from app.services.email_service import email_service
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -56,6 +58,46 @@ def require_login(request: Request):
|
||||
return request.state.user
|
||||
|
||||
|
||||
def require_admin(user: User = Depends(require_login)):
|
||||
"""依赖:要求管理员权限"""
|
||||
if not user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="仅管理员可访问系统设置")
|
||||
return user
|
||||
|
||||
|
||||
async def get_or_create_admin_settings(db: AsyncSession, user: User) -> Settings:
|
||||
"""获取或创建管理员设置,系统级 SMTP 配置挂在管理员设置记录上"""
|
||||
result = await db.execute(
|
||||
select(Settings).where(Settings.user_id == user.user_id)
|
||||
)
|
||||
settings = result.scalar_one_or_none()
|
||||
|
||||
if not settings:
|
||||
env_defaults = read_env_defaults()
|
||||
settings = Settings(
|
||||
user_id=user.user_id,
|
||||
smtp_provider=app_settings.SMTP_PROVIDER,
|
||||
smtp_host=app_settings.SMTP_HOST,
|
||||
smtp_port=app_settings.SMTP_PORT,
|
||||
smtp_username=app_settings.SMTP_USERNAME,
|
||||
smtp_password=app_settings.SMTP_PASSWORD,
|
||||
smtp_use_tls=app_settings.SMTP_USE_TLS,
|
||||
smtp_use_ssl=app_settings.SMTP_USE_SSL,
|
||||
smtp_from_email=app_settings.SMTP_FROM_EMAIL,
|
||||
smtp_from_name=app_settings.SMTP_FROM_NAME,
|
||||
email_auth_enabled=app_settings.EMAIL_AUTH_ENABLED,
|
||||
email_register_enabled=app_settings.EMAIL_REGISTER_ENABLED,
|
||||
verification_code_ttl_minutes=app_settings.EMAIL_VERIFICATION_CODE_TTL_MINUTES,
|
||||
verification_resend_interval_seconds=app_settings.EMAIL_VERIFICATION_RESEND_INTERVAL_SECONDS,
|
||||
**env_defaults
|
||||
)
|
||||
db.add(settings)
|
||||
await db.commit()
|
||||
await db.refresh(settings)
|
||||
|
||||
return settings
|
||||
|
||||
|
||||
async def get_user_ai_service(
|
||||
user: User = Depends(require_login),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
@@ -169,6 +211,115 @@ async def test_cover_settings(
|
||||
}
|
||||
|
||||
|
||||
@router.get("/system/smtp", response_model=SystemSMTPSettingsResponse)
|
||||
async def get_system_smtp_settings(
|
||||
user: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""获取系统 SMTP 设置(仅管理员)"""
|
||||
settings = await get_or_create_admin_settings(db, user)
|
||||
return settings
|
||||
|
||||
|
||||
@router.put("/system/smtp", response_model=SystemSMTPSettingsResponse)
|
||||
async def update_system_smtp_settings(
|
||||
data: SystemSMTPSettingsUpdate,
|
||||
user: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""更新系统 SMTP 设置(仅管理员)"""
|
||||
settings = await get_or_create_admin_settings(db, user)
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
if update_data.get("smtp_provider") == "qq":
|
||||
update_data.setdefault("smtp_host", "smtp.qq.com")
|
||||
update_data.setdefault("smtp_port", 465)
|
||||
update_data.setdefault("smtp_use_ssl", True)
|
||||
update_data.setdefault("smtp_use_tls", False)
|
||||
|
||||
if update_data.get("smtp_use_ssl") and update_data.get("smtp_use_tls"):
|
||||
raise HTTPException(status_code=400, detail="SSL 和 TLS 不能同时启用")
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(settings, key, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(settings)
|
||||
logger.info(f"管理员 {user.user_id} 更新系统 SMTP 设置")
|
||||
return settings
|
||||
|
||||
|
||||
@router.post("/system/smtp/test")
|
||||
async def test_system_smtp_settings(
|
||||
data: SMTPTestRequest,
|
||||
user: User = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""测试系统 SMTP 设置(真实发送测试邮件)"""
|
||||
settings = await get_or_create_admin_settings(db, user)
|
||||
|
||||
if not settings.smtp_host or not settings.smtp_username or not settings.smtp_password:
|
||||
raise HTTPException(status_code=400, detail="请先完善 SMTP 主机、用户名和授权码")
|
||||
|
||||
if settings.smtp_provider == "qq" and settings.smtp_host != "smtp.qq.com":
|
||||
raise HTTPException(status_code=400, detail="QQ 邮箱 SMTP 主机必须为 smtp.qq.com")
|
||||
|
||||
if "@" not in data.to_email or "." not in data.to_email.split("@")[-1]:
|
||||
raise HTTPException(status_code=400, detail="测试收件邮箱格式不正确")
|
||||
|
||||
from_email = settings.smtp_from_email or settings.smtp_username
|
||||
if not from_email:
|
||||
raise HTTPException(status_code=400, detail="请先配置发件人邮箱或 SMTP 用户名")
|
||||
|
||||
subject = "MuMuAINovel SMTP 测试邮件"
|
||||
text_body = (
|
||||
"这是一封来自 MuMuAINovel 系统设置页面的 SMTP 测试邮件。\n\n"
|
||||
f"发送时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"SMTP 服务商:{settings.smtp_provider}\n"
|
||||
f"SMTP 主机:{settings.smtp_host}:{settings.smtp_port}\n"
|
||||
"如果你收到这封邮件,说明当前 SMTP 配置可正常发送邮件。"
|
||||
)
|
||||
html_body = f"""
|
||||
<div style=\"font-family: Arial, sans-serif; line-height: 1.7; color: #1f1f1f;\">
|
||||
<h2 style=\"margin-bottom: 12px;\">MuMuAINovel SMTP 测试邮件</h2>
|
||||
<p>这是一封来自系统设置页面的 SMTP 测试邮件。</p>
|
||||
<ul>
|
||||
<li><strong>发送时间:</strong>{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</li>
|
||||
<li><strong>SMTP 服务商:</strong>{settings.smtp_provider}</li>
|
||||
<li><strong>SMTP 主机:</strong>{settings.smtp_host}:{settings.smtp_port}</li>
|
||||
</ul>
|
||||
<p>如果你收到这封邮件,说明当前 SMTP 配置可正常发送邮件。</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
try:
|
||||
await email_service.send_mail(
|
||||
host=settings.smtp_host,
|
||||
port=settings.smtp_port,
|
||||
username=settings.smtp_username,
|
||||
password=settings.smtp_password,
|
||||
use_tls=settings.smtp_use_tls,
|
||||
use_ssl=settings.smtp_use_ssl,
|
||||
from_email=from_email,
|
||||
from_name=settings.smtp_from_name,
|
||||
to_email=data.to_email,
|
||||
subject=subject,
|
||||
text_body=text_body,
|
||||
html_body=html_body,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception(f"SMTP 测试邮件发送失败: {exc}")
|
||||
raise HTTPException(status_code=400, detail=f"SMTP 测试邮件发送失败: {str(exc)}") from exc
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"测试邮件已发送至 {data.to_email},请检查收件箱和垃圾箱",
|
||||
"provider": settings.smtp_provider,
|
||||
"host": settings.smtp_host,
|
||||
"port": settings.smtp_port,
|
||||
}
|
||||
|
||||
|
||||
@router.post("", response_model=SettingsResponse)
|
||||
async def save_settings(
|
||||
data: SettingsCreate,
|
||||
|
||||
@@ -106,6 +106,21 @@ class Settings(BaseSettings):
|
||||
# 会话配置
|
||||
SESSION_EXPIRE_MINUTES: int = 120 # 会话过期时间(分钟),默认2小时
|
||||
SESSION_REFRESH_THRESHOLD_MINUTES: int = 30 # 会话刷新阈值(分钟),剩余时间少于此值时可刷新
|
||||
|
||||
# 系统 SMTP 默认配置(可被管理员系统设置覆盖)
|
||||
SMTP_PROVIDER: str = "qq"
|
||||
SMTP_HOST: Optional[str] = "smtp.qq.com"
|
||||
SMTP_PORT: int = 465
|
||||
SMTP_USERNAME: Optional[str] = None
|
||||
SMTP_PASSWORD: Optional[str] = None
|
||||
SMTP_USE_TLS: bool = False
|
||||
SMTP_USE_SSL: bool = True
|
||||
SMTP_FROM_EMAIL: Optional[str] = None
|
||||
SMTP_FROM_NAME: str = "MuMuAINovel"
|
||||
EMAIL_AUTH_ENABLED: bool = True
|
||||
EMAIL_REGISTER_ENABLED: bool = True
|
||||
EMAIL_VERIFICATION_CODE_TTL_MINUTES: int = 10
|
||||
EMAIL_VERIFICATION_RESEND_INTERVAL_SECONDS: int = 60
|
||||
|
||||
# 提示词工坊配置
|
||||
WORKSHOP_MODE: str = "client" # client: 本地部署实例, server: 云端中央服务器
|
||||
|
||||
@@ -26,6 +26,21 @@ class Settings(Base):
|
||||
cover_image_model = Column(String(100), comment="封面图片模型名称")
|
||||
cover_enabled = Column(Boolean, default=False, server_default="0", nullable=False, comment="是否启用封面图片生成")
|
||||
|
||||
# 系统级 SMTP 配置(仅管理员维护)
|
||||
smtp_provider = Column(String(50), default="qq", server_default="qq", nullable=False, comment="SMTP 提供商")
|
||||
smtp_host = Column(String(255), comment="SMTP 主机")
|
||||
smtp_port = Column(Integer, default=465, server_default="465", nullable=False, comment="SMTP 端口")
|
||||
smtp_username = Column(String(255), comment="SMTP 用户名")
|
||||
smtp_password = Column(String(500), comment="SMTP 密码或授权码")
|
||||
smtp_use_tls = Column(Boolean, default=False, server_default="0", nullable=False, comment="是否启用 TLS")
|
||||
smtp_use_ssl = Column(Boolean, default=True, server_default="1", nullable=False, comment="是否启用 SSL")
|
||||
smtp_from_email = Column(String(255), comment="发件人邮箱")
|
||||
smtp_from_name = Column(String(255), default="MuMuAINovel", server_default="MuMuAINovel", nullable=False, comment="发件人名称")
|
||||
email_auth_enabled = Column(Boolean, default=True, server_default="1", nullable=False, comment="是否启用邮箱认证")
|
||||
email_register_enabled = Column(Boolean, default=True, server_default="1", nullable=False, comment="是否启用邮箱注册")
|
||||
verification_code_ttl_minutes = Column(Integer, default=10, server_default="10", nullable=False, comment="验证码有效期(分钟)")
|
||||
verification_resend_interval_seconds = Column(Integer, default=60, server_default="60", nullable=False, comment="验证码重发间隔(秒)")
|
||||
|
||||
preferences = Column(Text, comment="其他偏好设置(JSON)")
|
||||
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
|
||||
|
||||
@@ -43,6 +43,47 @@ class SettingsResponse(SettingsBase):
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class SystemSMTPSettingsBase(BaseModel):
|
||||
"""系统 SMTP 设置基础模型"""
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
smtp_provider: str = Field(default="qq", description="SMTP 提供商")
|
||||
smtp_host: Optional[str] = Field(default=None, description="SMTP 主机")
|
||||
smtp_port: int = Field(default=465, ge=1, le=65535, description="SMTP 端口")
|
||||
smtp_username: Optional[str] = Field(default=None, description="SMTP 用户名")
|
||||
smtp_password: Optional[str] = Field(default=None, description="SMTP 密码或授权码")
|
||||
smtp_use_tls: bool = Field(default=False, description="是否启用 TLS")
|
||||
smtp_use_ssl: bool = Field(default=True, description="是否启用 SSL")
|
||||
smtp_from_email: Optional[str] = Field(default=None, description="发件人邮箱")
|
||||
smtp_from_name: str = Field(default="MuMuAINovel", description="发件人名称")
|
||||
email_auth_enabled: bool = Field(default=True, description="是否启用邮箱认证")
|
||||
email_register_enabled: bool = Field(default=True, description="是否启用邮箱注册")
|
||||
verification_code_ttl_minutes: int = Field(default=10, ge=1, le=120, description="验证码有效期(分钟)")
|
||||
verification_resend_interval_seconds: int = Field(default=60, ge=10, le=3600, description="验证码重发间隔(秒)")
|
||||
|
||||
|
||||
class SystemSMTPSettingsUpdate(SystemSMTPSettingsBase):
|
||||
"""系统 SMTP 设置更新模型"""
|
||||
pass
|
||||
|
||||
|
||||
class SystemSMTPSettingsResponse(SystemSMTPSettingsBase):
|
||||
"""系统 SMTP 设置响应模型"""
|
||||
model_config = ConfigDict(from_attributes=True, protected_namespaces=())
|
||||
|
||||
id: str
|
||||
user_id: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class SMTPTestRequest(BaseModel):
|
||||
"""SMTP 测试请求模型"""
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
to_email: str = Field(..., min_length=3, max_length=255, description="测试收件邮箱")
|
||||
|
||||
|
||||
# ========== API配置预设相关模型 ==========
|
||||
|
||||
class APIKeyPresetConfig(BaseModel):
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""SMTP 邮件发送服务"""
|
||||
from __future__ import annotations
|
||||
|
||||
from email.message import EmailMessage
|
||||
from typing import Optional
|
||||
|
||||
import aiosmtplib
|
||||
|
||||
from app.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class EmailService:
|
||||
"""系统 SMTP 邮件发送服务"""
|
||||
|
||||
async def send_mail(
|
||||
self,
|
||||
*,
|
||||
host: str,
|
||||
port: int,
|
||||
username: str,
|
||||
password: str,
|
||||
use_tls: bool,
|
||||
use_ssl: bool,
|
||||
from_email: str,
|
||||
from_name: str,
|
||||
to_email: str,
|
||||
subject: str,
|
||||
text_body: str,
|
||||
html_body: Optional[str] = None,
|
||||
) -> None:
|
||||
if use_tls and use_ssl:
|
||||
raise ValueError("SMTP 配置错误:TLS 和 SSL 不能同时启用")
|
||||
|
||||
message = EmailMessage()
|
||||
message['From'] = f'{from_name} <{from_email}>' if from_name else from_email
|
||||
message['To'] = to_email
|
||||
message['Subject'] = subject
|
||||
message.set_content(text_body)
|
||||
if html_body:
|
||||
message.add_alternative(html_body, subtype='html')
|
||||
|
||||
logger.info(f"[SMTP] 准备发送测试邮件到 {self._mask_email(to_email)},服务器: {host}:{port}")
|
||||
|
||||
await aiosmtplib.send(
|
||||
message,
|
||||
hostname=host,
|
||||
port=port,
|
||||
username=username,
|
||||
password=password,
|
||||
use_tls=use_ssl,
|
||||
start_tls=use_tls,
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
logger.info(f"[SMTP] 测试邮件发送成功: {self._mask_email(to_email)}")
|
||||
|
||||
@staticmethod
|
||||
def _mask_email(email: str) -> str:
|
||||
if '@' not in email:
|
||||
return email
|
||||
name, domain = email.split('@', 1)
|
||||
if len(name) <= 2:
|
||||
masked_name = name[0] + '*'
|
||||
else:
|
||||
masked_name = name[0] + '*' * (len(name) - 2) + name[-1]
|
||||
return f"{masked_name}@{domain}"
|
||||
|
||||
|
||||
email_service = EmailService()
|
||||
Reference in New Issue
Block a user