Skip to content

frontend: extract <RequireRole> guard to replace inline role-gate pattern (5 views) #58

Description

@countercheck

Source: codebase audit, 2026-06-11 (audit finding F4, Tier 1)
Severity: Medium
Category: DRY + safety (defense-in-depth)

Problem

Five admin views reimplement the same UI-side role check inline:

if (user && !user.roles.includes('admin')) {
  return <Alert tone="error">You do not have access to this area.</Alert>;
}

Sites:

A new admin view that forgets the check is a silent authorization regression on the UI side. The API still enforces (so it's not a security hole today), but the defense-in-depth pattern shouldn't be copy-paste.

Suggested approach

Add frontend/src/auth/RequireRole.tsx:

export function RequireRole({ roles, children }: { roles: Role[]; children: ReactNode }): JSX.Element;

Behavior:

  • Reads useAuth()
  • While status === 'loading' → render nothing (parent RequireAuth already handles the loading shell)
  • If user has none of the required roles → render the existing <Alert tone="error"> copy
  • Else → render children

Compose with the existing RequireAuth at the route level in AdminApp.tsx, e.g.:

<Route element={<RequireAuth />}>
  <Route path="users" element={<RequireRole roles={['admin']}><UsersView /></RequireRole>} />
</Route>

Colocated test: admin user passes; non-admin sees alert; multi-role intersection (researcher OR admin) works.

Part of a cluster

This pairs naturally with #55, #56, #57, #61 — the same PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions