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

@@ -10,10 +10,12 @@ from src.core.modules.base import ModuleDefinition
# 导入所有模块定义
from src.modules.ldap import ldap_module
from src.modules.oauth import oauth_module
# 所有模块列表
ALL_MODULES: List[ModuleDefinition] = [
ldap_module,
oauth_module,
]
__all__ = ["ALL_MODULES"]

View File

@@ -4,6 +4,8 @@ LDAP 认证模块
提供 LDAP/Active Directory 用户认证支持
"""
from typing import TYPE_CHECKING, Tuple
from src.core.modules.base import (
ModuleCategory,
ModuleDefinition,
@@ -11,6 +13,9 @@ from src.core.modules.base import (
ModuleMetadata,
)
if TYPE_CHECKING:
from sqlalchemy.orm import Session
def _get_router():
"""延迟导入路由(避免启动时加载重依赖)"""
@@ -26,6 +31,46 @@ async def _health_check() -> ModuleHealth:
return ModuleHealth.UNKNOWN
def _validate_config(db: "Session") -> Tuple[bool, str]:
"""
验证 LDAP 配置是否可以启用模块
检查项:
1. 配置是否存在
2. 必填字段是否完整
3. 绑定密码是否可解密
注意:不在此处执行连接测试,因为 validate_config 会在每次查询模块状态时调用,
同步阻塞等待 LDAP 服务器响应会严重影响性能。连接测试应在专门的测试接口中进行。
"""
from src.core.crypto import crypto_service
from src.models.database import LDAPConfig
config = db.query(LDAPConfig).first()
if not config:
return False, "请先配置 LDAP 连接信息"
# 检查必填字段
if not config.server_url:
return False, "请配置 LDAP 服务器地址"
if not config.bind_dn:
return False, "请配置绑定 DN"
if not config.base_dn:
return False, "请配置搜索基准 DN"
if not config.bind_password_encrypted:
return False, "请配置绑定密码"
# 尝试解密密码(仅验证可解密,不执行连接测试)
try:
bind_password = crypto_service.decrypt(config.bind_password_encrypted)
if not bind_password:
return False, "绑定密码为空,请重新设置"
except Exception:
return False, "绑定密码解密失败,请重新设置"
return True, ""
# LDAP 模块定义
ldap_module = ModuleDefinition(
metadata=ModuleMetadata(
@@ -47,4 +92,5 @@ ldap_module = ModuleDefinition(
),
router_factory=_get_router,
health_check=_health_check,
validate_config=_validate_config,
)

View File

@@ -0,0 +1,88 @@
"""
OAuth 认证模块
提供可配置的 OAuth 登录/绑定能力。
"""
from typing import TYPE_CHECKING, Tuple
from src.core.modules.base import (
ModuleCategory,
ModuleDefinition,
ModuleHealth,
ModuleMetadata,
)
if TYPE_CHECKING:
from sqlalchemy.orm import Session
def _get_router():
"""延迟导入路由(避免启动时加载重依赖/副作用)。"""
# 延迟 discover避免 alembic/mypy 等场景导入时触发 entry_points 解析
from src.services.auth.oauth.registry import get_oauth_provider_registry
get_oauth_provider_registry().discover_providers()
from src.api.oauth import router
return router
async def _health_check() -> ModuleHealth:
# v1不做外部网络探测避免启动时阻塞
return ModuleHealth.UNKNOWN
def _validate_config(db: "Session") -> Tuple[bool, str]:
"""
验证 OAuth 配置是否可以启用模块
检查项:
1. 至少有一个已启用的 Provider 配置
2. 已启用的 Provider 必须有 client_id 和 client_secret
"""
from src.models.database import OAuthProvider
# 查找所有已启用的 Provider
enabled_providers = (
db.query(OAuthProvider)
.filter(OAuthProvider.is_enabled.is_(True))
.all()
)
if not enabled_providers:
return False, "请先配置并启用至少一个 OAuth Provider"
# 检查每个已启用的 Provider 配置完整性
for provider in enabled_providers:
if not provider.client_id:
return False, f"Provider [{provider.display_name}] 未配置 Client ID"
if not provider.client_secret_encrypted:
return False, f"Provider [{provider.display_name}] 未配置 Client Secret"
if not provider.redirect_uri:
return False, f"Provider [{provider.display_name}] 未配置回调地址"
return True, ""
oauth_module = ModuleDefinition(
metadata=ModuleMetadata(
name="oauth",
display_name="OAuth 登录",
description="支持通过第三方 OAuth Provider 登录/绑定账号",
category=ModuleCategory.AUTH,
env_key="OAUTH_AVAILABLE",
default_available=True,
required_packages=["httpx", "redis"],
api_prefix="/api/oauth",
admin_route="/admin/oauth",
admin_menu_icon="Key",
admin_menu_group="system",
admin_menu_order=55,
),
router_factory=_get_router,
health_check=_health_check,
validate_config=_validate_config,
)