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:
@@ -1,14 +1,17 @@
|
||||
import { useState, type FormEvent, type ReactNode } from 'react'
|
||||
import { useState, type FormEvent } from 'react'
|
||||
import { Link, useNavigate, useSearch } from '@tanstack/react-router'
|
||||
import { AuthCard } from '@/components/auth/AuthCard'
|
||||
import { Alert } from '@/components/ui/Alert'
|
||||
import { Button, buttonClasses } from '@/components/ui/Button'
|
||||
import { Divider } from '@/components/ui/Divider'
|
||||
import { TextField } from '@/components/ui/TextField'
|
||||
import { errorMessage, useLogin, useProviders } from '@/lib/auth'
|
||||
import { API_BASE, errorMessage } from '@/lib/api'
|
||||
import { useLogin, useProviders } from '@/lib/auth'
|
||||
import { safeRedirectPath } from '@/lib/redirect'
|
||||
|
||||
// Server-initiated redirect (not a fetch): the browser navigates to the Go
|
||||
// handler, which 302s to the IdP.
|
||||
const OIDC_LOGIN_URL = '/api/v1/auth/oidc/login'
|
||||
const OIDC_LOGIN_URL = `${API_BASE}/auth/oidc/login`
|
||||
|
||||
// Messages for the ?error= codes the OIDC callback redirects back with.
|
||||
const callbackErrors: Record<string, string> = {
|
||||
@@ -20,11 +23,6 @@ const callbackErrors: Record<string, string> = {
|
||||
oidc_conflict: 'That single sign-on identity conflicts with an existing account.',
|
||||
}
|
||||
|
||||
// Only follow same-origin absolute paths, never a caller-controlled external URL.
|
||||
function safeRedirect(dest: string | undefined): string {
|
||||
return dest && dest.startsWith('/') && !dest.startsWith('//') ? dest : '/gardens'
|
||||
}
|
||||
|
||||
export function LoginPage() {
|
||||
const search = useSearch({ from: '/login' })
|
||||
const navigate = useNavigate()
|
||||
@@ -33,7 +31,7 @@ export function LoginPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
const dest = safeRedirect(search.redirect)
|
||||
const dest = safeRedirectPath(search.redirect)
|
||||
const callbackError = search.error ? (callbackErrors[search.error] ?? callbackErrors.oidc) : null
|
||||
|
||||
async function onSubmit(e: FormEvent) {
|
||||
@@ -66,7 +64,7 @@ export function LoginPage() {
|
||||
{oidc && local && <Divider>or</Divider>}
|
||||
|
||||
{local && (
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-3" noValidate>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-3">
|
||||
<TextField
|
||||
label="Email"
|
||||
name="email"
|
||||
@@ -110,13 +108,3 @@ export function LoginPage() {
|
||||
</AuthCard>
|
||||
)
|
||||
}
|
||||
|
||||
function Divider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 text-xs uppercase tracking-wide text-muted">
|
||||
<span className="h-px flex-1 bg-border" />
|
||||
{children}
|
||||
<span className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user