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
25 lines
1.0 KiB
TypeScript
25 lines
1.0 KiB
TypeScript
import type { ButtonHTMLAttributes } from 'react'
|
|
import { cn } from '@/lib/cn'
|
|
|
|
export type ButtonVariant = 'primary' | 'ghost'
|
|
|
|
/** Shared button styling, exported so anchor "buttons" (e.g. the OIDC link) match. */
|
|
export function buttonClasses(variant: ButtonVariant = 'primary', className?: string) {
|
|
return cn(
|
|
'inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors',
|
|
'outline-none focus-visible:ring-2 focus-visible:ring-accent/40',
|
|
'disabled:cursor-not-allowed disabled:opacity-60',
|
|
variant === 'primary' && 'bg-accent text-accent-contrast hover:bg-accent-strong',
|
|
variant === 'ghost' && 'border border-border text-fg hover:bg-border/50',
|
|
className,
|
|
)
|
|
}
|
|
|
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
variant?: ButtonVariant
|
|
}
|
|
|
|
export function Button({ variant = 'primary', className, type = 'button', ...props }: ButtonProps) {
|
|
return <button type={type} className={buttonClasses(variant, className)} {...props} />
|
|
}
|