feat: 添加 OAuth 认证支持及相关改进

- 新增 OAuth 模块,支持 LinuxDo/GitHub/Google 等第三方登录
- 用户邮箱改为可选字段,支持无邮箱注册
- 新增模块配置验证状态 (config_validated/config_error)
- 系统设置界面改为分块独立保存
- 用户设置新增 OAuth 绑定管理和首次密码设置
- 登录界面支持 OAuth 按钮展示
- 邮箱验证设置移至邮件设置页面
This commit is contained in:
fawney19
2026-01-19 03:19:17 +08:00
parent e2e14fd09c
commit 3d88dfd98a
61 changed files with 4548 additions and 950 deletions

View File

@@ -37,3 +37,4 @@ class AuthSource(str, Enum):
LOCAL = "local" # 本地认证
LDAP = "ldap" # LDAP 认证
OAUTH = "oauth" # OAuth 认证(账号首创来源)

View File

@@ -81,6 +81,13 @@ FIELD_NAME_TRANSLATIONS = {
"is_pinned": "置顶状态",
"start_time": "开始时间",
"end_time": "结束时间",
# OAuth 相关字段
"client_id": "Client ID",
"client_secret": "Client Secret",
"redirect_uri": "回调地址",
"frontend_callback_url": "前端回调地址",
"display_name": "显示名称",
"scopes": "授权范围",
}
@@ -340,6 +347,18 @@ class NotFoundException(ProxyException):
)
class ConfirmationRequiredException(ProxyException):
"""需要用户确认的操作"""
def __init__(self, message: str, affected_count: int, action: str = "disable"):
super().__init__(
status_code=status.HTTP_409_CONFLICT,
error_type="confirmation_required",
message=message,
details={"affected_count": affected_count, "action": action},
)
class ForbiddenException(ProxyException):
"""权限不足"""

View File

@@ -6,10 +6,11 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional
from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple
if TYPE_CHECKING:
from fastapi import APIRouter
from sqlalchemy.orm import Session
class ModuleCategory(str, Enum):
@@ -84,6 +85,9 @@ class ModuleDefinition:
# 自定义依赖检测(可选,用于检测 ldap3 等库是否安装)
check_dependencies: Optional[Callable[[], bool]] = None
# 配置验证(可选,启用模块时调用,返回 (success, error_message)
validate_config: Optional[Callable[["Session"], Tuple[bool, str]]] = None
@dataclass
class ModuleStatus:
@@ -97,6 +101,8 @@ class ModuleStatus:
available: bool # 部署级可用(环境变量 + 依赖库)
enabled: bool # 运行级启用(数据库配置)
active: bool # 最终激活状态 (available && enabled && dependencies_ok)
config_validated: bool # 配置验证通过(只有验证通过才允许启用)
config_error: Optional[str] # 配置验证失败的错误信息
# 显示信息
display_name: str

View File

@@ -175,6 +175,34 @@ class ModuleRegistry:
return True
# ========== 配置验证 ==========
def validate_config(self, name: str, db: "Session") -> tuple[bool, str]:
"""
验证模块配置是否有效
Args:
name: 模块名称
db: 数据库会话
Returns:
(validated, error_message) - validated 为 True 表示配置有效
"""
if name not in self._modules:
return False, "模块不存在"
module = self._modules[name]
# 没有配置验证函数的模块,默认配置有效
if not module.validate_config:
return True, ""
try:
return module.validate_config(db)
except Exception as e:
logger.warning(f"Module [{name}] config validation error: {e}")
return False, f"配置验证出错: {str(e)}"
# ========== 状态查询 ==========
def get_module_status(
@@ -195,11 +223,28 @@ class ModuleRegistry:
meta = module.metadata
available = self.is_available(name)
# 获取配置验证状态
config_validated = False
config_error: Optional[str] = None
if available:
config_validated, config_error = self.validate_config(name, db)
if config_validated:
config_error = None # 验证通过时清空错误信息
# 获取启用状态
enabled = self.is_enabled(name, db) if available else False
# 注意:配置验证失败时不自动禁用模块
# 自动禁用会在查询方法中产生写操作副作用,违反幂等性原则
# 配置验证状态通过 config_validated/config_error 字段返回,由调用方决定如何处理
return ModuleStatus(
name=name,
available=available,
enabled=self.is_enabled(name, db) if available else False,
enabled=enabled,
active=self.is_active(name, db) if available else False,
config_validated=config_validated,
config_error=config_error,
display_name=meta.display_name,
description=meta.description,
category=meta.category,