DealDocumentScreening/migrations/versions/0010_passkeys_magic_links.py

65 lines
2.4 KiB
Python

"""passkey_credentials table + magic-link columns on users
Revision ID: 0010
Revises: 0009
Create Date: 2026-08-22
WebAuthn (passkey) auth: one row per registered credential; `credential_id`
and `public_key` are base64url strings. Magic-link login reuses the
password-reset pattern on users: only a SHA-256 token hash + expiry is
stored, the raw token travels inside the emailed link.
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "0010"
down_revision: str | None = "0009"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"passkey_credentials",
sa.Column("id", sa.UUID(), server_default=sa.text("gen_random_uuid()"), nullable=False),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.Column("credential_id", sa.Text(), nullable=False),
sa.Column("public_key", sa.Text(), nullable=False),
sa.Column("sign_count", sa.BigInteger(), server_default=sa.text("0"), nullable=False),
sa.Column("device_name", sa.Text(), nullable=True),
sa.Column(
"created_at",
sa.DateTime(timezone=True),
server_default=sa.func.now(),
nullable=False,
),
sa.Column("last_used_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.CheckConstraint("sign_count >= 0", name="passkey_credentials_sign_count_nonneg"),
)
op.create_unique_constraint(
"passkey_credentials_credential_id_unique", "passkey_credentials", ["credential_id"]
)
op.create_index("passkey_credentials_user_idx", "passkey_credentials", ["user_id"])
op.add_column("users", sa.Column("magic_link_token_hash", sa.Text(), nullable=True))
op.add_column(
"users", sa.Column("magic_link_expires_at", sa.DateTime(timezone=True), nullable=True)
)
def downgrade() -> None:
op.drop_column("users", "magic_link_expires_at")
op.drop_column("users", "magic_link_token_hash")
op.drop_index("passkey_credentials_user_idx", table_name="passkey_credentials")
op.drop_constraint(
"passkey_credentials_credential_id_unique", "passkey_credentials", type_="unique"
)
op.drop_table("passkey_credentials")