feat: add multi-provider auth via AuthIdentity and extend site config

- Extract AuthIdentity model for multi-provider authentication (email_password, OAuth, Passkey, Magic Link)
- Remove password field from User model, credentials now stored in AuthIdentity
- Refactor unified login/register to use AuthIdentity-based provider checking
- Add site config fields: footer_code, tos_url, privacy_url, auth_methods
- Add auth settings defaults in migration (email_password enabled by default)
- Update admin user creation to create AuthIdentity records
- Update all tests to use AuthIdentity model

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 22:49:12 +08:00
parent d831c9c0d6
commit 729773cae3
20 changed files with 1447 additions and 412 deletions

139
sqlmodels/auth_identity.py Normal file
View File

@@ -0,0 +1,139 @@
"""
认证身份模块
一个用户可拥有多种登录方式邮箱密码、OAuth、Passkey、Magic Link 等)。
AuthIdentity 表存储每种认证方式的凭证信息。
"""
from enum import StrEnum
from typing import TYPE_CHECKING
from uuid import UUID
from sqlmodel import Field, Relationship, UniqueConstraint
from .base import SQLModelBase
from .mixin import UUIDTableBaseMixin
if TYPE_CHECKING:
from .user import User
class AuthProviderType(StrEnum):
"""认证提供者类型"""
EMAIL_PASSWORD = "email_password"
"""邮箱+密码"""
PHONE_SMS = "phone_sms"
"""手机号+短信验证码(预留)"""
GITHUB = "github"
"""GitHub OAuth"""
QQ = "qq"
"""QQ OAuth"""
PASSKEY = "passkey"
"""Passkey/WebAuthn"""
MAGIC_LINK = "magic_link"
"""邮箱 Magic Link"""
# ==================== DTO 模型 ====================
class AuthIdentityResponse(SQLModelBase):
"""认证身份响应 DTO列表展示用"""
id: UUID
"""身份UUID"""
provider: AuthProviderType
"""提供者类型"""
identifier: str
"""标识符(邮箱/手机号/OAuth openid"""
display_name: str | None = None
"""显示名称OAuth 昵称等)"""
avatar_url: str | None = None
"""头像 URL"""
is_primary: bool = False
"""是否主要身份"""
is_verified: bool = False
"""是否已验证"""
class BindIdentityRequest(SQLModelBase):
"""绑定认证身份请求 DTO"""
provider: AuthProviderType
"""提供者类型"""
identifier: str
"""标识符(邮箱/手机号/OAuth code"""
credential: str | None = None
"""凭证(密码、验证码等)"""
redirect_uri: str | None = None
"""OAuth 回调地址"""
# ==================== 数据库模型 ====================
class AuthIdentity(SQLModelBase, UUIDTableBaseMixin):
"""用户认证身份 — 一个用户可以有多种登录方式"""
__table_args__ = (
UniqueConstraint("provider", "identifier", name="uq_auth_identity_provider_identifier"),
)
provider: AuthProviderType = Field(index=True)
"""提供者类型"""
identifier: str = Field(max_length=255, index=True)
"""标识符(邮箱/手机号/OAuth openid"""
credential: str | None = Field(default=None, max_length=1024)
"""凭证Argon2 哈希密码 / null"""
display_name: str | None = Field(default=None, max_length=100)
"""OAuth 昵称"""
avatar_url: str | None = Field(default=None, max_length=512)
"""OAuth 头像 URL"""
extra_data: str | None = None
"""JSON 附加数据2FA secret、OAuth refresh_token 等)"""
is_primary: bool = False
"""是否主要身份"""
is_verified: bool = False
"""是否已验证"""
# 外键
user_id: UUID = Field(
foreign_key="user.id",
index=True,
ondelete="CASCADE",
)
"""所属用户UUID"""
# 关系
user: "User" = Relationship(back_populates="auth_identities")
def to_response(self) -> AuthIdentityResponse:
"""转换为响应 DTO"""
return AuthIdentityResponse(
id=self.id,
provider=self.provider,
identifier=self.identifier,
display_name=self.display_name,
avatar_url=self.avatar_url,
is_primary=self.is_primary,
is_verified=self.is_verified,
)