All docs

Admin portal

Admin portal

Single-admin (hard-coded grasl.t@centrum.cz) cross-tenant ops surface at /admin/*. Pixel-perfect implementation of docs/design/final/project/admin*.{jsx,css}. Non-admin users get 404 (not 403) — the portal should look like it doesn't exist.

What the admin can / cannot do

See ADR 0010 — privacy invariant. TL;DR: metadata, counts, names, timestamps, audit. Never tasks.description, knowledge_entries.body, task_comments.body.

State machine

organizations.status:
   active ─┬──▶ suspended ──▶ active        (reversible, audit-tracked)
           └──▶ deleted ──▶ (30-day grace) ──▶ hard-delete (task 49)
                  │
                  └──▶ active (restored within grace)

users.status:
   active ◀─▶ suspended    (toggle; active sessions invalidated on suspend)

Migration: 0016_admin_portal.sql. Service: @drobek/core admin module. Gate: apps/web/app/lib/admin.server.ts.

Suspended semantics

  • `organizations.status='suspended'`:

- MCP auth (verifyAgentToken) returns null → tools fail with auth.invalid_token. - Web UI requireOrgRole blocks any non-GET request with 423 Locked. GET still works so members can see the suspension banner and export their data. - All members still log in normally; the org workspace renders a banner.

  • `users.status='suspended'`: all sessions revoked immediately (in the

same DB transaction as the status flip). Next sign-in is blocked by the session lookup returning none.

Routes

All /admin/* routes go through requireAdmin(request) which throws 404 on a non-admin session.

2FA

Out of scope. There is no 2FA in the admin portal — the old requiresTwoFactorEnrollment stub (and its always-on banner) has been removed. Admin access is gated by a single hard-coded verified email + session; the portal 404s for everyone else. Revisit only if there's a concrete need.

Email notifications

Suspend / resume / delete actions write the audit row; the welcome / status emails ship as part of task 45 (transactional emails). The action handlers in @drobek/core/admin are intentionally email-free — they record the intent in admin_audit and let the worker pattern from 45 fan out the notifications. Until then, the audit row is the source of truth.

Testing

  • Unit + service integration:

packages/core/src/admin/admin.integration.test.ts

  • E2E (gate + read paths):

apps/web/app/e2e/admin.e2e.test.ts