All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 29s
Test Suite / lint-backend (push) Successful in 2s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 19s
Co-authored-by: Cursor <cursoragent@cursor.com>
154 lines
5.4 KiB
JavaScript
154 lines
5.4 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { fetchSetupStatus, login, register } from '../api/me.js'
|
|
import { useSession } from '../context/SessionContext.jsx'
|
|
|
|
export function AuthPanel() {
|
|
const { applyLogin } = useSession()
|
|
const [setup, setSetup] = useState(null)
|
|
const [setupLoading, setSetupLoading] = useState(true)
|
|
const [setupError, setSetupError] = useState(null)
|
|
const [authMode, setAuthMode] = useState('login')
|
|
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)
|
|
|
|
useEffect(() => {
|
|
fetchSetupStatus()
|
|
.then((body) => {
|
|
setSetup(body)
|
|
if (body.registration_open) setAuthMode('register')
|
|
})
|
|
.catch((err) => setSetupError(err.message))
|
|
.finally(() => setSetupLoading(false))
|
|
}, [])
|
|
|
|
async function handleLogin(e) {
|
|
e.preventDefault()
|
|
setAuthBusy(true)
|
|
setAuthError(null)
|
|
try {
|
|
const body = await login(email, password)
|
|
applyLogin(body)
|
|
} catch (err) {
|
|
setAuthError(err.message)
|
|
} finally {
|
|
setAuthBusy(false)
|
|
}
|
|
}
|
|
|
|
async function handleRegister(e) {
|
|
e.preventDefault()
|
|
if (password !== passwordConfirm) {
|
|
setAuthError('Passwörter stimmen nicht überein')
|
|
return
|
|
}
|
|
setAuthBusy(true)
|
|
setAuthError(null)
|
|
try {
|
|
const body = await register({
|
|
email,
|
|
password,
|
|
display_name: displayName,
|
|
organization_name: organizationName || undefined,
|
|
})
|
|
applyLogin(body)
|
|
} catch (err) {
|
|
setAuthError(err.message)
|
|
} finally {
|
|
setAuthBusy(false)
|
|
}
|
|
}
|
|
|
|
const registrationOpen = setup?.registration_open === true
|
|
|
|
return (
|
|
<main className="auth-shell">
|
|
<header className="auth-hero">
|
|
<h1>Kairo</h1>
|
|
<p>Jinkendo Program Director — Vorhaben & Maßnahmen</p>
|
|
</header>
|
|
|
|
<section className="card auth-card">
|
|
<h2 className="card-title card-title--lg">
|
|
{registrationOpen ? 'Ersteinrichtung' : 'Anmelden'}
|
|
</h2>
|
|
|
|
{setupLoading && <p className="muted">Prüfe Ersteinrichtung …</p>}
|
|
{setupError && <p className="error">{setupError}</p>}
|
|
|
|
{!setupLoading && !setupError && (
|
|
<>
|
|
<div className="tabs" role="tablist">
|
|
<button
|
|
type="button"
|
|
className={authMode === 'login' ? 'tab active' : 'tab'}
|
|
onClick={() => { setAuthMode('login'); setAuthError(null) }}
|
|
>
|
|
Anmelden
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={authMode === 'register' ? 'tab active' : 'tab'}
|
|
disabled={!registrationOpen}
|
|
onClick={() => { setAuthMode('register'); setAuthError(null) }}
|
|
>
|
|
Registrieren
|
|
</button>
|
|
</div>
|
|
|
|
{authMode === 'login' && (
|
|
<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" className="btn btn-primary btn-block-mobile" disabled={authBusy}>
|
|
{authBusy ? 'Anmelden …' : 'Anmelden'}
|
|
</button>
|
|
</form>
|
|
)}
|
|
|
|
{authMode === 'register' && registrationOpen && (
|
|
<form className="form" onSubmit={handleRegister}>
|
|
<label>
|
|
Anzeigename
|
|
<input value={displayName} onChange={(e) => setDisplayName(e.target.value)} required />
|
|
</label>
|
|
<label>
|
|
Organisation (optional)
|
|
<input value={organizationName} onChange={(e) => setOrganizationName(e.target.value)} />
|
|
</label>
|
|
<label>
|
|
E-Mail
|
|
<input type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
|
</label>
|
|
<label>
|
|
Passwort
|
|
<input type="password" autoComplete="new-password" value={password} onChange={(e) => setPassword(e.target.value)} minLength={8} required />
|
|
</label>
|
|
<label>
|
|
Passwort bestätigen
|
|
<input type="password" autoComplete="new-password" value={passwordConfirm} onChange={(e) => setPasswordConfirm(e.target.value)} minLength={8} required />
|
|
</label>
|
|
<button type="submit" className="btn btn-primary btn-block-mobile" disabled={authBusy}>
|
|
{authBusy ? 'Registrieren …' : 'Systemadmin anlegen'}
|
|
</button>
|
|
</form>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{authError && <p className="error">{authError}</p>}
|
|
</section>
|
|
</main>
|
|
)
|
|
}
|