Spaces:
Running
Running
| import { useEffect, useState, type ReactNode } from 'react' | |
| import { useQuery, useQueryClient } from '@tanstack/react-query' | |
| import { apiFetch, setToken, UNAUTHORIZED_EVENT } from '@/lib/api' | |
| import { Button } from '@/components/ui/button' | |
| import { Input } from '@/components/ui/input' | |
| import { Label } from '@/components/ui/label' | |
| import { useI18n } from '@/i18n' | |
| interface AuthStatus { | |
| needsSetup: boolean | |
| authenticated: boolean | |
| email: string | null | |
| } | |
| function Centered({ children }: { children: ReactNode }) { | |
| return ( | |
| <div className="min-h-screen flex items-center justify-center bg-background px-4"> | |
| <div className="w-full max-w-sm">{children}</div> | |
| </div> | |
| ) | |
| } | |
| function AuthForm({ mode, onAuthed }: { mode: 'setup' | 'login'; onAuthed: () => void }) { | |
| const { t } = useI18n() | |
| const [email, setEmail] = useState('') | |
| const [password, setPassword] = useState('') | |
| const [error, setError] = useState('') | |
| const [busy, setBusy] = useState(false) | |
| const isSetup = mode === 'setup' | |
| async function submit(e: React.FormEvent) { | |
| e.preventDefault() | |
| setBusy(true) | |
| setError('') | |
| try { | |
| const res = await apiFetch<{ token: string }>(isSetup ? '/api/auth/setup' : '/api/auth/login', { | |
| method: 'POST', | |
| body: JSON.stringify({ email, password }), | |
| }) | |
| setToken(res.token) | |
| onAuthed() | |
| } catch (err) { | |
| setError((err as Error).message) | |
| } finally { | |
| setBusy(false) | |
| } | |
| } | |
| return ( | |
| <Centered> | |
| <div className="mb-6 flex items-center gap-2"> | |
| <span className="inline-block size-2 rounded-full bg-foreground" /> | |
| <span className="font-semibold tracking-tight text-sm">FreeLLMAPI</span> | |
| </div> | |
| <div className="rounded-3xl border bg-card p-6"> | |
| <h1 className="text-base font-medium">{isSetup ? t('auth.createYourAccount') : t('auth.signIn')}</h1> | |
| <p className="text-xs text-muted-foreground mt-1 mb-4"> | |
| {isSetup | |
| ? t('auth.setupDescription') | |
| : t('auth.loginDescription')} | |
| </p> | |
| <form onSubmit={submit} className="space-y-3"> | |
| <div className="space-y-1.5"> | |
| <Label className="text-xs" htmlFor="auth-email">{t('auth.email')}</Label> | |
| <Input | |
| id="auth-email" | |
| type="email" | |
| autoComplete="username" | |
| value={email} | |
| onChange={e => setEmail(e.target.value)} | |
| placeholder={t('auth.emailPlaceholder')} | |
| /> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <Label className="text-xs" htmlFor="auth-password">{t('auth.password')}</Label> | |
| <Input | |
| id="auth-password" | |
| type="password" | |
| autoComplete={isSetup ? 'new-password' : 'current-password'} | |
| value={password} | |
| onChange={e => setPassword(e.target.value)} | |
| placeholder={isSetup ? t('auth.passwordPlaceholderSetup') : t('auth.passwordPlaceholderLogin')} | |
| /> | |
| </div> | |
| {error && <p className="text-destructive text-xs">{error}</p>} | |
| <Button type="submit" className="w-full" disabled={busy || !email || !password}> | |
| {busy ? (isSetup ? t('auth.creating') : t('auth.signingIn')) : isSetup ? t('auth.createAccount') : t('auth.signIn')} | |
| </Button> | |
| </form> | |
| </div> | |
| </Centered> | |
| ) | |
| } | |
| export function AuthGate({ children }: { children: ReactNode }) { | |
| const { t } = useI18n() | |
| const queryClient = useQueryClient() | |
| const { data, isLoading, isError, refetch } = useQuery<AuthStatus>({ | |
| queryKey: ['auth-status'], | |
| queryFn: () => apiFetch('/api/auth/status'), | |
| retry: false, | |
| }) | |
| useEffect(() => { | |
| const handler = () => { refetch() } | |
| window.addEventListener(UNAUTHORIZED_EVENT, handler) | |
| return () => window.removeEventListener(UNAUTHORIZED_EVENT, handler) | |
| }, [refetch]) | |
| function onAuthed() { | |
| // New session: drop any cached (unauthenticated) data and re-check status. | |
| queryClient.invalidateQueries() | |
| refetch() | |
| } | |
| if (isLoading) return <Centered><p className="text-sm text-muted-foreground text-center">{t('auth.loading')}</p></Centered> | |
| if (isError || !data) { | |
| return ( | |
| <Centered> | |
| <div className="rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2.5 text-xs text-destructive"> | |
| {t('auth.serverUnreachableBefore')}<code className="font-mono">npm run dev</code>{t('auth.serverUnreachableAfter')} | |
| </div> | |
| </Centered> | |
| ) | |
| } | |
| if (data.needsSetup) return <AuthForm mode="setup" onAuthed={onAuthed} /> | |
| if (!data.authenticated) return <AuthForm mode="login" onAuthed={onAuthed} /> | |
| return <>{children}</> | |
| } | |