Problem
The frontend cannot tell whether the current user is an admin, so it offers admin-only actions to everyone and the refusal arrives only after the user has done the work.
Most visible in the #1247 merge-gate override: a non-superuser sees the blocked merge, opens the override dialog, writes a justification, clicks Override and Merge — and gets a 403. The reason they typed is discarded. The 403 does surface inside the dialog (deliberately, next to the text they wrote) but it should not have been reachable.
Same shape applies to credential storage and GitHub PAT storage, which carry the same guard.
Why it was deferred
Building UI scope-awareness needs a new backend surface and a pattern for gating actions — larger than #1247, and it would have been the only reason that PR touched auth.
Suggested approach
- Expose the current principal's scopes on an existing authenticated endpoint (or a small
/api/v2/me), respecting CODEFRAME_AUTH_REQUIRED the way /api/v2/settings/keys does so auth-off local dev still works.
- Note the auth-disabled synthetic principal carries all scopes — it is the single-operator local opt-out, and the UI must treat it as admin, not as unknown.
- Establish one pattern for admin-gated actions (disable + explain, rather than hide — a hidden button is indistinguishable from a missing feature) and apply it to the override, credential and PAT surfaces.
- Keep the 403 handling as the backstop. The server stays the authority; this only stops the UI promising something it cannot deliver.
Acceptance criteria
Follow-up from #1247. Refs #898, #336.
Problem
The frontend cannot tell whether the current user is an admin, so it offers admin-only actions to everyone and the refusal arrives only after the user has done the work.
web-ui/src/lib/auth.tsstores the raw JWT and nothing else — no decode, no/users/me, no scopes.is_superuser(grepped all ofcodeframe/ui/routers/).require_scope(SCOPE_ADMIN)genuinely refuses a non-superuser session.Most visible in the #1247 merge-gate override: a non-superuser sees the blocked merge, opens the override dialog, writes a justification, clicks Override and Merge — and gets a 403. The reason they typed is discarded. The 403 does surface inside the dialog (deliberately, next to the text they wrote) but it should not have been reachable.
Same shape applies to credential storage and GitHub PAT storage, which carry the same guard.
Why it was deferred
Building UI scope-awareness needs a new backend surface and a pattern for gating actions — larger than #1247, and it would have been the only reason that PR touched auth.
Suggested approach
/api/v2/me), respectingCODEFRAME_AUTH_REQUIREDthe way/api/v2/settings/keysdoes so auth-off local dev still works.Acceptance criteria
adminscope.Follow-up from #1247. Refs #898, #336.