Address Gadfly review on #6: logout errors, register gating, guard errors
Build image / build-and-push (push) Successful in 7s
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
This commit is contained in:
+12
-9
@@ -6,6 +6,7 @@ import {
|
||||
} from '@tanstack/react-router'
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
import { AppShell } from '@/components/layout/AppShell'
|
||||
import { RouteError } from '@/components/RouteError'
|
||||
import { LoginPage } from '@/pages/LoginPage'
|
||||
import { RegisterPage } from '@/pages/RegisterPage'
|
||||
import { GardensPage } from '@/pages/GardensPage'
|
||||
@@ -13,19 +14,25 @@ import { GardenEditorPage } from '@/pages/GardenEditorPage'
|
||||
import { PlantsPage } from '@/pages/PlantsPage'
|
||||
import { meQueryOptions } from '@/lib/auth'
|
||||
import { queryClient } from '@/lib/queryClient'
|
||||
import { safeRedirectPath } from '@/lib/redirect'
|
||||
|
||||
interface RouterContext {
|
||||
queryClient: QueryClient
|
||||
}
|
||||
|
||||
const rootRoute = createRootRouteWithContext<RouterContext>()({ component: AppShell })
|
||||
const rootRoute = createRootRouteWithContext<RouterContext>()({
|
||||
component: AppShell,
|
||||
// A beforeLoad/loader failure that isn't a redirect (e.g. /auth/me errors with
|
||||
// a 500 or the network drops) lands here instead of a blank screen.
|
||||
errorComponent: RouteError,
|
||||
})
|
||||
|
||||
// requireAuth: resolve the current user (shared cache with useMe); send anyone
|
||||
// unauthenticated to /login, remembering where they were headed.
|
||||
async function requireAuth(context: RouterContext, href: string) {
|
||||
// unauthenticated to /login, remembering the path they were headed to.
|
||||
async function requireAuth(context: RouterContext, path: string) {
|
||||
const me = await context.queryClient.ensureQueryData(meQueryOptions)
|
||||
if (!me) {
|
||||
throw redirect({ to: '/login', search: { redirect: href } })
|
||||
throw redirect({ to: '/login', search: { redirect: path } })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +44,6 @@ async function requireGuest(context: RouterContext, redirectTo: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function safeInternalPath(dest: string | undefined): string {
|
||||
return dest && dest.startsWith('/') && !dest.startsWith('//') ? dest : '/gardens'
|
||||
}
|
||||
|
||||
const indexRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/',
|
||||
@@ -65,7 +68,7 @@ const loginRoute = createRoute({
|
||||
if (typeof search.error === 'string') out.error = search.error
|
||||
return out
|
||||
},
|
||||
beforeLoad: ({ context, search }) => requireGuest(context, safeInternalPath(search.redirect)),
|
||||
beforeLoad: ({ context, search }) => requireGuest(context, safeRedirectPath(search.redirect)),
|
||||
component: LoginPage,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user