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.
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:
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:Behavior:
useAuth()status === 'loading'→ render nothing (parentRequireAuthalready handles the loading shell)<Alert tone="error">copychildrenCompose with the existing
RequireAuthat the route level inAdminApp.tsx, e.g.: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.