Fixes from the PR #25 adversarial review (graded 23 real / 5 false positive):
Error handling
- onLogout wraps mutateAsync in try/catch: a failed logout no longer
becomes an unhandled rejection; the user stays put (session still valid)
and the button offers "Retry sign out" (5 models flagged this).
- Root errorComponent (RouteError): a non-401 /auth/me failure in a
beforeLoad guard now shows a recoverable "Try again" screen instead of
blanking.
- Forms drop noValidate, restoring native required/type=email/minLength
checks before hitting the server.
Correctness
- RegisterPage gates on providers.isPending/isError before rendering, so
the form no longer briefly appears (submittable) on an SSO-only server.
- TextField id falls back to name then a useId() value, so the label/hint
associations hold even if a caller omits both.
Maintainability
- Single safeRedirectPath (lib/redirect.ts) replaces the duplicated
safeRedirect/safeInternalPath (4 models).
- Generic ApiError helpers (apiErrorCode, errorMessage) moved to lib/api.ts;
LoginPage builds the OIDC URL from the exported API_BASE.
- Divider moved to components/ui/. errorMessage doc clarified.
Verified again in a real browser: register -> /gardens; Sign out -> /login.
tsc --noEmit and vite build clean.
Not changed (graded, with rationale): OIDC deep-link redirect isn't
preserved (needs backend state threading — follow-up); 60s me staleTime in
guards (server is authoritative); the login/register onSubmit catches are
the intended react-query pattern (errors render via mutation state).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
Frontend for pansy auth, rendering whatever /auth/providers reports.
- lib/auth.ts: zod-validated User/Providers, a shared meQueryOptions
(a 401 resolves to null, not an error) reused by useMe() and the router
guard, useProviders(), and useLogin/useRegister/useLogout mutations that
prime/clear the me cache (logout also drops non-auth cached data).
- lib/queryClient.ts: one QueryClient shared by the React tree and the
router context so guard and components hit the same cache.
- router.tsx: createRootRouteWithContext with the queryClient; requireAuth
(redirects to /login?redirect=<dest>) on gardens/plants/editor, and
requireGuest on login/register (bounces authed users away). Login search
params (redirect, error) validated as optional; redirect targets are
restricted to same-origin paths.
- pages/LoginPage: OIDC button (label from providers) linking to
/auth/oidc/login, "or" divider, local email/password form, and friendly
messages for the OIDC callback ?error= codes. RegisterPage: email +
display name + password (min 8), registration-closed and SSO-only
handling; auto-login lands on /gardens.
- AppShell: auth-aware nav — shows the user's display name + Sign out when
logged in, Sign in otherwise; nav links only when authed.
- ui/: TextField (16px text to avoid iOS zoom, autocomplete + a11y),
Button (+ shared buttonClasses for the OIDC anchor), Alert; AuthCard.
Verified in a real browser against the embedded binary: register →
auto-login → /gardens; refresh stays in; Sign out → /login; a logged-out
/gardens/1 redirects to /login and returns after login; OIDC button renders
with the Authentik label when configured. tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi