"""Add token_hash columns to oauth_grants and oauth_codes Revision ID: acct_0002 Revises: acct_0001 Create Date: 2026-02-26 """ import hashlib import sqlalchemy as sa from alembic import op revision = "acct_0002" down_revision = "acct_0001" branch_labels = None depends_on = None def _hash(token: str) -> str: return hashlib.sha256(token.encode()).hexdigest() def upgrade(): # Add new hash columns op.add_column("oauth_grants", sa.Column("token_hash", sa.String(64), nullable=True)) op.add_column("oauth_codes", sa.Column("code_hash", sa.String(64), nullable=True)) op.add_column("oauth_codes", sa.Column("grant_token_hash", sa.String(64), nullable=True)) # Backfill hashes from existing plaintext tokens conn = op.get_bind() grants = conn.execute(sa.text("SELECT id, token FROM oauth_grants WHERE token IS NOT NULL")) for row in grants: conn.execute( sa.text("UPDATE oauth_grants SET token_hash = :h WHERE id = :id"), {"h": _hash(row.token), "id": row.id}, ) codes = conn.execute(sa.text("SELECT id, code, grant_token FROM oauth_codes WHERE code IS NOT NULL")) for row in codes: params = {"id": row.id, "ch": _hash(row.code)} params["gh"] = _hash(row.grant_token) if row.grant_token else None conn.execute( sa.text("UPDATE oauth_codes SET code_hash = :ch, grant_token_hash = :gh WHERE id = :id"), params, ) # Create unique indexes on hash columns op.create_index("ix_oauth_grant_token_hash", "oauth_grants", ["token_hash"], unique=True) op.create_index("ix_oauth_code_code_hash", "oauth_codes", ["code_hash"], unique=True) # Make original token columns nullable (keep for rollback safety) op.alter_column("oauth_grants", "token", nullable=True) op.alter_column("oauth_codes", "code", nullable=True) # Drop old unique indexes on plaintext columns try: op.drop_index("ix_oauth_grant_token", "oauth_grants") except Exception: pass try: op.drop_index("ix_oauth_code_code", "oauth_codes") except Exception: pass def downgrade(): # Restore original NOT NULL constraints op.alter_column("oauth_grants", "token", nullable=False) op.alter_column("oauth_codes", "code", nullable=False) # Drop hash columns and indexes try: op.drop_index("ix_oauth_grant_token_hash", "oauth_grants") except Exception: pass try: op.drop_index("ix_oauth_code_code_hash", "oauth_codes") except Exception: pass op.drop_column("oauth_grants", "token_hash") op.drop_column("oauth_codes", "code_hash") op.drop_column("oauth_codes", "grant_token_hash") # Restore original unique indexes op.create_index("ix_oauth_grant_token", "oauth_grants", ["token"], unique=True) op.create_index("ix_oauth_code_code", "oauth_codes", ["code"], unique=True)