Build image / build-and-push (push) Successful in 7s
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
15 lines
673 B
TypeScript
15 lines
673 B
TypeScript
// Where to land after auth. The router guard stashes the intended destination
|
|
// in ?redirect=, and the login page reads it back; both go through this so a
|
|
// caller-controlled value can never send the user to an external origin.
|
|
|
|
const DEFAULT_DESTINATION = '/gardens'
|
|
|
|
/**
|
|
* Return dest only if it is a same-origin absolute path (starts with a single
|
|
* '/'); otherwise fall back to the gardens list. Rejects protocol-relative
|
|
* ('//evil.com') and absolute ('https://…') URLs.
|
|
*/
|
|
export function safeRedirectPath(dest: string | undefined, fallback = DEFAULT_DESTINATION): string {
|
|
return dest && dest.startsWith('/') && !dest.startsWith('//') ? dest : fallback
|
|
}
|