Skip to content

Role requests API

Handler: handleRoleRequests() in src/lib/roleRequests.ts, loaded on demand by apiFetch (like the leave routes), so the API chunk every screen downloads does not carry it. The file's header lists every route with its function and permission.

A staff role changes only when it is requested, a CEO approves it and an admin applies it (ACC-077 … ACC-080, ACC-082). An administrative role — one whose permissions control users, access, settings or role changes — is asked for, added or removed, only by a holder of roles.apply (IT Admin, Super Admin; ACC-083), enforced by the trigger RoleChangeRequest_admin_roles_by_admins. Every route is one SECURITY DEFINER database function. The function takes the caller from the session, checks the permission and the separation of duties (ACC-078), refuses a paused login and writes the audit row. The requirePermission() calls in the handler only shape the screen (ACC-001). Permissions: PERMISSIONS.md §4.7, §6.22. A database refusal comes back as its sentence, for the screen to show as it is (403 for a permission or separation-of-duties refusal, 409 for a duplicate, 400 for a missing reason).

Routes

Method Path Body / query Function Permission
GET /role-requests ?scope=ceo\|admin\|mine\|history\|open\|all&userId= role_change_requests any staff login; the people in a request read it, and roles.request / roles.approve / roles.apply read every request (checked in the function and by the row policy)
GET /role-requests/roles — role_change_roles any signed-in login; the staff roles a request can name (never CUSTOMER, AGENT, TOUR_LEADER), as [{ name, administrative, requestable }] for the caller — requestable is false for an administrative role unless the caller holds roles.apply (ACC-083)
POST /role-requests { userId, action: 'add'\|'remove', role, reason } role_change_request_create roles.request; to add or remove an administrative role, roles.apply as well — otherwise 403 "Only IT Admin or Super Admin can ask for the CEO role." (ACC-083)
POST /role-requests/:id/ceo-decision { decision: 'approve'\|'reject', remarks } role_change_ceo_decide roles.approve; never the requester or the person; a rejection needs remarks
POST /role-requests/:id/admin-decision { decision: 'accept'\|'reject', remarks } role_change_admin_decide roles.apply; never the requester, the person or the approver; a rejection needs remarks
POST /role-requests/:id/withdraw { reason } role_change_withdraw roles.request, and only the requester

GET /role-requests returns { rows, canRequest, canApprove, canApply, counts: { ceo, admin } }. Each row carries the person, the change (action, roleName), the reason, the status (pending_ceo, approved_by_ceo, applied, rejected_by_ceo, rejected_by_admin, withdrawn), who decided each step and when, and three flags the database works out for the caller — canApprove, canApply, canWithdraw. The screens show a button only when its flag is true. The write routes return the same row; a second click returns the first answer with existing: true (filing) or alreadyDecided: true (a decision).

On approve every other active holder of roles.apply gets a work-inbox job (type role_change, subject RoleChangeRequest) that opens /admin/role-requests?tab=admin&request=<id>; the jobs close when an admin decides or the requester withdraws. accept writes the UserRole row in the same transaction.

admin-users (edge function)

Action What it does now
create_user { …, roles?, reason? } Makes the login with no staff role, then files one role request per role in roles, as the caller, with reason (role_change_request_create_as, service role only). Needs roles.request and a reason when roles is given; CUSTOMER, AGENT and TOUR_LEADER are refused in roles. An administrative role in roles (role_is_administrative()) needs roles.apply; without it the call answers 403 "Only IT Admin or Super Admin can ask for the … role." and no login is made (ACC-083). Returns { user, tempPassword?, roleRequests, roleRequestErrors? } (ACC-082).
assign_role / remove_role with a staff role Files a role request (reason required, roles.request) and answers 202 { ok, requested: true, request }. It never writes the role (ACC-079).
assign_role / remove_role with CUSTOMER or AGENT Unchanged: the portal login's role is set (ACC-080). TOUR_LEADER is refused — a tour leader is appointed on the departure.
delete_user Soft-deletes the login first, then clears its role rows (the lock lets a deleted login's rows go).

Retired

Route Now
POST /users/:id/roles Retired: answers 410 — request the change instead (ACC-079).
PUT /permissions/users/:id with roleIds Answers 410; the route changes per-user overrides only.