Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
3.5 KiB
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
- Users tab — server-paginated (
limit/offset, page size 25), search (q), staff-only filter, set-password invite + local dev tools preserved. - Staff roles — dialog →
PATCH /api/admin/users/{id}/staff-rolewithadmin|developer|support_staff| clear. Uses agent 6 API; self-change blocked server-side. - Companies tab — paginated list with active plan name, legacy/public/custom badge, credits remaining/total, “without plan” filter.
- Assign plan — dialog lists all admin plans (ladder + legacy + custom) →
POST /api/admin/plans/assign. - API enrichments —
GET /api/admin/usersreturnstotal,staff_role,resolved_role, search/filters;GET /api/admin/companiesreturnstotal,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
- Platform admin →
/admin/users. - Search users; toggle Staff only; page Next/Previous.
- Assign staff role (admin / developer / support_staff); confirm badge.
- Companies tab → see plan badge; filter Without plan; Assign plan (legacy + public).
- As
support_staff, confirm/admin/usersis forbidden (full-admin-only nav).
Coordination
- Agent 6: staff model +
PATCH …/staff-role+RequirePlatformAdminexcluding 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.