import { useState, useCallback, useEffect } from 'react'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { ArrowRight, Loader2 } from 'lucide-react'; import { AuthCanvas } from '@/components/auth/AuthCanvas'; import { AuthStepHeader } from '@/components/auth/AuthStepHeader'; import { ErrorRail } from '@/components/auth/ErrorRail'; import { EnvironmentChecks, type EnvironmentReport } from '@/components/settings/EnvironmentChecks'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; interface SetupProps { onComplete: () => void; } const POST_SETUP_KEY = 'sencho:post-setup'; const INPUT_CLASS = 'h-11 bg-background/60 border-card-border font-sans text-base shadow-[inset_0_2px_4px_0_oklch(0_0_0/0.25)] placeholder:text-stat-subtitle/60 focus-visible:border-brand/60 focus-visible:ring-2 focus-visible:ring-brand/40 focus-visible:ring-offset-0'; type Strength = { label: string; tone: 'weak' | 'fair' | 'strong' } | null; function gaugePassword(pw: string): Strength { if (pw.length === 0) return null; if (pw.length < 8) return { label: 'Weak', tone: 'weak' }; const classes = Number(/[a-z]/.test(pw)) + Number(/[A-Z]/.test(pw)) + Number(/\d/.test(pw)) + Number(/[^A-Za-z0-9]/.test(pw)); if (pw.length >= 12 && classes >= 3) return { label: 'Strong', tone: 'strong' }; return { label: 'Fair', tone: 'fair' }; } export function Setup({ onComplete, className, ...props }: SetupProps & React.ComponentPropsWithoutRef<'div'>) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); const [step, setStep] = useState<'account' | 'env'>('account'); const [envReport, setEnvReport] = useState(null); const [envLoading, setEnvLoading] = useState(false); const loadEnvironment = useCallback(async () => { setEnvLoading(true); try { const res = await apiFetch('/diagnostics/environment', { localOnly: true }); if (!res.ok) { const err = await res.json().catch(() => ({})); toast.error((err as { error?: string })?.error || 'Failed to run environment checks.'); setEnvReport(null); return; } setEnvReport((await res.json()) as EnvironmentReport); } catch (e: unknown) { toast.error((e as Error)?.message || 'Failed to run environment checks.'); setEnvReport(null); } finally { setEnvLoading(false); } }, []); useEffect(() => { if (step !== 'env') return; void loadEnvironment(); }, [step, loadEnvironment]); const strength = gaugePassword(password); const strengthClass = strength?.tone === 'strong' ? 'text-success' : strength?.tone === 'fair' ? 'text-warning' : strength ? 'text-destructive' : ''; const handleEnterSencho = () => { const adoptCount = envReport?.discovery?.adoptCandidateCount ?? 0; if (adoptCount > 0) { try { sessionStorage.setItem(POST_SETUP_KEY, JSON.stringify({ openAdopt: true })); } catch { // sessionStorage unavailable } } onComplete(); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); if (password !== confirmPassword) { setError('Passwords do not match'); return; } if (username.length < 3) { setError('Username must be at least 3 characters'); return; } if (password.length < 8) { setError('Password must be at least 8 characters'); return; } setIsLoading(true); try { const response = await fetch('/api/auth/setup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ username, password, confirmPassword }), }); const data = await response.json(); if (response.ok && data.success) { setStep('env'); } else { setError(data.error || 'Setup failed'); } } catch { setError('Network error. Please try again.'); } finally { setIsLoading(false); } }; if (step === 'env') { return (
Console · First boot Account ready
} >
); } return (
Console · First boot Empty database
} >
setUsername(e.target.value)} className={INPUT_CLASS} />
{strength && ( {strength.label} )}
setPassword(e.target.value)} className={INPUT_CLASS} />
setConfirmPassword(e.target.value)} className={INPUT_CLASS} /> {error && {error}}
); } function Field({ id, label, children }: { id: string; label: string; children: React.ReactNode }) { return (
{children}
); }