Files

92 lines
3.6 KiB
Python

"""
migrations/versions/phase7_mobile_api.py
-----------------------------------------
Phase 7 — Mobile API: JWT refresh tokens and APNs device tokens.
Revision ID : phase7_mobile_api
Revises : phase6_features
Create Date : 2026-03-17
New tables
----------
api_refresh_tokens
Stores server-side refresh token hashes for mobile sessions.
Enables instant revocation by deleting the row.
api_device_tokens
Stores APNs device tokens for push notification delivery.
One row per (user_id, device_id) — upserted on every app launch.
All columns include safe IF NOT EXISTS / IF EXISTS guards so the
migration is idempotent and safe to re-run.
"""
from alembic import op
import sqlalchemy as sa
revision = 'phase7_mobile_api'
down_revision = 'phase6_features'
branch_labels = None
depends_on = None
def upgrade():
bind = op.get_bind()
inspector = sa.inspect(bind)
tables = inspector.get_table_names()
# ── 1. api_refresh_tokens ─────────────────────────────────────────────
if 'api_refresh_tokens' not in tables:
op.create_table(
'api_refresh_tokens',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('user_id', sa.Integer(),
sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False),
sa.Column('token_hash', sa.String(64), nullable=False, unique=True),
sa.Column('device_id', sa.String(64), nullable=True),
sa.Column('device_name', sa.String(100), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('expires_at', sa.DateTime(), nullable=False),
sa.Column('revoked', sa.Boolean(), nullable=False,
server_default='0'),
)
op.create_index('ix_api_refresh_tokens_user_id',
'api_refresh_tokens', ['user_id'])
op.create_index('ix_api_refresh_tokens_token_hash',
'api_refresh_tokens', ['token_hash'], unique=True)
# ── 2. api_device_tokens ──────────────────────────────────────────────
if 'api_device_tokens' not in tables:
op.create_table(
'api_device_tokens',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('user_id', sa.Integer(),
sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False),
sa.Column('device_id', sa.String(64), nullable=False),
sa.Column('apns_token', sa.String(200), nullable=False),
sa.Column('device_name', sa.String(100), nullable=True),
sa.Column('app_version', sa.String(20), nullable=True),
sa.Column('registered_at', sa.DateTime(), nullable=False),
)
op.create_index('ix_api_device_tokens_user_id',
'api_device_tokens', ['user_id'])
op.create_unique_constraint(
'uq_device_token_user_device',
'api_device_tokens',
['user_id', 'device_id'],
)
def downgrade():
bind = op.get_bind()
inspector = sa.inspect(bind)
tables = inspector.get_table_names()
if 'api_device_tokens' in tables:
op.drop_table('api_device_tokens')
if 'api_refresh_tokens' in tables:
op.drop_table('api_refresh_tokens')