197 lines
9.1 KiB
Python
197 lines
9.1 KiB
Python
"""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')
|