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>
This commit is contained in:
2026-08-14 07:37:29 -06:00
co-authored by Claude Sonnet 5
parent 0b995ef75f
commit 4aa8ef89c5
22 changed files with 1394 additions and 10 deletions
+143
View File
@@ -0,0 +1,143 @@
.admin-page {
height: 100%;
display: flex;
flex-direction: column;
background: var(--ds-void);
}
.admin-body {
flex: 1;
overflow-y: auto;
padding: var(--sp-6) var(--sp-8);
max-width: 960px;
width: 100%;
margin: 0 auto;
}
.admin-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--sp-6);
}
.admin-header h1 {
font-size: 1.3rem;
margin: 0;
}
.admin-tabs {
display: flex;
gap: var(--sp-2);
border-bottom: 1px solid var(--ds-border);
margin-bottom: var(--sp-4);
}
.admin-tab {
background: transparent;
border: none;
color: var(--ds-muted);
font-size: 0.86rem;
font-weight: 700;
padding: 10px 4px;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.admin-tab:hover {
color: var(--ds-text);
}
.admin-tab.active {
color: var(--ds-accent);
border-bottom-color: var(--ds-accent);
}
.admin-error {
color: var(--ds-danger);
font-size: 0.84rem;
margin: 0 0 var(--sp-4);
}
.admin-placeholder {
color: var(--ds-muted);
font-size: 0.9rem;
}
.admin-table {
width: 100%;
border-collapse: collapse;
font-size: 0.84rem;
}
.admin-table th {
text-align: left;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--ds-muted);
padding: 8px 10px;
border-bottom: 1px solid var(--ds-border);
}
.admin-table td {
padding: 10px;
border-bottom: 1px solid var(--ds-border);
vertical-align: middle;
}
.admin-table tbody tr:hover {
background: var(--ds-surface);
}
.status-badge {
display: inline-flex;
align-items: center;
border-radius: var(--radius-pill);
font-size: 0.68rem;
font-weight: 800;
padding: 2px 8px;
}
.status-badge.active {
border: 1px solid color-mix(in srgb, var(--ds-accent) 50%, transparent);
background: color-mix(in srgb, var(--ds-accent) 14%, transparent);
color: var(--ds-accent);
}
.status-badge.inactive {
border: 1px solid color-mix(in srgb, var(--ds-danger) 50%, transparent);
background: color-mix(in srgb, var(--ds-danger) 14%, transparent);
color: var(--ds-danger);
}
.admin-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.admin-actions button {
background: transparent;
border: 1px solid var(--ds-border);
color: var(--ds-muted);
font-size: 0.72rem;
padding: 4px 8px;
border-radius: 6px;
cursor: pointer;
}
.admin-actions button:hover:not(:disabled) {
color: var(--ds-text);
border-color: var(--ds-accent);
}
.admin-actions button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.admin-load-more {
margin-top: var(--sp-4);
}