Spaces:
Runtime error
Runtime error
File size: 4,601 Bytes
077865a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | 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'
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 [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 ? 'Create your account' : 'Sign in'}</h1>
<p className="text-xs text-muted-foreground mt-1 mb-4">
{isSetup
? 'Set the email and password that will protect this dashboard.'
: 'Sign in to manage your keys, routing, and analytics.'}
</p>
<form onSubmit={submit} className="space-y-3">
<div className="space-y-1.5">
<Label className="text-xs" htmlFor="auth-email">Email</Label>
<Input
id="auth-email"
type="email"
autoComplete="username"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="you@example.com"
/>
</div>
<div className="space-y-1.5">
<Label className="text-xs" htmlFor="auth-password">Password</Label>
<Input
id="auth-password"
type="password"
autoComplete={isSetup ? 'new-password' : 'current-password'}
value={password}
onChange={e => setPassword(e.target.value)}
placeholder={isSetup ? 'at least 8 characters' : 'your password'}
/>
</div>
{error && <p className="text-destructive text-xs">{error}</p>}
<Button type="submit" className="w-full" disabled={busy || !email || !password}>
{busy ? (isSetup ? 'Creating…' : 'Signing in…') : isSetup ? 'Create account' : 'Sign in'}
</Button>
</form>
</div>
</Centered>
)
}
export function AuthGate({ children }: { children: ReactNode }) {
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">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">
Can't reach the server. Make sure the backend is running (<code className="font-mono">npm run dev</code>).
</div>
</Centered>
)
}
if (data.needsSetup) return <AuthForm mode="setup" onAuthed={onAuthed} />
if (!data.authenticated) return <AuthForm mode="login" onAuthed={onAuthed} />
return <>{children}</>
}
|