Some checks failed
Test / test (push) Failing after 3m47s
- 替换 Field(max_length=X) 为 StrX/TextX 类型别名(21 个 sqlmodels 文件) - 替换 get + 404 检查为 get_exist_one()(17 个路由文件,约 50 处) - 替换 save + session.refresh 为 save(load=...) - 替换 session.add + commit 为 save()(dav/provider.py) - 更新所有依赖至最新版本 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
149 lines
3.5 KiB
Python
149 lines
3.5 KiB
Python
"""
|
||
认证身份模块
|
||
|
||
一个用户可拥有多种登录方式(邮箱密码、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 sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, Str100, Str128, Str255, Text1024
|
||
|
||
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 ChangePasswordRequest(SQLModelBase):
|
||
"""修改密码请求 DTO"""
|
||
|
||
old_password: str = Field(min_length=1)
|
||
"""当前密码"""
|
||
|
||
new_password: Str128 = Field(min_length=8)
|
||
"""新密码(至少 8 位)"""
|
||
|
||
|
||
# ==================== 数据库模型 ====================
|
||
|
||
class AuthIdentity(SQLModelBase, UUIDTableBaseMixin):
|
||
"""用户认证身份 — 一个用户可以有多种登录方式"""
|
||
|
||
__table_args__ = (
|
||
UniqueConstraint("provider", "identifier", name="uq_auth_identity_provider_identifier"),
|
||
)
|
||
|
||
provider: AuthProviderType = Field(index=True)
|
||
"""提供者类型"""
|
||
|
||
identifier: Str255 = Field(index=True)
|
||
"""标识符(邮箱/手机号/OAuth openid)"""
|
||
|
||
credential: Text1024 | None = None
|
||
"""凭证(Argon2 哈希密码 / null)"""
|
||
|
||
display_name: Str100 | None = None
|
||
"""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,
|
||
)
|