'use client'; import { FormEvent, ReactNode, useEffect, useState } from 'react'; import { AlgaLogo } from '../AlgaLogo'; import styles from './auth.module.css'; import { TokenInput } from './TokenInput'; type Phase = 'loading' | 'error' | 'needs-token' | 'set-password' | 'needs-password' | 'authenticated'; function passwordValidationError(value: string): string | null { if (value.length < 8) return 'Use at least 8 characters.'; if (!/[a-z]/.test(value)) return 'Include a lowercase letter.'; if (!/[A-Z]/.test(value)) return 'Include an uppercase letter.'; if (!/\d/.test(value)) return 'Include a number.'; if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) return 'Include a special character.'; return null; } function Shell({ title, subtitle, children }: { title: string; subtitle: string; children: ReactNode }) { return ( Alga PSAAppliance setup {title} {subtitle} {children} ); } export function AuthGate({ children }: { children: ReactNode }) { const [phase, setPhase] = useState('loading'); const [token, setToken] = useState(''); const [tokenComplete, setTokenComplete] = useState(false); const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); async function loadState() { try { const response = await fetch('/api/auth/state', { cache: 'no-store' }); if (!response.ok) throw new Error('Unable to reach the appliance.'); const data = await response.json(); setPhase(data.phase === 'authenticated' ? 'authenticated' : data.phase === 'needs-password' ? 'needs-password' : 'needs-token'); } catch { setPhase('error'); } } useEffect(() => { loadState(); }, []); async function postJson(path: string, body: Record) { const response = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), }); const data = await response.json().catch(() => ({})); return { response, data }; } async function submitToken(event?: FormEvent) { event?.preventDefault(); if (!tokenComplete || busy) return; setBusy(true); setError(null); try { const { response, data } = await postJson('/api/auth/redeem-token', { token }); if (!response.ok) throw new Error(data.error || 'Incorrect setup token.'); setPhase('set-password'); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } } async function submitSetPassword(event: FormEvent) { event.preventDefault(); if (busy) return; const policyError = passwordValidationError(password); if (policyError) { setError(policyError); return; } if (password !== confirm) { setError('Passwords do not match.'); return; } setBusy(true); setError(null); try { const { response, data } = await postJson('/api/auth/set-password', { token, password }); if (!response.ok) throw new Error(data.error || 'Unable to set the password.'); window.location.reload(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setBusy(false); } } async function submitLogin(event: FormEvent) { event.preventDefault(); if (busy) return; setBusy(true); setError(null); try { const { response, data } = await postJson('/api/auth/login', { password }); if (!response.ok) throw new Error(data.error || 'Incorrect password.'); window.location.reload(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setBusy(false); } } if (phase === 'authenticated') return <>{children}>; if (phase === 'loading') { return One moment…; } if (phase === 'error') { return ( { setPhase('loading'); loadState(); }}>Retry ); } if (phase === 'needs-token') { return ( { setToken(value); setTokenComplete(complete); }} onSubmit={() => submitToken()} /> {error ? {error} : null} {busy ? 'Checking…' : 'Continue'} ); } if (phase === 'set-password') { return ( New password { setPassword(event.target.value); setError(null); }} disabled={busy} /> At least 8 characters with uppercase, lowercase, number, and special character. Confirm password { setConfirm(event.target.value); setError(null); }} disabled={busy} /> {error ? {error} : null} {busy ? 'Saving…' : 'Set password and continue'} ); } // needs-password return ( Password { setPassword(event.target.value); setError(null); }} disabled={busy} autoFocus /> {error ? {error} : null} {busy ? 'Signing in…' : 'Sign in'} ); }
{subtitle}
One moment…