Skip to content

feat(web): login page, auth context and admin pages - #201

Open
TartanLeGrand wants to merge 28 commits into
BananaOps:mainfrom
TartanLeGrand:feat/auth-web
Open

TartanLeGrand wants to merge 28 commits into
BananaOps:mainfrom
TartanLeGrand:feat/auth-web

Conversation

@TartanLeGrand

@TartanLeGrand TartanLeGrand commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Second PR of the SSO / RBAC series (refs #196), on top of the backend merged in #200 and the react-router 7 upgrade from #202.

What

  • AuthProvider loads GET /auth/config and GET /auth/me once, exposes principal, hasPermission, inScope, logout, reload.
  • axios sends the session cookie (withCredentials); VITE_API_TOKEN is removed. 401 redirects to /login?redirect=<page>, 403 shows an "Access denied" toast.
  • /login (local form, SSO button when oidcEnabled, generic error, rate-limit and cross-site messages, forced redirect to /account/password when mustChangePassword).
  • /account/password for local accounts.
  • Sidebar entries hidden without the section's *:read permission, create/edit/delete/lock buttons hidden without *:write, new Administration section, account menu (source, teams, change password, sign out), demo banner hint for anonymous visitors.
  • /admin/users, /admin/teams, /admin/api-keys (secret shown once with copy, revoke with confirmation), all behind access:manage.
  • Route guards: RequirePermission for pages, Can for actions. Static (GitHub Pages) build keeps working with a read-mostly anonymous principal.
  • Test toolchain: vitest 5 + Testing Library, 75 tests across 14 files; new Web workflow (scoped ESLint, scoped tsc filter, vitest, build).

Not in this PR

  • OIDC login itself (PR 3): the SSO button links to /api/v1alpha1/auth/oidc/login, hidden until the backend reports oidcEnabled.
  • Per-service scope selector (follow-up of feat(auth): per-service team scope #216): the team dialog shows the team's scope read-only and sends it back unchanged, so a team restricted through the API is not widened when edited from the UI.
  • linksApi.ts still uses fetch and is not wired to the 401/403 interceptor; anonymous keeps links:* until PR 5.

Checks

Known follow-ups (not blocking, tracked for PR 3+)

  • Admin pages do not refresh the signed-in principal after editing your own team; the backend enforces, the UI goes stale until reload.
  • reload() has no request-generation guard, a late /auth/me response can produce one stale render.
  • Interceptor exemptions match with endsWith, harmless with the current route set.

Refs #196

Login only called reload() on the normal sign-in path. When the backend
required an immediate password change, the app navigated to
/account/password while the AuthContext still held the stale anonymous
principal, so the RequirePermission guard bounced the user straight
back to /login.
…assword change

The top-level guard that sends an already-signed-in visitor away from
/login used the plain redirect target, racing the explicit navigation
to /account/password once the auth context refreshes mid-submit. The
guard now redirects to /account/password itself when the principal
still needs a password change, so the outcome is correct regardless of
which navigation wins the race.
…s in flight

Route changes are React transitions, so after sign-out a protected page
re-rendered with the anonymous principal before the navigation committed
and its guard pushed /login?redirect=<page>. The provider now shows the
loading screen from the sign-out until the location reaches /login.
Reproduced in a browser on the built image and pinned by a test that
signs out from a guarded route.
main moved to react-router 7 (BananaOps#202), where the v7_* future flags are the
default behaviour and no longer part of the MemoryRouter props.
Aligns with BananaOps#205 while keeping the actions pinned to commit SHAs.
main moved to Tailwind 4 (BananaOps#206): rounded becomes rounded-sm and
outline-none becomes outline-hidden in the user menu and the API key
dialog.
The login page now carries the Single Sign-On button announced by the
OpenID Connect documentation, and CLAUDE.md said no frontend test runner
existed.
The team dialog always sent scopeAll=true, so saving a team restricted
through the API silently widened it to every service. The dialog now
sends back the scope it loaded and shows it read-only; a new team still
gets all services.

Refs BananaOps#196
TartanLeGrand added a commit to TartanLeGrand/tracker that referenced this pull request Oct 4, 2026
The web team dialog (BananaOps#201) now sends back the scope it loaded, so
editing a restricted team from the UI no longer widens it.

Refs BananaOps#196

This branch has not been deployed

No deployments
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.

1 participant