Private
Public Access
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>
64 lines
2.6 KiB
Python
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 ###
|