Flows
F 02 Role Management
Status: 🟢 ready to test Owner: Adilet Last updated: 2026-05-08 Last verified on production: —
Goal
An admin can change another user's system role (Admin | Tester |
Student | Guest) inline from the All Users table or from the user's
profile page. The change is immediate; the affected user sees new menus
on next page load.
Actors
- Primary — Admin (
users.role_id = 1) - Secondary — The target user (sees new sidebar items / pages)
Preconditions
- Two
usersrows exist: an admin and a target. - Admin is signed in. Target may or may not be signed in.
Trigger
Admin opens /app/admin/all-users, finds the target row, and clicks the
role badge (which is now a dropdown — see InlineRoleSelect).
Happy path — promote via inline dropdown
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | /app/admin/all-users | Open role dropdown on target row | — | — | — | — |
| 2 | Admin | dropdown | Pick "Admin" / "Tester" / "Student" / "Guest" | PATCH /api/admin/users/:userId/role body { roleId: 1|4|2|3 } | UPDATE users SET role_id = :n WHERE id = :userId | — | user.role_changed |
| 3 | Frontend | — | TanStack invalidates admin-all-users + admin-users | — | — | toast: "Role updated" | — |
Happy path — promote via profile page
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | /app/admin/users/:id | Open "Actions" panel | — | — | — | — |
| 2 | Admin | Actions panel | Click "Set as Admin" / "Set as Tester" | PATCH /api/admin/users/:userId/role | UPDATE users SET role_id = :n … | — | user.role_changed |
| 3 | Frontend | profile page | re-render with new badge | — | — | toast: "Role updated" | — |
Acceptance criteria
- AC-1 — Given the admin is signed in and viewing All Users, when
they pick "Admin" from the dropdown on a Student row, then
users.role_idflips to1and the badge updates without a page reload. - AC-2 — Given the admin tries to demote themselves to a non-admin
role, then the dropdown either disables the non-admin options or the
PATCH responds 400 with a "cannot self-demote" error (UI guard via
useRoleManagement). - AC-3 — Given a non-admin user calls
PATCH /api/admin/users/:id/role, then the API responds 403. - AC-4 — Given the target user becomes Admin, when they navigate to
/app/admin, then they see the admin dashboard (no redirect). - AC-5 — Given the target user becomes Tester (
role_id = 4), when they navigate to/app/test-cycles, then they see the QA section in the sidebar and any cycles they're a member of.
Test data / fixtures
Baseline seed + at least one extra non-admin user to act as the target.
Negative paths
| # | Scenario | Expected behavior |
|---|---|---|
| N-1 | Self-demotion attempt | Dropdown disables non-admin options OR API returns 400 |
| N-2 | Invalid roleId (e.g. 99) | API returns 400 "Invalid role" |
| N-3 | userId does not exist | API returns 404 |
| N-4 | Non-admin caller | API returns 403 |
| N-5 | Concurrent edit (admin A and admin B both PATCH same target) | Last write wins; no error |
Manual QA checklist
- Open
/app/admin/all-users, find a Student row - Click the Student badge → dropdown opens with 4 options
- Pick "Tester" → toast confirms, badge becomes "Tester"
- Refresh page → badge still "Tester" (persisted)
- Pick "Admin" on the same user → badge becomes "Admin"
- Sign in as that user (or impersonate via F-03) → confirm admin sidebar shows
- Try to demote yourself → action blocked with friendly message
- Demote target back to "Student" → admin items disappear on their next page load
Automated test outline
test.describe("F-02 role management", () => {
test("inline dropdown promotes student to admin", async ({ page }) => { … });
test("self-demotion guard", async ({ page }) => { … });
test("non-admin cannot call patch", async ({ request }) => { … });
});Code references
- UI:
src/components/admin/InlineRoleSelect.tsx - Pages:
src/app/app/admin/all-users/page.tsx+columns.tsx,src/app/app/admin/users/[id]/page.tsx - API:
src/app/api/admin/users/[userId]/role/route.ts - Hooks:
src/hooks/admin/useRoleManagement.ts - Schema:
users.role_id,ROLESenum ({ ADMIN: 1, STUDENT: 2, GUEST: 3, QA: 4 })
Events emitted (proposed)
user.role_changed— fired on successful PATCH. Payload:{ target_user_id, old_role_id, new_role_id, changed_by_user_id }.
Open questions / known gaps
- Bulk role change not supported (one user at a time).
- No audit trail beyond
console.log—user.role_changedevent would be the audit record once telemetry is wired.