"""Passwortrichtlinie und Zwei-Faktor-Authentisierung. Bringt die globale Einstellungstabelle (``app_settings``), die TOTP-Felder am Benutzer, die Wiederherstellungscodes und den Zwischenzustand „Passwort ok, zweiter Faktor fehlt noch" an der Sitzung. Revision ID: 0008 Revises: 0007 Create Date: 2026-08-20 """ from alembic import op import sqlalchemy as sa revision = "0008" down_revision = "0007" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "app_settings", sa.Column("key", sa.String(length=64), primary_key=True), sa.Column("value", sa.String(length=255), nullable=True), ) op.create_table( "recovery_codes", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("user_id", sa.Integer(), nullable=False), sa.Column("code_hash", sa.String(length=64), nullable=False), sa.Column("used_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=True), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), ) op.create_index("ix_recovery_codes_user_id", "recovery_codes", ["user_id"]) op.create_index("ix_recovery_codes_code_hash", "recovery_codes", ["code_hash"]) with op.batch_alter_table("users") as batch: batch.add_column(sa.Column("must_change_password_reason", sa.String(length=16), nullable=True)) batch.add_column(sa.Column("totp_secret", sa.String(length=64), nullable=True)) batch.add_column( sa.Column("totp_enabled", sa.Boolean(), nullable=False, server_default=sa.text("0")) ) batch.add_column(sa.Column("totp_confirmed_at", sa.DateTime(timezone=True), nullable=True)) batch.add_column(sa.Column("totp_last_step", sa.Integer(), nullable=True)) with op.batch_alter_table("sessions") as batch: batch.add_column( sa.Column("pending_2fa", sa.Boolean(), nullable=False, server_default=sa.text("0")) ) batch.add_column( sa.Column("failed_2fa", sa.Integer(), nullable=False, server_default=sa.text("0")) ) # Bestehende Nutzer, die bereits ein Temporär-Passwort haben, bekommen den # passenden Grund nachgetragen – sonst stünde dort der Richtlinien-Hinweis. op.execute( "UPDATE users SET must_change_password_reason = 'temp' WHERE must_change_password = 1" ) def downgrade() -> None: with op.batch_alter_table("sessions") as batch: batch.drop_column("failed_2fa") batch.drop_column("pending_2fa") with op.batch_alter_table("users") as batch: batch.drop_column("totp_last_step") batch.drop_column("totp_confirmed_at") batch.drop_column("totp_enabled") batch.drop_column("totp_secret") batch.drop_column("must_change_password_reason") op.drop_index("ix_recovery_codes_code_hash", table_name="recovery_codes") op.drop_index("ix_recovery_codes_user_id", table_name="recovery_codes") op.drop_table("recovery_codes") op.drop_table("app_settings")