'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 ( ); } if (phase === 'needs-token') { return (
{ setToken(value); setTokenComplete(complete); }} onSubmit={() => submitToken()} /> {error ?
{error}
: null}
); } if (phase === 'set-password') { return (
{ setPassword(event.target.value); setError(null); }} disabled={busy} /> At least 8 characters with uppercase, lowercase, number, and special character.
{ setConfirm(event.target.value); setError(null); }} disabled={busy} />
{error ?
{error}
: null}
); } // needs-password return (
{ setPassword(event.target.value); setError(null); }} disabled={busy} autoFocus />
{error ?
{error}
: null}
); }