Files
descrybe/docs/admin-roles-support/10-admin-orgs-ui.md
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

3.5 KiB

10 — Admin users & organizations UI (agent 10/20)

Owns: /admin/users — users directory, company list with plan view/assign, platform staff role assignment.
Does not own: Billing plans table (agent 9), Permissions panel (agent 8), staff middleware/model (agent 6), admin shell chrome (agent 7 — nav label only).
Gate: requirePlatformAdmin (full admin / developer). support_staff is excluded.


PROBLEM

/admin/users was a thin client-filtered user list (no companies, no plan view, no staff role edits, no server pagination). Operators needed one place to manage orgs and staff after cutover.


Delivered

  1. Users tab — server-paginated (limit/offset, page size 25), search (q), staff-only filter, set-password invite + local dev tools preserved.
  2. Staff roles — dialog → PATCH /api/admin/users/{id}/staff-role with admin | developer | support_staff | clear. Uses agent 6 API; self-change blocked server-side.
  3. Companies tab — paginated list with active plan name, legacy/public/custom badge, credits remaining/total, “without plan” filter.
  4. Assign plan — dialog lists all admin plans (ladder + legacy + custom) → POST /api/admin/plans/assign.
  5. API enrichmentsGET /api/admin/users returns total, staff_role, resolved_role, search/filters; GET /api/admin/companies returns total, plan_id / plan_name / plan_is_*, search.

Source files

File Intent
apps/web/src/routes/admin/users/+page.svelte Orgs UI (users + companies tabs)
apps/web/src/lib/admin-orgs.ts Client helpers + types
apps/api/internal/httpapi/admin_orgs_handlers.go Paginated list handlers
apps/api/internal/httpapi/admin_staff_handlers.go Staff role PATCH (agent 6)
apps/web/src/lib/components/AdminNav.svelte Nav label “Users & orgs”

API contract

Method Path Use
GET /api/admin/users?limit&offset&q&staff_only&active_only&inactive_only User directory
PATCH /api/admin/users/{id}/staff-role Body { "staff_role": "admin"|"developer"|"support_staff"|null }
GET /api/admin/companies?limit&offset&q&without_active_plan Companies + plan summary
GET /api/admin/plans Plan picker
POST /api/admin/plans/assign { company_id, plan_id }
POST /api/admin/emails/set-password Existing invite tooling

All under RequireSession + RequirePlatformAdmin (full admin only).


Security / performance

  • AuthZ enforced server-side; UI gate is UX only.
  • Cannot change own staff role (agent 6 handler).
  • Lists use SQL LIMIT/OFFSET + COUNT(*) — no full-table client filter.
  • Company plan join is a single query (no N+1).

ASSUMPTION: users.staff_role migration (029_staff_roles.sql) applied. UI shows an info banner if PATCH returns 404/501.


Manual verify

  1. Platform admin → /admin/users.
  2. Search users; toggle Staff only; page Next/Previous.
  3. Assign staff role (admin / developer / support_staff); confirm badge.
  4. Companies tab → see plan badge; filter Without plan; Assign plan (legacy + public).
  5. As support_staff, confirm /admin/users is forbidden (full-admin-only nav).

Coordination

  • Agent 6: staff model + PATCH …/staff-role + RequirePlatformAdmin excluding support_staff.
  • Agent 9: billing assign UX remains; orgs page is the directory-focused assign path.
  • Agent 14: support_staff queue access after role grant from this UI.