import { useCallback, useEffect, useState } from 'react' const TOKEN_KEY = 'kairo_auth_token' function apiHeaders(token) { const headers = { 'Content-Type': 'application/json' } if (token) headers['X-Auth-Token'] = token return headers } function parseError(body, fallback) { if (!body) return fallback if (typeof body.detail === 'string') return body.detail if (Array.isArray(body.detail)) { return body.detail.map((d) => d.msg || JSON.stringify(d)).join(', ') } return fallback } export default function App() { const [health, setHealth] = useState(null) const [healthError, setHealthError] = useState(null) const [setup, setSetup] = useState(null) const [setupLoading, setSetupLoading] = useState(true) const [setupError, setSetupError] = useState(null) const [authMode, setAuthMode] = useState('login') const [token, setToken] = useState(() => localStorage.getItem(TOKEN_KEY) || '') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [displayName, setDisplayName] = useState('') const [organizationName, setOrganizationName] = useState('') const [passwordConfirm, setPasswordConfirm] = useState('') const [authError, setAuthError] = useState(null) const [authBusy, setAuthBusy] = useState(false) const [me, setMe] = useState(null) const [context, setContext] = useState(null) useEffect(() => { fetch('/api/health') .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`) return res.json() }) .then(setHealth) .catch((err) => setHealthError(err.message)) async function loadSetupStatus() { setSetupLoading(true) setSetupError(null) try { const res = await fetch('/api/auth/setup-status') const body = await res.json().catch(() => ({})) if (!res.ok) { throw new Error( res.status === 404 ? 'Endpoint /api/auth/setup-status nicht gefunden — Backend neu deployen?' : parseError(body, `Setup-Status fehlgeschlagen (HTTP ${res.status})`), ) } if (typeof body.registration_open !== 'boolean') { throw new Error('Ungültige Antwort vom Setup-Status — Backend-Version prüfen') } setSetup(body) if (body.registration_open) setAuthMode('register') } catch (err) { setSetup(null) setSetupError(err.message) } finally { setSetupLoading(false) } } loadSetupStatus() }, []) const loadSession = useCallback(async (activeToken) => { if (!activeToken) { setMe(null) setContext(null) return } const meRes = await fetch('/api/me', { headers: apiHeaders(activeToken) }) if (!meRes.ok) { localStorage.removeItem(TOKEN_KEY) setToken('') setMe(null) setContext(null) throw new Error(meRes.status === 401 ? 'Session abgelaufen' : `HTTP ${meRes.status}`) } setMe(await meRes.json()) const ctxRes = await fetch('/api/me/context', { headers: apiHeaders(activeToken) }) if (ctxRes.ok) setContext(await ctxRes.json()) }, []) useEffect(() => { if (!token) return loadSession(token).catch((err) => setAuthError(err.message)) }, [token, loadSession]) function applyLogin(data) { localStorage.setItem(TOKEN_KEY, data.token) setToken(data.token) setPassword('') setPasswordConfirm('') setSetup({ registration_open: false, has_users: true, user_count: 1 }) setAuthMode('login') } async function handleLogin(event) { event.preventDefault() setAuthBusy(true) setAuthError(null) try { const res = await fetch('/api/auth/login', { method: 'POST', headers: apiHeaders(), body: JSON.stringify({ email, password }), }) const body = await res.json().catch(() => ({})) if (!res.ok) throw new Error(parseError(body, `Login fehlgeschlagen (${res.status})`)) applyLogin(body) } catch (err) { setAuthError(err.message) } finally { setAuthBusy(false) } } async function handleRegister(event) { event.preventDefault() if (password !== passwordConfirm) { setAuthError('Passwörter stimmen nicht überein') return } setAuthBusy(true) setAuthError(null) try { const res = await fetch('/api/auth/register', { method: 'POST', headers: apiHeaders(), body: JSON.stringify({ email, password, display_name: displayName, organization_name: organizationName || undefined, }), }) const body = await res.json().catch(() => ({})) if (!res.ok) throw new Error(parseError(body, `Registrierung fehlgeschlagen (${res.status})`)) applyLogin(body) } catch (err) { setAuthError(err.message) } finally { setAuthBusy(false) } } async function handleLogout() { setAuthBusy(true) setAuthError(null) try { if (token) { await fetch('/api/auth/logout', { method: 'POST', headers: apiHeaders(token), }) } } catch { /* lokal entfernen */ } finally { localStorage.removeItem(TOKEN_KEY) setToken('') setMe(null) setContext(null) setAuthBusy(false) } } const registrationOpen = setup?.registration_open === true const registrationClosedBecauseUsersExist = setup?.has_users === true && !registrationOpen return (

Jinkendo Kairo

Operativer Program Director — Sprint 0 / AP0.5

Capability Registry & Entitlements

{token ? 'Session' : registrationOpen ? 'Ersteinrichtung' : 'Anmeldung'}

{!token && registrationOpen && (

Noch kein Systemadministrator. Der erste Account wird automatisch{' '} Portal-Admin und erhält einen Default-Tenant.

)} {!token && setupLoading && (

Prüfe Ersteinrichtung …

)} {!token && setupError && (

{setupError}

)} {!token && registrationClosedBecauseUsersExist && (

Es existiert bereits mindestens ein Benutzer ({setup.user_count ?? '≥1'}). Registrierung ist nur für die Ersteinrichtung vorgesehen — bitte anmelden. Falls der Admin per KAIRO_BOOTSTRAP_* in der .env angelegt wurde, nutzen Sie diese Zugangsdaten.

)} {!token && !setupLoading && !setupError && ( <>
{authMode === 'login' && (
)} {authMode === 'register' && registrationOpen && (
)} {authMode === 'register' && !registrationOpen && (

Registrierung ist geschlossen. Bitte melden Sie sich an oder wenden Sie sich an den Administrator.

)} )} {token && me && (

Eingeloggt als {me.display_name} ({me.email})

{me.portal_role === 'admin' ? 'Portal-Systemadmin' : `Portal-Rolle: ${me.portal_role}`} {context?.tenant?.name && ` · ${context.tenant.name}`}

)} {authError &&

{authError}

}

Ersteinrichtung

{setupLoading &&

Lade …

} {setupError &&

{setupError}

} {setup &&
{JSON.stringify(setup, null, 2)}
} {!setupLoading && !setup && !setupError && (

Kein Setup-Status verfügbar.

)}

API Health

{healthError &&

Fehler: {healthError}

} {!healthError && !health &&

Lade …

} {health &&
{JSON.stringify(health, null, 2)}
}
{token && me && ( <>

/api/me

{JSON.stringify(me, null, 2)}
{context && (

/api/me/context

{JSON.stringify(context, null, 2)}
)} )}
) }