March 17 2026: Mobile app phase 1
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
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')
|
||||
Reference in New Issue
Block a user