- models/totp_usage.py: replay-prevention table, unique on (user_id, code, window) - models/backup_code.py: Argon2id-hashed recovery codes with used_at tracking - services/totp.py: Fernet encrypt/decrypt, verify_totp_code returns actual window, QR base64, backup code generation - routers/totp.py: setup (idempotent), confirm, totp-verify (mfa_token + TOTP or backup code), disable, regenerate, status - alembic/024: creates totp_usage and backup_codes tables - main.py: register totp router, import new models for Alembic discovery - requirements.txt: add pyotp>=2.9.0, qrcode[pil]>=7.4.0, cryptography>=42.0.0 - jobs/notifications.py: periodic cleanup for totp_usage (5 min) and expired user_sessions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
20 lines
798 B
Python
20 lines
798 B
Python
from sqlalchemy import String, ForeignKey, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from app.database import Base
|
|
|
|
|
|
class BackupCode(Base):
|
|
__tablename__ = "backup_codes"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True
|
|
)
|
|
# Argon2id hash of the plaintext recovery code — never store plaintext
|
|
code_hash: Mapped[str] = mapped_column(String(255), nullable=False)
|
|
# Null until redeemed; set to datetime.now() on successful use
|
|
used_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, default=None)
|
|
created_at: Mapped[datetime] = mapped_column(default=func.now())
|