Files
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

618 lines
21 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
// ==============================================
// Types
// ==============================================
interface TestResult {
name: string
description: string
expected: string
actual: string
status: 'passed' | 'failed' | 'pending' | 'skipped'
duration_ms: number
error_message?: string
}
interface TestCategoryResult {
category: string
display_name: string
description: string
why_important: string
tests: TestResult[]
passed: number
failed: number
total: number
duration_ms: number
}
interface FullTestResults {
timestamp: string
categories: TestCategoryResult[]
total_passed: number
total_failed: number
total_tests: number
duration_ms: number
}
type StepStatus = 'pending' | 'active' | 'completed' | 'failed'
interface WizardStep {
id: string
name: string
icon: string
status: StepStatus
category?: string
}
// ==============================================
// Constants
// ==============================================
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
const STEPS: WizardStep[] = [
{ id: 'welcome', name: 'Willkommen', icon: '👋', status: 'pending' },
{ id: 'request-id', name: 'Request-ID', icon: '🔑', status: 'pending', category: 'request-id' },
{ id: 'security-headers', name: 'Security Headers', icon: '🛡️', status: 'pending', category: 'security-headers' },
{ id: 'rate-limiter', name: 'Rate Limiting', icon: '⏱️', status: 'pending', category: 'rate-limiter' },
{ id: 'pii-redactor', name: 'PII Redaktion', icon: '🔒', status: 'pending', category: 'pii-redactor' },
{ id: 'input-gate', name: 'Input Validierung', icon: '🚧', status: 'pending', category: 'input-gate' },
{ id: 'cors', name: 'CORS', icon: '🌐', status: 'pending', category: 'cors' },
{ id: 'summary', name: 'Zusammenfassung', icon: '📊', status: 'pending' },
]
const EDUCATION_CONTENT: Record<string, { title: string; content: string[] }> = {
'welcome': {
title: 'Willkommen zum Middleware-Test-Wizard',
content: [
'Middleware ist die unsichtbare Schutzschicht Ihrer Anwendung. Sie verarbeitet jede Anfrage bevor sie Ihren Code erreicht - und jede Antwort bevor sie den Benutzer erreicht.',
'In diesem Wizard testen wir alle Middleware-Komponenten und Sie lernen dabei:',
'• Warum jede Komponente wichtig ist',
'• Welche Angriffe sie verhindert',
'• Wie Sie Probleme erkennen und beheben',
'Klicken Sie auf "Starten" um den Test-Wizard zu beginnen.',
],
},
'request-id': {
title: 'Request-ID & Distributed Tracing',
content: [
'Stellen Sie sich vor, ein Benutzer meldet einen Fehler. Ohne Request-ID muessen Sie Tausende von Log-Eintraegen durchsuchen. Mit Request-ID finden Sie den genauen Pfad der Anfrage durch alle Microservices in Sekunden.',
'Request-IDs sind essentiell fuer:',
'• Fehlersuche in verteilten Systemen',
'• Performance-Analyse',
'• Audit-Trails fuer Compliance',
],
},
'security-headers': {
title: 'Security Headers - Erste Verteidigungslinie',
content: [
'Security Headers sind Anweisungen an den Browser, wie er Ihre Seite schuetzen soll:',
'• X-Content-Type-Options: nosniff - Verhindert MIME-Sniffing Angriffe',
'• X-Frame-Options: DENY - Blockiert Clickjacking-Angriffe',
'• Content-Security-Policy - Stoppt XSS durch Whitelist erlaubter Quellen',
'• Strict-Transport-Security - Erzwingt HTTPS',
'OWASP empfiehlt diese Headers als Mindeststandard.',
],
},
'rate-limiter': {
title: 'Rate Limiting - Schutz vor Ueberflutung',
content: [
'Ohne Rate Limiting kann ein Angreifer:',
'• Passwort-Brute-Force durchfuehren (Millionen Versuche/Minute)',
'• Ihre Server mit Anfragen ueberfluten (DDoS)',
'• Teure API-Aufrufe missbrauchen',
'BreakPilot limitiert:',
'• 100 Anfragen/Minute pro IP (allgemein)',
'• 20 Anfragen/Minute fuer Auth-Endpoints',
'• 500 Anfragen/Minute pro authentifiziertem Benutzer',
],
},
'pii-redactor': {
title: 'PII Redaktion - DSGVO Pflicht',
content: [
'Personenbezogene Daten in Logs sind ein DSGVO-Verstoss:',
'• Email-Adressen: Bussgelder bis 20 Mio. EUR',
'• IP-Adressen: Gelten als personenbezogen (EuGH-Urteil)',
'• Telefonnummern: Direkter Personenbezug',
'Der PII Redactor erkennt automatisch:',
'• Email-Adressen → [EMAIL_REDACTED]',
'• IP-Adressen → [IP_REDACTED]',
'• Deutsche Telefonnummern → [PHONE_REDACTED]',
],
},
'input-gate': {
title: 'Input Gate - Der Tuersteher',
content: [
'Das Input Gate prueft jede Anfrage bevor sie Ihren Code erreicht:',
'• Groessenlimit: Blockiert ueberdimensionierte Payloads (DoS-Schutz)',
'• Content-Type: Erlaubt nur erwartete Formate',
'• Dateiendungen: Blockiert .exe, .bat, .sh Uploads',
'Ein Angreifer, der an Ihrem Code vorbeikommt, wird hier gestoppt.',
],
},
'cors': {
title: 'CORS - Kontrollierte Zugriffe',
content: [
'CORS bestimmt, welche Websites Ihre API aufrufen duerfen:',
'• Zu offen (*): Jede Website kann Ihre API missbrauchen',
'• Zu streng: Ihre eigene Frontend-App wird blockiert',
'BreakPilot erlaubt nur:',
'• https://breakpilot.app (Produktion)',
'• http://localhost:3000 (Development)',
],
},
'summary': {
title: 'Test-Zusammenfassung',
content: [
'Hier sehen Sie eine Uebersicht aller durchgefuehrten Tests:',
'• Anzahl bestandener Tests',
'• Fehlgeschlagene Tests mit Details',
'• Empfehlungen zur Behebung',
],
},
}
// ==============================================
// Components
// ==============================================
function WizardStepper({
steps,
currentStep,
onStepClick
}: {
steps: WizardStep[]
currentStep: number
onStepClick: (index: number) => void
}) {
return (
<div className="flex items-center justify-between mb-8 overflow-x-auto pb-4">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
<button
onClick={() => onStepClick(index)}
className={`flex flex-col items-center min-w-[80px] p-2 rounded-lg transition-colors ${
index === currentStep
? 'bg-blue-100 text-blue-700'
: step.status === 'completed'
? 'bg-green-100 text-green-700 cursor-pointer hover:bg-green-200'
: step.status === 'failed'
? 'bg-red-100 text-red-700 cursor-pointer hover:bg-red-200'
: 'text-gray-400'
}`}
disabled={index > currentStep && steps[index - 1]?.status === 'pending'}
>
<span className="text-2xl mb-1">{step.icon}</span>
<span className="text-xs font-medium text-center">{step.name}</span>
{step.status === 'completed' && <span className="text-xs text-green-600"></span>}
{step.status === 'failed' && <span className="text-xs text-red-600"></span>}
</button>
{index < steps.length - 1 && (
<div className={`h-0.5 w-8 mx-1 ${
index < currentStep ? 'bg-green-400' : 'bg-gray-200'
}`} />
)}
</div>
))}
</div>
)
}
function EducationCard({ stepId }: { stepId: string }) {
const content = EDUCATION_CONTENT[stepId]
if (!content) return null
return (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6">
<h3 className="text-lg font-semibold text-blue-800 mb-4 flex items-center">
<span className="mr-2">💡</span>
Warum ist das wichtig?
</h3>
<h4 className="text-md font-medium text-blue-700 mb-3">{content.title}</h4>
<div className="space-y-2 text-blue-900">
{content.content.map((line, index) => (
<p key={index} className={line.startsWith('•') ? 'ml-4' : ''}>
{line}
</p>
))}
</div>
</div>
)
}
function TestResultCard({ result }: { result: TestResult }) {
const statusColors = {
passed: 'bg-green-100 border-green-300 text-green-800',
failed: 'bg-red-100 border-red-300 text-red-800',
pending: 'bg-yellow-100 border-yellow-300 text-yellow-800',
skipped: 'bg-gray-100 border-gray-300 text-gray-600',
}
const statusIcons = {
passed: '✓',
failed: '✗',
pending: '○',
skipped: '',
}
return (
<div className={`border rounded-lg p-4 mb-3 ${statusColors[result.status]}`}>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium flex items-center">
<span className="mr-2">{statusIcons[result.status]}</span>
{result.name}
</h4>
<p className="text-sm opacity-80 mt-1">{result.description}</p>
</div>
<span className="text-xs opacity-60">{result.duration_ms.toFixed(0)}ms</span>
</div>
<div className="mt-3 grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Erwartet:</span>
<code className="block mt-1 bg-white bg-opacity-50 px-2 py-1 rounded text-xs">
{result.expected}
</code>
</div>
<div>
<span className="font-medium">Erhalten:</span>
<code className="block mt-1 bg-white bg-opacity-50 px-2 py-1 rounded text-xs">
{result.actual}
</code>
</div>
</div>
{result.error_message && (
<div className="mt-2 text-xs text-red-700 bg-red-50 p-2 rounded">
Fehler: {result.error_message}
</div>
)}
</div>
)
}
function TestSummaryCard({ results }: { results: FullTestResults }) {
const passRate = results.total_tests > 0
? ((results.total_passed / results.total_tests) * 100).toFixed(1)
: '0'
return (
<div className="bg-white rounded-lg shadow-lg p-6">
<h3 className="text-xl font-bold mb-4">Test-Ergebnisse</h3>
{/* Summary Stats */}
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-gray-50 rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-gray-700">{results.total_tests}</div>
<div className="text-sm text-gray-500">Gesamt</div>
</div>
<div className="bg-green-50 rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-green-600">{results.total_passed}</div>
<div className="text-sm text-green-600">Bestanden</div>
</div>
<div className="bg-red-50 rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-red-600">{results.total_failed}</div>
<div className="text-sm text-red-600">Fehlgeschlagen</div>
</div>
<div className={`rounded-lg p-4 text-center ${
parseFloat(passRate) >= 80 ? 'bg-green-50' : parseFloat(passRate) >= 50 ? 'bg-yellow-50' : 'bg-red-50'
}`}>
<div className={`text-3xl font-bold ${
parseFloat(passRate) >= 80 ? 'text-green-600' : parseFloat(passRate) >= 50 ? 'text-yellow-600' : 'text-red-600'
}`}>{passRate}%</div>
<div className="text-sm text-gray-500">Erfolgsrate</div>
</div>
</div>
{/* Category Results */}
<div className="space-y-4">
{results.categories.map((category) => (
<div key={category.category} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium">{category.display_name}</h4>
<span className={`text-sm px-2 py-1 rounded ${
category.failed === 0 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{category.passed}/{category.total} bestanden
</span>
</div>
<p className="text-sm text-gray-600">{category.description}</p>
</div>
))}
</div>
{/* Duration */}
<div className="mt-6 text-sm text-gray-500 text-right">
Gesamtdauer: {(results.duration_ms / 1000).toFixed(2)}s
</div>
</div>
)
}
// ==============================================
// Main Component
// ==============================================
export default function TestWizardPage() {
const [currentStep, setCurrentStep] = useState(0)
const [steps, setSteps] = useState<WizardStep[]>(STEPS)
const [categoryResults, setCategoryResults] = useState<Record<string, TestCategoryResult>>({})
const [fullResults, setFullResults] = useState<FullTestResults | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const currentStepData = steps[currentStep]
const isTestStep = currentStepData?.category !== undefined
const isWelcome = currentStepData?.id === 'welcome'
const isSummary = currentStepData?.id === 'summary'
const runCategoryTest = async (category: string) => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`${BACKEND_URL}/api/admin/ui-tests/${category}`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result: TestCategoryResult = await response.json()
setCategoryResults((prev) => ({ ...prev, [category]: result }))
// Update step status
setSteps((prev) =>
prev.map((step) =>
step.category === category
? { ...step, status: result.failed === 0 ? 'completed' : 'failed' }
: step
)
)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const runAllTests = async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`${BACKEND_URL}/api/admin/ui-tests/run-all`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const results: FullTestResults = await response.json()
setFullResults(results)
// Update all step statuses
setSteps((prev) =>
prev.map((step) => {
if (step.category) {
const catResult = results.categories.find((c) => c.category === step.category)
if (catResult) {
return { ...step, status: catResult.failed === 0 ? 'completed' : 'failed' }
}
}
return step
})
)
// Store category results
const newCategoryResults: Record<string, TestCategoryResult> = {}
results.categories.forEach((cat) => {
newCategoryResults[cat.category] = cat
})
setCategoryResults(newCategoryResults)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const goToNext = () => {
if (currentStep < steps.length - 1) {
setSteps((prev) =>
prev.map((step, idx) =>
idx === currentStep && step.status === 'pending'
? { ...step, status: 'completed' }
: step
)
)
setCurrentStep((prev) => prev + 1)
}
}
const goToPrev = () => {
if (currentStep > 0) {
setCurrentStep((prev) => prev - 1)
}
}
const handleStepClick = (index: number) => {
// Allow clicking on completed steps or the next available step
if (index <= currentStep || steps[index - 1]?.status !== 'pending') {
setCurrentStep(index)
}
}
return (
<div className="min-h-screen bg-gray-100 py-8">
<div className="max-w-4xl mx-auto px-4">
{/* Header */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-800">🧪 UI Test Wizard</h1>
<p className="text-gray-600 mt-1">
Interaktives Middleware-Testing mit Lernmaterial
</p>
</div>
<a
href="/admin/middleware"
className="text-blue-600 hover:text-blue-800 text-sm"
>
Zurueck zur Middleware-Konfiguration
</a>
</div>
</div>
{/* Stepper */}
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
<WizardStepper
steps={steps}
currentStep={currentStep}
onStepClick={handleStepClick}
/>
</div>
{/* Content */}
<div className="bg-white rounded-lg shadow-lg p-6">
{/* Step Header */}
<div className="flex items-center mb-6">
<span className="text-3xl mr-3">{currentStepData?.icon}</span>
<div>
<h2 className="text-xl font-bold text-gray-800">
Schritt {currentStep + 1}: {currentStepData?.name}
</h2>
<p className="text-gray-500 text-sm">
{currentStep + 1} von {steps.length}
</p>
</div>
</div>
{/* Education Card */}
<EducationCard stepId={currentStepData?.id || ''} />
{/* Error Display */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 mb-6">
<strong>Fehler:</strong> {error}
</div>
)}
{/* Welcome Step */}
{isWelcome && (
<div className="text-center py-8">
<button
onClick={goToNext}
className="bg-blue-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
🚀 Starten
</button>
</div>
)}
{/* Test Steps */}
{isTestStep && currentStepData?.category && (
<div>
{/* Run Test Button */}
{!categoryResults[currentStepData.category] && (
<div className="text-center py-6">
<button
onClick={() => runCategoryTest(currentStepData.category!)}
disabled={isLoading}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
isLoading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-600 text-white hover:bg-green-700'
}`}
>
{isLoading ? '⏳ Tests laufen...' : '▶️ Tests ausfuehren'}
</button>
</div>
)}
{/* Test Results */}
{categoryResults[currentStepData.category] && (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-700">Testergebnisse</h3>
<button
onClick={() => runCategoryTest(currentStepData.category!)}
disabled={isLoading}
className="text-sm text-blue-600 hover:text-blue-800"
>
🔄 Erneut ausfuehren
</button>
</div>
{categoryResults[currentStepData.category].tests.map((test, index) => (
<TestResultCard key={index} result={test} />
))}
</div>
)}
</div>
)}
{/* Summary Step */}
{isSummary && (
<div>
{!fullResults ? (
<div className="text-center py-8">
<p className="text-gray-600 mb-4">
Fuehren Sie alle Tests aus um eine Zusammenfassung zu sehen.
</p>
<button
onClick={runAllTests}
disabled={isLoading}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
isLoading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{isLoading ? '⏳ Alle Tests laufen...' : '🔬 Alle Tests ausfuehren'}
</button>
</div>
) : (
<TestSummaryCard results={fullResults} />
)}
</div>
)}
{/* Navigation */}
<div className="flex justify-between mt-8 pt-6 border-t">
<button
onClick={goToPrev}
disabled={currentStep === 0}
className={`px-6 py-2 rounded-lg transition-colors ${
currentStep === 0
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
Zurueck
</button>
{!isSummary && (
<button
onClick={goToNext}
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
Weiter
</button>
)}
</div>
</div>
{/* Footer Info */}
<div className="text-center text-gray-500 text-sm mt-6">
Diese Tests pruefen die Middleware-Konfiguration Ihrer Anwendung.
Bei Fragen wenden Sie sich an den Administrator.
</div>
</div>
</div>
)
}