Kairo-Jinkendo/frontend/src/App.jsx

183 lines
5.2 KiB
React
Raw Normal View History

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
}
export default function App() {
const [health, setHealth] = useState(null)
const [healthError, setHealthError] = useState(null)
const [token, setToken] = useState(() => localStorage.getItem(TOKEN_KEY) || '')
const [email, setEmail] = useState('')
const [password, setPassword] = 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))
}, [])
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])
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 }),
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new Error(body.detail || `Login fehlgeschlagen (${res.status})`)
}
const data = await res.json()
localStorage.setItem(TOKEN_KEY, data.token)
setToken(data.token)
setPassword('')
} 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 {
/* Session trotzdem lokal entfernen */
} finally {
localStorage.removeItem(TOKEN_KEY)
setToken('')
setMe(null)
setContext(null)
setAuthBusy(false)
}
}
return (
<main className="shell">
<header className="hero">
<h1>Jinkendo Kairo</h1>
<p>Operativer Program Director — Sprint 0 / AP0.2</p>
<p className="muted">Auth, Tenant &amp; Actor Foundation</p>
</header>
<section className="card">
<h2>API Health</h2>
{healthError && <p className="error">Fehler: {healthError}</p>}
{!healthError && !health && <p>Lade …</p>}
{health && <pre>{JSON.stringify(health, null, 2)}</pre>}
</section>
<section className="card">
<h2>Session</h2>
{!token && (
<form className="form" onSubmit={handleLogin}>
<label>
E-Mail
<input
type="email"
autoComplete="username"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</label>
<label>
Passwort
<input
type="password"
autoComplete="current-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
minLength={8}
required
/>
</label>
<button type="submit" disabled={authBusy}>
{authBusy ? 'Anmelden …' : 'Anmelden'}
</button>
</form>
)}
{token && me && (
<div className="session">
<p>
Eingeloggt als <strong>{me.display_name}</strong> ({me.email})
</p>
<p className="muted">
Portal-Rolle: {me.portal_role}
{me.active_tenant_id && ` · Tenant: ${me.active_tenant_id}`}
</p>
<button type="button" className="secondary" onClick={handleLogout} disabled={authBusy}>
Abmelden
</button>
</div>
)}
{authError && <p className="error">{authError}</p>}
</section>
{token && me && (
<>
<section className="card">
<h2>/api/me</h2>
<pre>{JSON.stringify(me, null, 2)}</pre>
</section>
{context && (
<section className="card">
<h2>/api/me/context</h2>
<pre>{JSON.stringify(context, null, 2)}</pre>
</section>
)}
</>
)}
</main>
)
}