feat(02-01): add BackupCode ORM model, password_must_change field, Alembic migration, extend Settings
- Add BackupCode model to db/models.py with user_id FK, code_hash (Argon2), used_at (nullable) - Add ix_backup_codes_user_id index on backup_codes.user_id - Add password_must_change BOOLEAN NOT NULL DEFAULT false to User model (ADMIN-01) - Extend config.py Settings with JWT, SMTP, admin bootstrap, and CORS fields (D-01, D-04, D-09) - Add env_list_separator=',' for cors_origins env var parsing - Append PyJWT, pwdlib[argon2], pyotp, aioredis, slowapi to requirements.txt - Add .env.example entries for SECRET_KEY, ADMIN_EMAIL, SMTP_*, CORS_ORIGINS - Create migration 0002 adding backup_codes table and password_must_change column - Add TDD tests for all Task 1 acceptance criteria (7 tests pass)
This commit is contained in:
@@ -55,6 +55,7 @@ class User(Base):
|
||||
totp_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
role: Mapped[str] = mapped_column(String, nullable=False, default="user")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
password_must_change: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false")
|
||||
ai_provider: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
ai_model: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
||||
default_storage_backend: Mapped[str] = mapped_column(
|
||||
@@ -103,6 +104,32 @@ class RefreshToken(Base):
|
||||
__table_args__ = (Index("ix_refresh_tokens_user_revoked", "user_id", "revoked"),)
|
||||
|
||||
|
||||
class BackupCode(Base):
|
||||
"""Single-use backup codes for TOTP recovery (AUTH-02).
|
||||
|
||||
code_hash stores the Argon2 hash of the original code (never plaintext).
|
||||
used_at is None when the code is unused; set to now() on first use.
|
||||
Verification iterates ALL codes to prevent timing-based enumeration (SEC-06).
|
||||
"""
|
||||
|
||||
__tablename__ = "backup_codes"
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
code_hash: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
used_at: Mapped[Optional[datetime]] = mapped_column(
|
||||
TIMESTAMP(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
__table_args__ = (Index("ix_backup_codes_user_id", "user_id"),)
|
||||
|
||||
|
||||
class Folder(Base):
|
||||
__tablename__ = "folders"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user