fix(staff): add revoke button to super user rows + serialize guardrail in transaction

Frontend:
- Super users now see a "Revoke" button (disabled when last super user)
  alongside the ★ badge on super-user rows in the Staff table.
  Non-super-user rows show the existing "+ Grant" button.

Backend (race condition fix):
- PATCH /api/staff/:id (isSuperUser=false or active=false): count check +
  update now wrapped in a db.transaction() with FOR UPDATE lock on the
  target row, preventing a race where two concurrent revokes could both
  pass the guard and leave zero super users.
- DELETE /api/staff/🆔 same transaction + FOR UPDATE guard applied.

GRO-206 CTO review feedback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
groombook-ci[bot]
2026-03-29 12:11:53 +00:00
parent e3c5ebb337
commit c76a37b15c
2 changed files with 90 additions and 56 deletions
+15 -3
View File
@@ -143,9 +143,21 @@ export function StaffPage() {
<td style={tdStyle}><span style={{ textTransform: "capitalize" }}>{s.role}</span></td>
<td style={tdStyle}>
{s.isSuperUser ? (
<span style={{ display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", borderRadius: 12, fontSize: 11, fontWeight: 600, background: "#ede9fe", color: "#5b21b6" }}>
Super User
</span>
<>
<span style={{ display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", borderRadius: 12, fontSize: 11, fontWeight: 600, background: "#ede9fe", color: "#5b21b6" }}>
Super User
</span>
{isCurrentUserSuperUser && s.id !== me?.id && (
<button
onClick={() => toggleSuperUser(s)}
disabled={togglingSuperUser === s.id || activeSuperUserCount <= 1}
title={activeSuperUserCount <= 1 ? "Cannot revoke: last super user" : undefined}
style={{ ...btnStyle, padding: "0.2rem 0.6rem", fontSize: 11, background: "#f3f4f6", color: "#374151", borderColor: "#d1d5db", marginLeft: 4 }}
>
{togglingSuperUser === s.id ? "…" : "Revoke"}
</button>
)}
</>
) : isCurrentUserSuperUser ? (
<button
onClick={() => toggleSuperUser(s)}