Hackorda Docs
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 users rows 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

#ActorUI surfaceActionAPI callDB writesSide effectsSuggested event
1Admin/app/admin/all-usersOpen role dropdown on target row
2AdmindropdownPick "Admin" / "Tester" / "Student" / "Guest"PATCH /api/admin/users/:userId/role body { roleId: 1|4|2|3 }UPDATE users SET role_id = :n WHERE id = :userIduser.role_changed
3FrontendTanStack invalidates admin-all-users + admin-userstoast: "Role updated"

Happy path — promote via profile page

#ActorUI surfaceActionAPI callDB writesSide effectsSuggested event
1Admin/app/admin/users/:idOpen "Actions" panel
2AdminActions panelClick "Set as Admin" / "Set as Tester"PATCH /api/admin/users/:userId/roleUPDATE users SET role_id = :n …user.role_changed
3Frontendprofile pagere-render with new badgetoast: "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_id flips to 1 and 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

#ScenarioExpected behavior
N-1Self-demotion attemptDropdown disables non-admin options OR API returns 400
N-2Invalid roleId (e.g. 99)API returns 400 "Invalid role"
N-3userId does not existAPI returns 404
N-4Non-admin callerAPI returns 403
N-5Concurrent 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, ROLES enum ({ 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.loguser.role_changed event would be the audit record once telemetry is wired.

On this page