Files
ds-chat/backend/alembic/versions/6ee71c8d5e07_admin_portal.py
T
ksmithandClaude Sonnet 5 4aa8ef89c5 Phase 6: Admin portal
Adds is_site_admin-gated site administration: user management (list,
deactivate/reactivate, reset password, promote/demote), room management
(list all rooms including private ones, archive/unarchive, force-transfer
ownership), and an audit log of every admin action.

Backend: User.is_active (deactivation) and Room.is_archived (archive) are
new columns; AdminAuditLog is a new table matching ARCHITECTURE.md's
admin_audit_log design, written to in the same transaction as every
mutating admin action. require_site_admin (dependencies.py) gates all
/api/admin/* routes. get_current_user now rechecks is_active on every
request, so deactivating a user kills their already-open session
immediately, not just future logins. An admin can't deactivate or demote
their own account (the one self-lockout guard included). Archived rooms
drop out of the open-room browse list but stay readable for existing
members.

Frontend: new /admin route (AdminRoute guard, redirects non-admins to
/rooms) with a tabbed Users / Rooms / Audit log / Settings page, plus an
"Admin" link in the account menu for site admins.

Bot/integration management and system settings -- both listed in the
original issue -- are intentionally not here: bot management has nothing
to manage until Phase 7 builds the actual bot data model, and there's no
settings storage or concrete setting to configure yet. Settings has an
empty placeholder tab; bot management is deferred entirely to Phase 7.
Confirmed this scope cut with the repo owner before implementing.

New tests/test_admin.py (14 tests, full suite now 58/58) covers every
admin endpoint's permission gate, the self-action guards, deactivation's
immediate effect on an already-open session, and that every mutating
action produces exactly one audit log row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-14 07:37:29 -06:00

64 lines
2.6 KiB
Python

"""admin portal
Revision ID: 6ee71c8d5e07
Revises: 8a55c5254edb
Create Date: 2026-08-14 07:25:19.603206
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '6ee71c8d5e07'
down_revision: Union[str, Sequence[str], None] = '8a55c5254edb'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('admin_audit_log',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('actor_id', sa.Uuid(), nullable=False),
sa.Column('action', sa.String(length=100), nullable=False),
sa.Column('target_type', sa.String(length=50), nullable=False),
sa.Column('target_id', sa.Uuid(), nullable=False),
sa.Column('metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['actor_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_admin_audit_log_actor_id'), 'admin_audit_log', ['actor_id'], unique=False)
op.create_index(op.f('ix_admin_audit_log_created_at'), 'admin_audit_log', ['created_at'], unique=False)
# server_default backfills existing rows; dropped right after since the
# ORM already sends an explicit value on every insert (matches the
# client-side-default style used for is_private/is_bot in the initial
# migration, which didn't need a backfill because those tables were
# still empty at the time).
op.add_column(
'rooms',
sa.Column('is_archived', sa.Boolean(), nullable=False, server_default=sa.false()),
)
op.alter_column('rooms', 'is_archived', server_default=None)
op.add_column(
'users',
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.true()),
)
op.alter_column('users', 'is_active', server_default=None)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'is_active')
op.drop_column('rooms', 'is_archived')
op.drop_index(op.f('ix_admin_audit_log_created_at'), table_name='admin_audit_log')
op.drop_index(op.f('ix_admin_audit_log_actor_id'), table_name='admin_audit_log')
op.drop_table('admin_audit_log')
# ### end Alembic commands ###