← All tests

Team role escalation — reported, reproduced, fixed STAGING

Narong's "Random Bugs" #1, filed 2026-07-22. nix-cafe only. No migration, no backend change.

His report:

"Team > Members: Remove a user removes all the roles > when set from Active to Inactive or vice versa > Escalates the User to Owner Privileges."

What was actually happening

Not the Active/Inactive toggle — that branch only writes is_active. The escalation rode along on the same Save:

  1. Remove deletes the member's nix_user_product_roles row → roleId is now null.
  2. listTeamMembers left-joins the role, so the member stays in the list with an Edit button.
  3. The Edit dialog initialised with useState(member.roleId || roles[0]?.id || "") — so it silently pre-selected an arbitrary role.
  4. handleSave always sent roleId, so a save intended only to flip the status wrote that role.
  5. listCafeRoles had no ORDER BYroles[0] was raw Postgres heap order: unstable, and on lumiere-coffee it was literally Owner.

The server-side guards were never at fault — they held throughout. A Manager attempting this still gets a 403. What this actually was: an Owner silently granting a role nobody chose, with no confirmation, and wrong in both directions (on get-coffee the same flow silently assigned POS User instead).

The fix

ChangeWhy
useState(member.roleId || "")no silent fallback — the dangerous line
Disabled "Select a role…" placeholder + hintthe operator is told the member has no role
handleSave omits roleId when emptya status-only save can't write a role
listCafeRoles ORDER BY privilege ascendingdeterministic; any roles[0] fallback now fails safe
role="switch" + aria-checked + testid on the Active toggleit was an unlabelled div — untestable and inaccessible
edit dialog with no pre-selected role
Role-less member on get-coffee.staging — the role field is empty with the placeholder, and the hint explains why. Previously this silently showed a real role.
explicit role pick
Explicitly picking a role still assigns it — the fix removes the silent default, not the feature.

Proof the test isn't vacuous

The local suite was re-run with the old default temporarily restored. It failed exactly where it should — so the test genuinely reproduces the reported bug before asserting the fix:

✗ opening Edit does NOT silently pre-select a role: pre-filled with "9e600b16-…"
✗ toggling Active + Save does NOT assign any role: status-only save assigned role "pos_user"

It wrote pos_user rather than Owner only because the ORDER BY fix was already in place — which shows the two changes are complementary: the ordering caps the blast radius, the dialog fix stops the silent write.

Checks

SuiteResult
Local — remove role → Edit → toggle Active → Save, asserted against the DB (+ negative control)8/8
Staging — same flow over real SSO on get-coffee.staging, asserted against Neon6/6
Staging smoke (SSO, cross-tenant guard, Starter login, no-session redirect)5/5
tsc --noEmitclean

Both suites assert on database state, not screenshots — the property that matters is "no role row was written". Each restores its subject exactly on the way out.

Also in this push

Deleted app/(authed)/orders/management/order-management-client.tsx — orphaned dead code (392 lines). Its route is now a redirect to /orders, so nothing rendered it. It was a stale duplicate of the live Order Management view and had already caused one bug to need fixing in two places.

nix-cafe staging 9b32ca1 (Worker e97448a8). No migration. Parked for the scheduled prod promote. Note: on staging the account Narong had escalated has since been reset to cashier and deactivated — no lingering Owner privilege.