Skip to content

feat(frontend): AuthGuard + JWT-derived role for /admin - #432

Merged
dark-sarge merged 2 commits into
arflexx:mainfrom
Zuliyat:fix/issue-284
Oct 1, 2026
Merged

dark-sarge merged 2 commits into
arflexx:mainfrom
Zuliyat:fix/issue-284

Conversation

@Zuliyat

@Zuliyat Zuliyat commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Closes #284

The admin page had an ad-hoc, in-component 403 check and derived role from the stored user object rather than the JWT. This adds a reusable guard and exposes the role on the auth context. (Note: a role !== "admin" guard already existed on main — deliberately a UX affordance, not a security control — so this is the acceptance's AuthGuard/redirect/role work on top of it.)

Changes

  • app/lib/auth.ts — readRole(token) decodes the JWT payload's role (base64url, no signature verification); absent/malformed ⇒ null (treated as non-admin). Documented as a rendering hint, never authorisation.
  • AuthContext — new role field (from the JWT, falling back to the stored user), kept in sync across hydration/login/logout.
  • app/components/AuthGuard.tsx — client guard { role: "admin" | "user"; children }. During hydration it renders a placeholder (never children, never a premature redirect); once hydrated a non-qualifying role is router.replace("/")d with a minimal notice so the admin UI never flashes.
  • app/admin/page.tsx — dashboard wrapped in <AuthGuard role="admin">; the bespoke 403 block removed; header comment updated to point at the guard.

Tests

  • Jest (11 passed): readRole (admin/user/malformed) and AuthGuard (admin renders children; non-admin redirects to /).
  • Playwright: e2e/admin-auth.spec.ts — a non-admin session is redirected away from /admin to / and never sees the dashboard. Uses the existing signIn(page, { role }) mock helper. (Added here; it needs the app + stub API servers to run, which aren't available in this environment.)
  • npx tsc --noEmit → exit 0.

The admin page had an ad-hoc in-component 403 check and derived role from the
stored user object, not the JWT. Add a reusable guard and expose the role on
the auth context:

- `app/lib/auth.ts`: `readRole(token)` decodes the JWT payload's `role`
  (base64url, no signature verification), returning null for absent/malformed
  values — so it is a rendering hint, never authorisation.
- `AuthContext`: expose `role` (from the JWT, falling back to the stored user),
  kept in sync across hydration/login/logout.
- `app/components/AuthGuard.tsx`: client guard with `role: "admin" | "user"`.
  While auth is hydrating it renders a placeholder (never the children, never a
  premature redirect); once hydrated, a non-qualifying role is redirected to `/`
  with `router.replace` and a minimal notice so the admin UI never flashes.
- `app/admin/page.tsx`: wrap the dashboard in `<AuthGuard role="admin">` and
  drop the bespoke 403 block.

Tests: `readRole` unit tests (admin/user/malformed), an `AuthGuard` unit test
(admin renders, non-admin redirects) and a Playwright spec
(`e2e/admin-auth.spec.ts`) asserting a non-admin session is redirected away
from `/admin`.

Closes arflexx#284
@Zuliyat
Zuliyat requested a review from dark-sarge as a code owner September 30, 2026 11:30
@drips-wave

drips-wave Bot commented Sep 30, 2026

Copy link
Copy Markdown

@Zuliyat Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@dark-sarge
dark-sarge merged commit 4df11b4 into arflexx:main Oct 1, 2026
3 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[frontend] - Admin page has no client-side access control; any authenticated user can navigate to it

3 participants