65 lines
2.4 KiB
Python
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")
|