"""B23: исполнение политики паролей + реальная ретенция журналов. Revises: 009_e3_operations """ from __future__ import annotations import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql as pg revision = '010_b23_policy' down_revision = '009_e3_operations' branch_labels = None depends_on = None def upgrade() -> None: # Парольная политика: когда пароль был сменён в последний раз op.add_column( 'users', sa.Column('password_changed_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()'), nullable=True), ) # История паролей (проверка переиспользования, password_history_count). # id — INTEGER monotonic (обрезка «последние N» детерминирована; created_at # в SQLite точен до секунды, UUID-порядок случайный — см. модели). op.create_table( 'password_history', sa.Column('id', sa.Integer, primary_key=True, autoincrement=True), sa.Column('public_id', pg.UUID(as_uuid=True), server_default=sa.text('gen_random_uuid()')), sa.Column('user_id', pg.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False), sa.Column('hashed_password', sa.String(255), nullable=False), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('NOW()')), ) op.create_index('idx_password_history_user', 'password_history', ['user_id', 'created_at']) def downgrade() -> None: op.drop_index('idx_password_history_user', table_name='password_history') op.drop_table('password_history') op.drop_column('users', 'password_changed_at')