"""Auth entities, audit logging, and enhanced schema Revision ID: 002_auth_and_audit Revises: 001_initial Create Date: 2025-12-10 """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql from sqlalchemy import text revision = '002_auth_and_audit' down_revision = '001_initial' branch_labels = None depends_on = None def upgrade() -> None: # Create enum for transaction status conn = op.get_bind() enum_types = [ ('transaction_status', ['draft', 'pending_approval', 'executed', 'reversed']), ('member_role', ['owner', 'adult', 'member', 'child', 'read_only']), ('event_action', ['create', 'update', 'delete', 'confirm', 'execute', 'reverse']), ] for enum_name, enum_values in enum_types: result = conn.execute( text(f"SELECT EXISTS(SELECT 1 FROM pg_type WHERE typname = '{enum_name}')") ) if not result.scalar(): values_str = ', '.join(f"'{v}'" for v in enum_values) conn.execute(text(f"CREATE TYPE {enum_name} AS ENUM ({values_str})")) # 1. Add session tracking to users (for JWT blacklisting) op.add_column('users', sa.Column('last_login_at', sa.DateTime(), nullable=True)) op.add_column('users', sa.Column('password_hash', sa.String(length=255), nullable=True)) # 2. Create sessions table (for refresh tokens) op.create_table( 'sessions', sa.Column('id', sa.String(length=36), nullable=False, primary_key=True), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('refresh_token_hash', sa.String(length=255), nullable=False), sa.Column('device_id', sa.String(length=255), nullable=True), sa.Column('ip_address', sa.String(length=45), nullable=True), sa.Column('user_agent', sa.String(length=500), nullable=True), sa.Column('expires_at', sa.DateTime(), nullable=False), sa.Column('revoked_at', sa.DateTime(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['users.id']), ) op.create_index('ix_sessions_user_id', 'sessions', ['user_id']) op.create_index('ix_sessions_expires_at', 'sessions', ['expires_at']) # 3. Create telegram_identities table op.create_table( 'telegram_identities', sa.Column('id', sa.Integer(), nullable=False, primary_key=True), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('chat_id', sa.BigInteger(), nullable=False, unique=True), sa.Column('username', sa.String(length=255), nullable=True), sa.Column('first_name', sa.String(length=255), nullable=True), sa.Column('last_name', sa.String(length=255), nullable=True), sa.Column('is_bot', sa.Boolean(), nullable=False, server_default='false'), sa.Column('verified_at', sa.DateTime(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['users.id']), ) op.create_index('ix_telegram_identities_chat_id', 'telegram_identities', ['chat_id'], unique=True) op.create_index('ix_telegram_identities_user_id', 'telegram_identities', ['user_id']) # 4. Enhance family_members with RBAC op.add_column('family_members', sa.Column('role', postgresql.ENUM('owner', 'adult', 'member', 'child', 'read_only', name='member_role', create_type=False), nullable=False, server_default='member')) op.add_column('family_members', sa.Column('permissions', postgresql.JSON(), nullable=False, server_default='{}')) op.add_column('family_members', sa.Column('status', sa.String(length=50), nullable=False, server_default='active')) # 5. Enhance transactions with status & approval workflow op.add_column('transactions', sa.Column('status', postgresql.ENUM('draft', 'pending_approval', 'executed', 'reversed', name='transaction_status', create_type=False), nullable=False, server_default='executed')) op.add_column('transactions', sa.Column('confirmation_required', sa.Boolean(), nullable=False, server_default='false')) op.add_column('transactions', sa.Column('confirmation_token', sa.String(length=255), nullable=True)) op.add_column('transactions', sa.Column('approved_by_id', sa.Integer(), nullable=True)) op.add_column('transactions', sa.Column('approved_at', sa.DateTime(), nullable=True)) op.add_column('transactions', sa.Column('reversed_at', sa.DateTime(), nullable=True)) op.add_column('transactions', sa.Column('reversal_reason', sa.String(length=500), nullable=True)) op.add_column('transactions', sa.Column('original_transaction_id', sa.Integer(), nullable=True)) op.add_column('transactions', sa.Column('executed_at', sa.DateTime(), nullable=True)) op.create_foreign_key( 'fk_transactions_approved_by', 'transactions', 'users', ['approved_by_id'], ['id'] ) op.create_foreign_key( 'fk_transactions_original', 'transactions', 'transactions', ['original_transaction_id'], ['id'] ) # 6. Create event_log table op.create_table( 'event_log', sa.Column('id', sa.BigInteger(), nullable=False, primary_key=True), sa.Column('family_id', sa.Integer(), nullable=False), sa.Column('entity_type', sa.String(length=50), nullable=False), sa.Column('entity_id', sa.Integer(), nullable=True), sa.Column('action', postgresql.ENUM(*['create', 'update', 'delete', 'confirm', 'execute', 'reverse'], name='event_action', create_type=False), nullable=False), sa.Column('actor_id', sa.Integer(), nullable=True), sa.Column('old_values', postgresql.JSON(), nullable=True), sa.Column('new_values', postgresql.JSON(), nullable=True), sa.Column('ip_address', sa.String(length=45), nullable=True), sa.Column('user_agent', sa.String(length=500), nullable=True), sa.Column('reason', sa.String(length=500), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['family_id'], ['families.id']), sa.ForeignKeyConstraint(['actor_id'], ['users.id']), ) op.create_index('ix_event_log_family_id', 'event_log', ['family_id']) op.create_index('ix_event_log_entity', 'event_log', ['entity_type', 'entity_id']) op.create_index('ix_event_log_created_at', 'event_log', ['created_at']) op.create_index('ix_event_log_action', 'event_log', ['action']) # 7. Create access_log table op.create_table( 'access_log', sa.Column('id', sa.BigInteger(), nullable=False, primary_key=True), sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('endpoint', sa.String(length=255), nullable=False), sa.Column('method', sa.String(length=10), nullable=False), sa.Column('status_code', sa.Integer(), nullable=False), sa.Column('response_time_ms', sa.Integer(), nullable=True), sa.Column('ip_address', sa.String(length=45), nullable=False), sa.Column('user_agent', sa.String(length=500), nullable=True), sa.Column('error_message', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['users.id']), ) op.create_index('ix_access_log_user_id', 'access_log', ['user_id']) op.create_index('ix_access_log_endpoint', 'access_log', ['endpoint']) op.create_index('ix_access_log_created_at', 'access_log', ['created_at']) # 8. Enhance wallets with balance history op.add_column('accounts', sa.Column('balance_snapshot', sa.Numeric(precision=19, scale=2), nullable=False, server_default='0')) op.add_column('accounts', sa.Column('snapshot_at', sa.DateTime(), nullable=True)) def downgrade() -> None: op.drop_table('access_log') op.drop_table('event_log') op.drop_table('telegram_identities') op.drop_table('sessions') op.drop_constraint('fk_transactions_original', 'transactions', type_='foreignkey') op.drop_constraint('fk_transactions_approved_by', 'transactions', type_='foreignkey') op.drop_column('transactions', 'executed_at') op.drop_column('transactions', 'original_transaction_id') op.drop_column('transactions', 'reversal_reason') op.drop_column('transactions', 'reversed_at') op.drop_column('transactions', 'approved_at') op.drop_column('transactions', 'approved_by_id') op.drop_column('transactions', 'confirmation_token') op.drop_column('transactions', 'confirmation_required') op.drop_column('transactions', 'status') op.drop_column('family_members', 'status') op.drop_column('family_members', 'permissions') op.drop_column('family_members', 'role') op.drop_column('accounts', 'snapshot_at') op.drop_column('accounts', 'balance_snapshot') op.drop_column('users', 'password_hash') op.drop_column('users', 'last_login_at')