"""登录凭据模型(密码库) Credential 是登录凭据的唯一事实源,粒度 = 站点 × 登录方式: - 同一邮箱注册多个站点 = 多条记录(username 重复是常态而非冗余) - 密码相同也各存一份(改密逐站发生,不做共享联动) - 授权登录(OAuth)是正常条目:password 为空 + login_type=oauth - 2FA(TOTP secret)挂在凭据上,是登录凭据的一部分 平台账号(Account)通过 credential_id 关联本表,账号密码读写全部重定向到此; 普通网站/邮箱等游离凭据直接建在本表,与平台/资产体系解耦。 敏感字段(password / otp secret)用 Fernet 加密存储(MASTER_KEY 不入库), Read schema 永不返回明文/密文,只给 has_password / has_otp 布尔标记。 """ from datetime import datetime from enum import Enum from typing import Optional from sqlmodel import Field, SQLModel from app.core.timeutils import utcnow class LoginType(str, Enum): """登录方式""" PASSWORD = "password" # 用户名 + 密码 OAUTH = "oauth" # 授权登录(Google/Apple/GitHub 等,本站无密码) OTHER = "other" # 其他(魔法链接、硬件 key 等) class Credential(SQLModel, table=True): """登录凭据(密码库条目) 不设 (site, username) 库级唯一约束:SQLite 对 NULL 不友好,且同站多账号合法; 重复录入由服务层提示(允许继续)。 """ __tablename__ = "credentials" id: Optional[int] = Field(default=None, primary_key=True) site: str = Field(index=True, description="站点/服务名,如 GitHub、阿里云") username: Optional[str] = Field( default=None, index=True, description="登录用户名/邮箱" ) login_type: LoginType = Field( default=LoginType.PASSWORD, index=True, description="登录方式" ) oauth_provider: Optional[str] = Field( default=None, description="授权登录来源:google/apple/github/wechat 等" ) password_encrypted: Optional[str] = Field( default=None, description="加密的登录密码(Fernet);oauth 登录为空" ) otp_secret_encrypted: Optional[str] = Field( default=None, description="加密的 TOTP base32 secret(2FA),动态码由服务端生成" ) url: Optional[str] = Field(default=None, description="登录页地址") note: Optional[str] = Field(default=None, description="备注") created_at: datetime = Field(default_factory=utcnow, description="创建时间") updated_at: datetime = Field( default_factory=utcnow, sa_column_kwargs={"onupdate": utcnow}, description="更新时间", )