Private
Public Access
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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user