35f46926a9
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
55 lines
2.1 KiB
Python
55 lines
2.1 KiB
Python
"""API keys as SHA-256 hashes, one per session (#41)
|
|
|
|
Replaces the plaintext `users.api_key` column with an `api_keys` table: one
|
|
row per login, holding only the SHA-256 hex digest, an expiry and the last
|
|
use. Existing keys are invalidated on purpose, not migrated - the plaintext
|
|
is exactly what must stop existing, so everybody logs in once again.
|
|
|
|
Revision ID: 015_api_key_hash
|
|
Revises: 014_vision_fk_ondelete
|
|
Create Date: 2026-10-06
|
|
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
revision: str = '015_api_key_hash'
|
|
down_revision: Union[str, None] = '014_vision_fk_ondelete'
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
'api_keys',
|
|
sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True),
|
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
|
sa.Column('key_hash', sa.String(64), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
sa.Column('expires_at', sa.DateTime(), nullable=False),
|
|
sa.Column('last_used_at', sa.DateTime(), nullable=True),
|
|
sa.ForeignKeyConstraint(
|
|
['user_id'], ['users.id'], name='fk_api_keys_user_id', ondelete='CASCADE'
|
|
),
|
|
)
|
|
op.create_index('ix_api_keys_key_hash', 'api_keys', ['key_hash'], unique=True)
|
|
op.create_index('ix_api_keys_user_id', 'api_keys', ['user_id'])
|
|
|
|
with op.batch_alter_table('users') as batch:
|
|
batch.drop_index('ix_users_api_key')
|
|
batch.drop_constraint('uq_users_api_key', type_='unique')
|
|
batch.drop_column('api_key')
|
|
|
|
|
|
def downgrade() -> None:
|
|
with op.batch_alter_table('users') as batch:
|
|
batch.add_column(sa.Column('api_key', sa.String(64), nullable=True))
|
|
batch.create_unique_constraint('uq_users_api_key', ['api_key'])
|
|
batch.create_index('ix_users_api_key', ['api_key'])
|
|
|
|
op.drop_index('ix_api_keys_user_id', table_name='api_keys')
|
|
op.drop_index('ix_api_keys_key_hash', table_name='api_keys')
|
|
op.drop_table('api_keys')
|