import { useEffect, useState, useRef } from 'react' import { useSearchParams, Link } from 'react-router-dom' type SetupStep = 'idle' | 'registering-sw' | 'generating' | 'done' | 'error' function isBrowserSupported(): boolean { try { // Ed25519 requires SubtleCrypto and the algorithm support // We can only really verify at runtime, but check for the basics here return ( typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined' && typeof crypto.randomUUID === 'function' && 'serviceWorker' in navigator ) } catch { return false } } async function sendMessageToSW( reg: ServiceWorkerRegistration, message: object ): Promise> { return new Promise((resolve, reject) => { const channel = new MessageChannel() channel.port1.onmessage = (e) => resolve(e.data as Record) channel.port1.onmessageerror = () => reject(new Error('MessageChannel-Fehler')) const sw = reg.active if (!sw) { reject(new Error('ServiceWorker nicht aktiv')); return } sw.postMessage({ ...message, port: channel.port2 }, [channel.port2]) }) } export function KioskSetupPage() { const [searchParams] = useSearchParams() const deviceIdParam = searchParams.get('device_id') ?? '' const [deviceId, setDeviceId] = useState(deviceIdParam) const [step, setStep] = useState('idle') const [errorMsg, setErrorMsg] = useState(null) const [publicKeyPem, setPublicKeyPem] = useState(null) const [copied, setCopied] = useState(false) const [existingDeviceId, setExistingDeviceId] = useState(null) const swRegRef = useRef(null) const supported = isBrowserSupported() // On mount: register SW and check for existing credentials useEffect(() => { if (!supported) return ;(async () => { try { const reg = await navigator.serviceWorker.register('/kiosk-sw.js', { scope: '/' }) // Wait until the SW is active if (!reg.active) { await new Promise((resolve) => { const sw = reg.installing || reg.waiting if (!sw) { resolve(); return } sw.addEventListener('statechange', function handler() { if (sw.state === 'activated') { sw.removeEventListener('statechange', handler) resolve() } }) }) // Re-fetch after activation const updated = await navigator.serviceWorker.ready swRegRef.current = updated } else { swRegRef.current = reg } // Check if credentials already exist const ready = await navigator.serviceWorker.ready swRegRef.current = ready const result = await sendMessageToSW(ready, { type: 'CHECK_CREDENTIALS' }) if (result.hasCredentials) { setExistingDeviceId(result.deviceId as string) } } catch { // SW registration failure is non-fatal for the setup UI } })() }, [supported]) async function handleGenerate() { setErrorMsg(null) setCopied(false) setPublicKeyPem(null) if (!deviceId.trim()) { setErrorMsg('Bitte eine Geraete-ID eingeben (UUID des Kiosk-Geraets).') return } setStep('registering-sw') try { if (!swRegRef.current) { const reg = await navigator.serviceWorker.register('/kiosk-sw.js', { scope: '/' }) const ready = await navigator.serviceWorker.ready swRegRef.current = reg.active ? reg : ready } const ready = await navigator.serviceWorker.ready swRegRef.current = ready setStep('generating') const result = await sendMessageToSW(ready, { type: 'GENERATE_KEYPAIR', deviceId: deviceId.trim(), }) if (!result.success) { throw new Error((result.error as string) ?? 'Unbekannter Fehler im ServiceWorker') } setPublicKeyPem(result.publicKeyPem as string) setExistingDeviceId(deviceId.trim()) setStep('done') } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e) // Ed25519 not supported if (msg.includes('Ed25519') || msg.includes('NotSupportedError') || msg.includes('algorithm')) { setErrorMsg( 'Dieser Browser unterstuetzt kein Ed25519. Bitte Chrome 113+, Firefox 130+ oder Safari 16.4+ verwenden.' ) } else { setErrorMsg(msg) } setStep('error') } } async function handleReset() { if (!swRegRef.current) return try { await sendMessageToSW(swRegRef.current, { type: 'CLEAR_CREDENTIALS' }) setExistingDeviceId(null) setPublicKeyPem(null) setStep('idle') setDeviceId(deviceIdParam) setErrorMsg(null) setCopied(false) } catch (e: unknown) { setErrorMsg(e instanceof Error ? e.message : 'Fehler beim Zuruecksetzen') } } async function handleCopy() { if (!publicKeyPem) return try { await navigator.clipboard.writeText(publicKeyPem) setCopied(true) setTimeout(() => setCopied(false), 2000) } catch { // fallback: select textarea } } const isGenerating = step === 'registering-sw' || step === 'generating' if (!supported) { return (
⚠️

Browser nicht unterstuetzt

Dieser Browser unterstuetzt kein Ed25519 oder ServiceWorker. Bitte Chrome 113+, Firefox 130+ oder Safari 16.4+ verwenden.

) } return (
{/* Header */}
🔐

Kiosk einrichten

Generiert ein Ed25519-Schluessel­paar fuer dieses Geraet. Der private Schluessel verlässt den Browser nie.

{/* Existing credentials warning */} {existingDeviceId && step !== 'done' && (
Achtung: Dieses Geraet hat bereits gespeicherte Credentials fuer Geraet {existingDeviceId}. Ein Neugenerieren ueberschreibt den vorhandenen Schluessel.{' '}
)} {/* Main card */}
{errorMsg && (
{errorMsg}
)} {step !== 'done' && ( )} {/* Success state */} {step === 'done' && publicKeyPem && (
Schluesselpaar generiert und gespeichert.
Public Key (PEM-Format)
                  {publicKeyPem}
                

Naechste Schritte:

  1. Public Key oben kopieren und an den Admin schicken (E-Mail reicht – kein Geheimnis).
  2. Admin registriert das Geraet auf dem Server:{' '} timemaster kiosk add --pubkey ...
  3. Admin gibt das Geraet in der Web-Oberflaeche frei.
  4. Kiosk-Modus starten.
Zum Kiosk →
)}
{/* Back link */} {step !== 'done' && (

Zurueck zum Login

)}
) }