fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
286
admin-v2/components/sdk/tom-generator/TOMGeneratorWizard.tsx
Normal file
286
admin-v2/components/sdk/tom-generator/TOMGeneratorWizard.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// TOM Generator Wizard Component
|
||||
// Main wizard container with step navigation
|
||||
// =============================================================================
|
||||
|
||||
import React from 'react'
|
||||
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
|
||||
import { TOM_GENERATOR_STEPS, TOMGeneratorStepId } from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
// =============================================================================
|
||||
// STEP INDICATOR
|
||||
// =============================================================================
|
||||
|
||||
interface StepIndicatorProps {
|
||||
stepId: TOMGeneratorStepId
|
||||
stepNumber: number
|
||||
title: string
|
||||
isActive: boolean
|
||||
isCompleted: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
function StepIndicator({
|
||||
stepNumber,
|
||||
title,
|
||||
isActive,
|
||||
isCompleted,
|
||||
onClick,
|
||||
}: StepIndicatorProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-3 p-3 rounded-lg transition-all w-full text-left ${
|
||||
isActive
|
||||
? 'bg-blue-50 border-2 border-blue-500'
|
||||
: isCompleted
|
||||
? 'bg-green-50 border border-green-300 hover:bg-green-100'
|
||||
: 'bg-gray-50 border border-gray-200 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
|
||||
isActive
|
||||
? 'bg-blue-500 text-white'
|
||||
: isCompleted
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-300 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
) : (
|
||||
stepNumber
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
isActive ? 'text-blue-700' : isCompleted ? 'text-green-700' : 'text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WIZARD NAVIGATION
|
||||
// =============================================================================
|
||||
|
||||
interface WizardNavigationProps {
|
||||
onPrevious: () => void
|
||||
onNext: () => void
|
||||
onSave: () => void
|
||||
canGoPrevious: boolean
|
||||
canGoNext: boolean
|
||||
isLastStep: boolean
|
||||
isSaving: boolean
|
||||
}
|
||||
|
||||
function WizardNavigation({
|
||||
onPrevious,
|
||||
onNext,
|
||||
onSave,
|
||||
canGoPrevious,
|
||||
canGoNext,
|
||||
isLastStep,
|
||||
isSaving,
|
||||
}: WizardNavigationProps) {
|
||||
return (
|
||||
<div className="flex justify-between items-center mt-8 pt-6 border-t">
|
||||
<button
|
||||
onClick={onPrevious}
|
||||
disabled={!canGoPrevious}
|
||||
className={`px-4 py-2 rounded-lg font-medium flex items-center gap-2 ${
|
||||
canGoPrevious
|
||||
? 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
: 'bg-gray-50 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Zurück
|
||||
</button>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
className="px-4 py-2 rounded-lg font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 flex items-center gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4" />
|
||||
</svg>
|
||||
)}
|
||||
Speichern
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={!canGoNext && !isLastStep}
|
||||
className={`px-4 py-2 rounded-lg font-medium flex items-center gap-2 ${
|
||||
canGoNext || isLastStep
|
||||
? 'bg-blue-600 text-white hover:bg-blue-700'
|
||||
: 'bg-blue-300 text-white cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isLastStep ? 'Abschließen' : 'Weiter'}
|
||||
{!isLastStep && (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROGRESS BAR
|
||||
// =============================================================================
|
||||
|
||||
interface ProgressBarProps {
|
||||
percentage: number
|
||||
}
|
||||
|
||||
function ProgressBar({ percentage }: ProgressBarProps) {
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between text-sm text-gray-600 mb-2">
|
||||
<span>Fortschritt</span>
|
||||
<span>{percentage}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-blue-500 transition-all duration-300"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN WIZARD COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
interface TOMGeneratorWizardProps {
|
||||
children: React.ReactNode
|
||||
showSidebar?: boolean
|
||||
showProgress?: boolean
|
||||
}
|
||||
|
||||
export function TOMGeneratorWizard({
|
||||
children,
|
||||
showSidebar = true,
|
||||
showProgress = true,
|
||||
}: TOMGeneratorWizardProps) {
|
||||
const {
|
||||
state,
|
||||
currentStepIndex,
|
||||
canGoNext,
|
||||
canGoPrevious,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
isStepCompleted,
|
||||
getCompletionPercentage,
|
||||
saveState,
|
||||
isLoading,
|
||||
} = useTOMGenerator()
|
||||
|
||||
const [isSaving, setIsSaving] = React.useState(false)
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await saveState()
|
||||
} catch (error) {
|
||||
console.error('Failed to save:', error)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isLastStep = currentStepIndex === TOM_GENERATOR_STEPS.length - 1
|
||||
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
{/* Sidebar */}
|
||||
{showSidebar && (
|
||||
<div className="w-72 flex-shrink-0">
|
||||
<div className="bg-white rounded-lg shadow-sm border p-4 sticky top-4">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Wizard-Schritte</h3>
|
||||
|
||||
{showProgress && <ProgressBar percentage={getCompletionPercentage()} />}
|
||||
|
||||
<div className="space-y-2">
|
||||
{TOM_GENERATOR_STEPS.map((step, index) => (
|
||||
<StepIndicator
|
||||
key={step.id}
|
||||
stepId={step.id}
|
||||
stepNumber={index + 1}
|
||||
title={step.title.de}
|
||||
isActive={state.currentStep === step.id}
|
||||
isCompleted={isStepCompleted(step.id)}
|
||||
onClick={() => goToStep(step.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="bg-white rounded-lg shadow-sm border p-6">
|
||||
{/* Step Header */}
|
||||
<div className="mb-6">
|
||||
<div className="text-sm text-blue-600 font-medium mb-1">
|
||||
Schritt {currentStepIndex + 1} von {TOM_GENERATOR_STEPS.length}
|
||||
</div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">
|
||||
{TOM_GENERATOR_STEPS[currentStepIndex].title.de}
|
||||
</h2>
|
||||
<p className="text-gray-600 mt-1">
|
||||
{TOM_GENERATOR_STEPS[currentStepIndex].description.de}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Step Content */}
|
||||
<div className="min-h-[400px]">{children}</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<WizardNavigation
|
||||
onPrevious={goToPreviousStep}
|
||||
onNext={goToNextStep}
|
||||
onSave={handleSave}
|
||||
canGoPrevious={canGoPrevious}
|
||||
canGoNext={canGoNext}
|
||||
isLastStep={isLastStep}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORTS
|
||||
// =============================================================================
|
||||
|
||||
export { StepIndicator, WizardNavigation, ProgressBar }
|
||||
19
admin-v2/components/sdk/tom-generator/index.ts
Normal file
19
admin-v2/components/sdk/tom-generator/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// =============================================================================
|
||||
// TOM Generator Components - Public API
|
||||
// =============================================================================
|
||||
|
||||
// Main Wizard
|
||||
export {
|
||||
TOMGeneratorWizard,
|
||||
StepIndicator,
|
||||
WizardNavigation,
|
||||
ProgressBar,
|
||||
} from './TOMGeneratorWizard'
|
||||
|
||||
// Step Components
|
||||
export { ScopeRolesStep } from './steps/ScopeRolesStep'
|
||||
export { DataCategoriesStep } from './steps/DataCategoriesStep'
|
||||
export { ArchitectureStep } from './steps/ArchitectureStep'
|
||||
export { SecurityProfileStep } from './steps/SecurityProfileStep'
|
||||
export { RiskProtectionStep } from './steps/RiskProtectionStep'
|
||||
export { ReviewExportStep } from './steps/ReviewExportStep'
|
||||
460
admin-v2/components/sdk/tom-generator/steps/ArchitectureStep.tsx
Normal file
460
admin-v2/components/sdk/tom-generator/steps/ArchitectureStep.tsx
Normal file
@@ -0,0 +1,460 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// Step 3: Architecture & Hosting
|
||||
// Hosting model, location, and provider configuration
|
||||
// =============================================================================
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
|
||||
import {
|
||||
ArchitectureProfile,
|
||||
HostingModel,
|
||||
HostingLocation,
|
||||
MultiTenancy,
|
||||
CloudProvider,
|
||||
} from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
const HOSTING_MODELS: { value: HostingModel; label: string; description: string; icon: string }[] = [
|
||||
{
|
||||
value: 'ON_PREMISE',
|
||||
label: 'On-Premise',
|
||||
description: 'Eigenes Rechenzentrum oder Co-Location',
|
||||
icon: '🏢',
|
||||
},
|
||||
{
|
||||
value: 'PRIVATE_CLOUD',
|
||||
label: 'Private Cloud',
|
||||
description: 'Dedizierte Cloud-Infrastruktur',
|
||||
icon: '☁️',
|
||||
},
|
||||
{
|
||||
value: 'PUBLIC_CLOUD',
|
||||
label: 'Public Cloud',
|
||||
description: 'AWS, Azure, GCP oder andere',
|
||||
icon: '🌐',
|
||||
},
|
||||
{
|
||||
value: 'HYBRID',
|
||||
label: 'Hybrid',
|
||||
description: 'Kombination aus On-Premise und Cloud',
|
||||
icon: '🔄',
|
||||
},
|
||||
]
|
||||
|
||||
const HOSTING_LOCATIONS: { value: HostingLocation; label: string; description: string }[] = [
|
||||
{ value: 'DE', label: 'Deutschland', description: 'Rechenzentrum in Deutschland' },
|
||||
{ value: 'EU', label: 'EU (nicht DE)', description: 'Innerhalb der EU, aber nicht in Deutschland' },
|
||||
{ value: 'EEA', label: 'EWR', description: 'Europäischer Wirtschaftsraum' },
|
||||
{ value: 'THIRD_COUNTRY_ADEQUATE', label: 'Drittland (Angemessenheit)', description: 'Mit Angemessenheitsbeschluss' },
|
||||
{ value: 'THIRD_COUNTRY', label: 'Drittland (andere)', description: 'Ohne Angemessenheitsbeschluss' },
|
||||
]
|
||||
|
||||
const MULTI_TENANCY_OPTIONS: { value: MultiTenancy; label: string; description: string }[] = [
|
||||
{ value: 'SINGLE_TENANT', label: 'Single-Tenant', description: 'Dedizierte Instanz pro Kunde' },
|
||||
{ value: 'MULTI_TENANT', label: 'Multi-Tenant', description: 'Geteilte Infrastruktur mit logischer Trennung' },
|
||||
{ value: 'DEDICATED', label: 'Dedicated', description: 'Dedizierte Hardware, aber gemeinsame Software' },
|
||||
]
|
||||
|
||||
const COMMON_CERTIFICATIONS = [
|
||||
'ISO 27001',
|
||||
'SOC 2 Type II',
|
||||
'C5',
|
||||
'TISAX',
|
||||
'PCI DSS',
|
||||
'HIPAA',
|
||||
'FedRAMP',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export function ArchitectureStep() {
|
||||
const { state, setArchitectureProfile, completeCurrentStep } = useTOMGenerator()
|
||||
|
||||
const [formData, setFormData] = useState<Partial<ArchitectureProfile>>({
|
||||
hostingModel: 'PUBLIC_CLOUD',
|
||||
hostingLocation: 'EU',
|
||||
providers: [],
|
||||
multiTenancy: 'MULTI_TENANT',
|
||||
hasSubprocessors: false,
|
||||
subprocessorCount: 0,
|
||||
encryptionAtRest: false,
|
||||
encryptionInTransit: false,
|
||||
})
|
||||
|
||||
const [newProvider, setNewProvider] = useState<Partial<CloudProvider>>({
|
||||
name: '',
|
||||
location: 'EU',
|
||||
certifications: [],
|
||||
})
|
||||
const [certificationInput, setCertificationInput] = useState('')
|
||||
|
||||
// Load existing data
|
||||
useEffect(() => {
|
||||
if (state.architectureProfile) {
|
||||
setFormData(state.architectureProfile)
|
||||
}
|
||||
}, [state.architectureProfile])
|
||||
|
||||
// Handle provider addition
|
||||
const addProvider = () => {
|
||||
if (newProvider.name?.trim()) {
|
||||
const provider: CloudProvider = {
|
||||
name: newProvider.name.trim(),
|
||||
location: newProvider.location || 'EU',
|
||||
certifications: newProvider.certifications || [],
|
||||
}
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
providers: [...(prev.providers || []), provider],
|
||||
}))
|
||||
setNewProvider({ name: '', location: 'EU', certifications: [] })
|
||||
}
|
||||
}
|
||||
|
||||
const removeProvider = (index: number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
providers: (prev.providers || []).filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle certification toggle
|
||||
const toggleCertification = (cert: string) => {
|
||||
setNewProvider((prev) => {
|
||||
const current = prev.certifications || []
|
||||
const updated = current.includes(cert)
|
||||
? current.filter((c) => c !== cert)
|
||||
: [...current, cert]
|
||||
return { ...prev, certifications: updated }
|
||||
})
|
||||
}
|
||||
|
||||
// Handle submit
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const profile: ArchitectureProfile = {
|
||||
hostingModel: formData.hostingModel || 'PUBLIC_CLOUD',
|
||||
hostingLocation: formData.hostingLocation || 'EU',
|
||||
providers: formData.providers || [],
|
||||
multiTenancy: formData.multiTenancy || 'MULTI_TENANT',
|
||||
hasSubprocessors: formData.hasSubprocessors || false,
|
||||
subprocessorCount: formData.subprocessorCount || 0,
|
||||
encryptionAtRest: formData.encryptionAtRest || false,
|
||||
encryptionInTransit: formData.encryptionInTransit || false,
|
||||
}
|
||||
|
||||
setArchitectureProfile(profile)
|
||||
completeCurrentStep(profile)
|
||||
}
|
||||
|
||||
const showProviderSection = formData.hostingModel !== 'ON_PREMISE'
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* Hosting Model */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Hosting-Modell</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Wie wird Ihre Infrastruktur betrieben?
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{HOSTING_MODELS.map((model) => (
|
||||
<label
|
||||
key={model.value}
|
||||
className={`relative flex items-start p-4 border rounded-lg cursor-pointer transition-all ${
|
||||
formData.hostingModel === model.value
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="hostingModel"
|
||||
value={model.value}
|
||||
checked={formData.hostingModel === model.value}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hostingModel: e.target.value as HostingModel }))}
|
||||
className="sr-only"
|
||||
/>
|
||||
<span className="text-2xl mr-3">{model.icon}</span>
|
||||
<div className="flex-1">
|
||||
<span className="font-medium text-gray-900">{model.label}</span>
|
||||
<p className="text-sm text-gray-500 mt-1">{model.description}</p>
|
||||
</div>
|
||||
{formData.hostingModel === model.value && (
|
||||
<svg className="w-5 h-5 text-blue-500 absolute top-3 right-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hosting Location */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Primärer Hosting-Standort</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Wo werden die Daten primär gespeichert?
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{HOSTING_LOCATIONS.map((location) => (
|
||||
<label
|
||||
key={location.value}
|
||||
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
|
||||
formData.hostingLocation === location.value
|
||||
? location.value.startsWith('THIRD_COUNTRY')
|
||||
? 'border-amber-500 bg-amber-50'
|
||||
: 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="hostingLocation"
|
||||
value={location.value}
|
||||
checked={formData.hostingLocation === location.value}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hostingLocation: e.target.value as HostingLocation }))}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="font-medium text-gray-900">{location.label}</span>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{location.description}</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{formData.hostingLocation?.startsWith('THIRD_COUNTRY') && (
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-lg p-4">
|
||||
<p className="text-sm text-amber-800">
|
||||
<strong>Hinweis:</strong> Bei Hosting in Drittländern sind zusätzliche Garantien nach Art. 46 DSGVO
|
||||
erforderlich (z.B. Standardvertragsklauseln).
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cloud Providers */}
|
||||
{showProviderSection && (
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Cloud-Provider / Rechenzentren</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Fügen Sie Ihre genutzten Provider hinzu.
|
||||
</p>
|
||||
|
||||
{/* Existing providers */}
|
||||
{formData.providers && formData.providers.length > 0 && (
|
||||
<div className="space-y-2 mb-4">
|
||||
{formData.providers.map((provider, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center justify-between p-3 bg-gray-50 border rounded-lg"
|
||||
>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900">{provider.name}</span>
|
||||
<span className="text-sm text-gray-500 ml-2">({provider.location})</span>
|
||||
{provider.certifications.length > 0 && (
|
||||
<div className="flex gap-1 mt-1">
|
||||
{provider.certifications.map((cert) => (
|
||||
<span
|
||||
key={cert}
|
||||
className="px-2 py-0.5 text-xs bg-green-100 text-green-800 rounded"
|
||||
>
|
||||
{cert}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeProvider(index)}
|
||||
className="text-gray-400 hover:text-red-500"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add new provider */}
|
||||
<div className="border rounded-lg p-4 bg-gray-50">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Provider-Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newProvider.name || ''}
|
||||
onChange={(e) => setNewProvider((prev) => ({ ...prev, name: e.target.value }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. AWS, Azure, Hetzner"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Standort
|
||||
</label>
|
||||
<select
|
||||
value={newProvider.location || 'EU'}
|
||||
onChange={(e) => setNewProvider((prev) => ({ ...prev, location: e.target.value as HostingLocation }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{HOSTING_LOCATIONS.map((loc) => (
|
||||
<option key={loc.value} value={loc.value}>{loc.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Zertifizierungen
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{COMMON_CERTIFICATIONS.map((cert) => (
|
||||
<button
|
||||
key={cert}
|
||||
type="button"
|
||||
onClick={() => toggleCertification(cert)}
|
||||
className={`px-3 py-1 text-sm rounded-full border transition-all ${
|
||||
newProvider.certifications?.includes(cert)
|
||||
? 'bg-green-100 border-green-300 text-green-800'
|
||||
: 'bg-white border-gray-300 text-gray-600 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
{cert}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addProvider}
|
||||
disabled={!newProvider.name?.trim()}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||
>
|
||||
Provider hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Multi-Tenancy */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Mandantentrennung</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Wie ist die Trennung zwischen verschiedenen Mandanten/Kunden umgesetzt?
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{MULTI_TENANCY_OPTIONS.map((option) => (
|
||||
<label
|
||||
key={option.value}
|
||||
className={`relative flex items-start p-4 border rounded-lg cursor-pointer transition-all ${
|
||||
formData.multiTenancy === option.value
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="multiTenancy"
|
||||
value={option.value}
|
||||
checked={formData.multiTenancy === option.value}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, multiTenancy: e.target.value as MultiTenancy }))}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="font-medium text-gray-900">{option.label}</span>
|
||||
<p className="text-sm text-gray-500 mt-1">{option.description}</p>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subprocessors */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Unterauftragsverarbeiter</h3>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasSubprocessors || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasSubprocessors: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-gray-700">
|
||||
Wir setzen Unterauftragsverarbeiter ein
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{formData.hasSubprocessors && (
|
||||
<div className="pl-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Anzahl der Unterauftragsverarbeiter
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.subprocessorCount || 0}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, subprocessorCount: parseInt(e.target.value) || 0 }))}
|
||||
className="w-32 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Encryption */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Verschlüsselung</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.encryptionAtRest || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, encryptionAtRest: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Verschlüsselung ruhender Daten (at rest)</span>
|
||||
<p className="text-sm text-gray-500">Daten werden verschlüsselt gespeichert</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.encryptionInTransit || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, encryptionInTransit: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Transportverschlüsselung (in transit)</span>
|
||||
<p className="text-sm text-gray-500">TLS/SSL für alle Datenübertragungen</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ArchitectureStep
|
||||
@@ -0,0 +1,374 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// Step 2: Data Categories
|
||||
// Data categories and data subjects selection
|
||||
// =============================================================================
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
|
||||
import {
|
||||
DataProfile,
|
||||
DataCategory,
|
||||
DataSubject,
|
||||
DataVolume,
|
||||
DATA_CATEGORIES_METADATA,
|
||||
DATA_SUBJECTS_METADATA,
|
||||
} from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
const DATA_VOLUMES: { value: DataVolume; label: string; description: string }[] = [
|
||||
{ value: 'LOW', label: 'Niedrig', description: '< 1.000 Datensätze' },
|
||||
{ value: 'MEDIUM', label: 'Mittel', description: '1.000 - 100.000 Datensätze' },
|
||||
{ value: 'HIGH', label: 'Hoch', description: '100.000 - 1 Mio. Datensätze' },
|
||||
{ value: 'VERY_HIGH', label: 'Sehr hoch', description: '> 1 Mio. Datensätze' },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export function DataCategoriesStep() {
|
||||
const { state, setDataProfile, completeCurrentStep } = useTOMGenerator()
|
||||
|
||||
const [formData, setFormData] = useState<Partial<DataProfile>>({
|
||||
categories: [],
|
||||
subjects: [],
|
||||
hasSpecialCategories: false,
|
||||
processesMinors: false,
|
||||
dataVolume: 'MEDIUM',
|
||||
thirdCountryTransfers: false,
|
||||
thirdCountryList: [],
|
||||
})
|
||||
|
||||
const [thirdCountryInput, setThirdCountryInput] = useState('')
|
||||
|
||||
// Load existing data
|
||||
useEffect(() => {
|
||||
if (state.dataProfile) {
|
||||
setFormData(state.dataProfile)
|
||||
}
|
||||
}, [state.dataProfile])
|
||||
|
||||
// Check for special categories
|
||||
useEffect(() => {
|
||||
const hasSpecial = formData.categories?.some((cat) => {
|
||||
const meta = DATA_CATEGORIES_METADATA.find((m) => m.id === cat)
|
||||
return meta?.isSpecialCategory
|
||||
})
|
||||
setFormData((prev) => ({ ...prev, hasSpecialCategories: hasSpecial || false }))
|
||||
}, [formData.categories])
|
||||
|
||||
// Check for minors
|
||||
useEffect(() => {
|
||||
const hasMinors = formData.subjects?.includes('MINORS')
|
||||
setFormData((prev) => ({ ...prev, processesMinors: hasMinors || false }))
|
||||
}, [formData.subjects])
|
||||
|
||||
// Handle category toggle
|
||||
const toggleCategory = (category: DataCategory) => {
|
||||
setFormData((prev) => {
|
||||
const current = prev.categories || []
|
||||
const updated = current.includes(category)
|
||||
? current.filter((c) => c !== category)
|
||||
: [...current, category]
|
||||
return { ...prev, categories: updated }
|
||||
})
|
||||
}
|
||||
|
||||
// Handle subject toggle
|
||||
const toggleSubject = (subject: DataSubject) => {
|
||||
setFormData((prev) => {
|
||||
const current = prev.subjects || []
|
||||
const updated = current.includes(subject)
|
||||
? current.filter((s) => s !== subject)
|
||||
: [...current, subject]
|
||||
return { ...prev, subjects: updated }
|
||||
})
|
||||
}
|
||||
|
||||
// Handle third country addition
|
||||
const addThirdCountry = () => {
|
||||
if (thirdCountryInput.trim()) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
thirdCountryList: [...(prev.thirdCountryList || []), thirdCountryInput.trim()],
|
||||
}))
|
||||
setThirdCountryInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeThirdCountry = (index: number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
thirdCountryList: (prev.thirdCountryList || []).filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle submit
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const profile: DataProfile = {
|
||||
categories: formData.categories || [],
|
||||
subjects: formData.subjects || [],
|
||||
hasSpecialCategories: formData.hasSpecialCategories || false,
|
||||
processesMinors: formData.processesMinors || false,
|
||||
dataVolume: formData.dataVolume || 'MEDIUM',
|
||||
thirdCountryTransfers: formData.thirdCountryTransfers || false,
|
||||
thirdCountryList: formData.thirdCountryList || [],
|
||||
}
|
||||
|
||||
setDataProfile(profile)
|
||||
completeCurrentStep(profile)
|
||||
}
|
||||
|
||||
const selectedSpecialCategories = (formData.categories || []).filter((cat) => {
|
||||
const meta = DATA_CATEGORIES_METADATA.find((m) => m.id === cat)
|
||||
return meta?.isSpecialCategory
|
||||
})
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* Data Categories */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Datenkategorien</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Wählen Sie alle Kategorien personenbezogener Daten, die Sie verarbeiten.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{DATA_CATEGORIES_METADATA.map((category) => (
|
||||
<label
|
||||
key={category.id}
|
||||
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
|
||||
formData.categories?.includes(category.id)
|
||||
? category.isSpecialCategory
|
||||
? 'border-amber-500 bg-amber-50'
|
||||
: 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.categories?.includes(category.id) || false}
|
||||
onChange={() => toggleCategory(category.id)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{category.name.de}</span>
|
||||
{category.isSpecialCategory && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-amber-100 text-amber-800 rounded">
|
||||
Art. 9
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{formData.categories?.includes(category.id) && (
|
||||
<svg className="w-5 h-5 text-blue-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Special Categories Warning */}
|
||||
{selectedSpecialCategories.length > 0 && (
|
||||
<div className="mt-4 bg-amber-50 border border-amber-200 rounded-lg p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-medium text-amber-900">Besondere Kategorien nach Art. 9 DSGVO</h4>
|
||||
<p className="text-sm text-amber-700 mt-1">
|
||||
Sie verarbeiten besonders schützenswerte Daten. Dies erfordert zusätzliche Schutzmaßnahmen
|
||||
und möglicherweise eine Datenschutz-Folgenabschätzung (DSFA).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Data Subjects */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Betroffene Personen</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Wählen Sie alle Personengruppen, deren Daten Sie verarbeiten.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{DATA_SUBJECTS_METADATA.map((subject) => (
|
||||
<label
|
||||
key={subject.id}
|
||||
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
|
||||
formData.subjects?.includes(subject.id)
|
||||
? subject.isVulnerable
|
||||
? 'border-red-500 bg-red-50'
|
||||
: 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.subjects?.includes(subject.id) || false}
|
||||
onChange={() => toggleSubject(subject.id)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-gray-900">{subject.name.de}</span>
|
||||
{subject.isVulnerable && (
|
||||
<span className="px-2 py-0.5 text-xs font-medium bg-red-100 text-red-800 rounded">
|
||||
Schutzbedürftig
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{formData.subjects?.includes(subject.id) && (
|
||||
<svg className="w-5 h-5 text-blue-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Minors Warning */}
|
||||
{formData.subjects?.includes('MINORS') && (
|
||||
<div className="mt-4 bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-medium text-red-900">Verarbeitung von Minderjährigen-Daten</h4>
|
||||
<p className="text-sm text-red-700 mt-1">
|
||||
Die Verarbeitung von Daten Minderjähriger erfordert besondere Schutzmaßnahmen nach Art. 8 DSGVO
|
||||
und erhöhte Sorgfaltspflichten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Data Volume */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Datenvolumen</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Geschätzte Anzahl der Datensätze, die Sie verarbeiten.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{DATA_VOLUMES.map((volume) => (
|
||||
<label
|
||||
key={volume.value}
|
||||
className={`relative flex flex-col items-center p-4 border rounded-lg cursor-pointer transition-all text-center ${
|
||||
formData.dataVolume === volume.value
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="dataVolume"
|
||||
value={volume.value}
|
||||
checked={formData.dataVolume === volume.value}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, dataVolume: e.target.value as DataVolume }))}
|
||||
className="sr-only"
|
||||
/>
|
||||
<span className="font-medium text-gray-900">{volume.label}</span>
|
||||
<span className="text-xs text-gray-500 mt-1">{volume.description}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Third Country Transfers */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Drittlandübermittlungen</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Werden Daten in Länder außerhalb der EU/EWR übermittelt?
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.thirdCountryTransfers || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, thirdCountryTransfers: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span className="text-gray-700">
|
||||
Ja, wir übermitteln Daten in Drittländer
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{formData.thirdCountryTransfers && (
|
||||
<div className="pl-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Zielländer
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={thirdCountryInput}
|
||||
onChange={(e) => setThirdCountryInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addThirdCountry())}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="z.B. USA, Schweiz, UK"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addThirdCountry}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
{formData.thirdCountryList && formData.thirdCountryList.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{formData.thirdCountryList.map((country, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 text-gray-800 rounded-full text-sm"
|
||||
>
|
||||
{country}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeThirdCountry(index)}
|
||||
className="hover:text-gray-600"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.thirdCountryList?.includes('USA') && (
|
||||
<div className="mt-3 bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Hinweis:</strong> Für Übermittlungen in die USA ist seit dem EU-US Data Privacy Framework
|
||||
ein Angemessenheitsbeschluss vorhanden. Prüfen Sie, ob Ihr US-Partner zertifiziert ist.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataCategoriesStep
|
||||
593
admin-v2/components/sdk/tom-generator/steps/ReviewExportStep.tsx
Normal file
593
admin-v2/components/sdk/tom-generator/steps/ReviewExportStep.tsx
Normal file
@@ -0,0 +1,593 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// Step 6: Review & Export
|
||||
// Summary, derived TOMs table, gap analysis, and export
|
||||
// =============================================================================
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
|
||||
import { CONTROL_CATEGORIES } from '@/lib/sdk/tom-generator/types'
|
||||
import { getControlById } from '@/lib/sdk/tom-generator/controls/loader'
|
||||
import { generateDOCXBlob } from '@/lib/sdk/tom-generator/export/docx'
|
||||
import { generatePDFBlob } from '@/lib/sdk/tom-generator/export/pdf'
|
||||
import { generateZIPBlob } from '@/lib/sdk/tom-generator/export/zip'
|
||||
|
||||
// =============================================================================
|
||||
// SUMMARY CARD COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
interface SummaryCardProps {
|
||||
title: string
|
||||
value: string | number
|
||||
description?: string
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger'
|
||||
}
|
||||
|
||||
function SummaryCard({ title, value, description, variant = 'default' }: SummaryCardProps) {
|
||||
const colors = {
|
||||
default: 'bg-gray-100 text-gray-800',
|
||||
success: 'bg-green-100 text-green-800',
|
||||
warning: 'bg-yellow-100 text-yellow-800',
|
||||
danger: 'bg-red-100 text-red-800',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg p-4 ${colors[variant]}`}>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
<div className="font-medium">{title}</div>
|
||||
{description && <div className="text-sm opacity-75 mt-1">{description}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TOMS TABLE COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
function TOMsTable() {
|
||||
const { state } = useTOMGenerator()
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('all')
|
||||
const [selectedApplicability, setSelectedApplicability] = useState<string>('all')
|
||||
|
||||
const filteredTOMs = state.derivedTOMs.filter((tom) => {
|
||||
const control = getControlById(tom.controlId)
|
||||
const categoryMatch = selectedCategory === 'all' || control?.category === selectedCategory
|
||||
const applicabilityMatch = selectedApplicability === 'all' || tom.applicability === selectedApplicability
|
||||
return categoryMatch && applicabilityMatch
|
||||
})
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const badges: Record<string, { bg: string; text: string }> = {
|
||||
IMPLEMENTED: { bg: 'bg-green-100', text: 'text-green-800' },
|
||||
PARTIAL: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
|
||||
NOT_IMPLEMENTED: { bg: 'bg-red-100', text: 'text-red-800' },
|
||||
}
|
||||
const labels: Record<string, string> = {
|
||||
IMPLEMENTED: 'Umgesetzt',
|
||||
PARTIAL: 'Teilweise',
|
||||
NOT_IMPLEMENTED: 'Offen',
|
||||
}
|
||||
const config = badges[status] || badges.NOT_IMPLEMENTED
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
|
||||
{labels[status] || status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const getApplicabilityBadge = (applicability: string) => {
|
||||
const badges: Record<string, { bg: string; text: string }> = {
|
||||
REQUIRED: { bg: 'bg-red-100', text: 'text-red-800' },
|
||||
RECOMMENDED: { bg: 'bg-blue-100', text: 'text-blue-800' },
|
||||
OPTIONAL: { bg: 'bg-gray-100', text: 'text-gray-800' },
|
||||
NOT_APPLICABLE: { bg: 'bg-gray-50', text: 'text-gray-500' },
|
||||
}
|
||||
const labels: Record<string, string> = {
|
||||
REQUIRED: 'Erforderlich',
|
||||
RECOMMENDED: 'Empfohlen',
|
||||
OPTIONAL: 'Optional',
|
||||
NOT_APPLICABLE: 'N/A',
|
||||
}
|
||||
const config = badges[applicability] || badges.OPTIONAL
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${config.bg} ${config.text}`}>
|
||||
{labels[applicability] || applicability}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Kategorie</label>
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">Alle Kategorien</option>
|
||||
{CONTROL_CATEGORIES.map((cat) => (
|
||||
<option key={cat.id} value={cat.id}>{cat.name.de}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Anwendbarkeit</label>
|
||||
<select
|
||||
value={selectedApplicability}
|
||||
onChange={(e) => setSelectedApplicability(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="all">Alle</option>
|
||||
<option value="REQUIRED">Erforderlich</option>
|
||||
<option value="RECOMMENDED">Empfohlen</option>
|
||||
<option value="OPTIONAL">Optional</option>
|
||||
<option value="NOT_APPLICABLE">Nicht anwendbar</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto border rounded-lg">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
ID
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Maßnahme
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Anwendbarkeit
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Nachweise
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredTOMs.map((tom) => (
|
||||
<tr key={tom.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3 text-sm font-mono text-gray-900">
|
||||
{tom.controlId}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-sm font-medium text-gray-900">{tom.name}</div>
|
||||
<div className="text-xs text-gray-500 max-w-md truncate">{tom.applicabilityReason}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{getApplicabilityBadge(tom.applicability)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{getStatusBadge(tom.implementationStatus)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-500">
|
||||
{tom.linkedEvidence.length > 0 ? (
|
||||
<span className="text-green-600">{tom.linkedEvidence.length} Dok.</span>
|
||||
) : tom.evidenceGaps.length > 0 ? (
|
||||
<span className="text-red-600">{tom.evidenceGaps.length} fehlen</span>
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-sm text-gray-500">
|
||||
{filteredTOMs.length} von {state.derivedTOMs.length} Maßnahmen angezeigt
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GAP ANALYSIS PANEL
|
||||
// =============================================================================
|
||||
|
||||
function GapAnalysisPanel() {
|
||||
const { state, runGapAnalysis } = useTOMGenerator()
|
||||
|
||||
useEffect(() => {
|
||||
if (!state.gapAnalysis && state.derivedTOMs.length > 0) {
|
||||
runGapAnalysis()
|
||||
}
|
||||
}, [state.derivedTOMs, state.gapAnalysis, runGapAnalysis])
|
||||
|
||||
if (!state.gapAnalysis) {
|
||||
return (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2" />
|
||||
Lückenanalyse wird durchgeführt...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { overallScore, missingControls, partialControls, recommendations } = state.gapAnalysis
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 80) return 'text-green-600'
|
||||
if (score >= 50) return 'text-yellow-600'
|
||||
return 'text-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Score */}
|
||||
<div className="text-center">
|
||||
<div className={`text-5xl font-bold ${getScoreColor(overallScore)}`}>
|
||||
{overallScore}%
|
||||
</div>
|
||||
<div className="text-gray-600 mt-1">Compliance Score</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="h-4 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-500 ${
|
||||
overallScore >= 80 ? 'bg-green-500' : overallScore >= 50 ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${overallScore}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Missing Controls */}
|
||||
{missingControls.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">
|
||||
Fehlende Maßnahmen ({missingControls.length})
|
||||
</h4>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{missingControls.map((mc) => {
|
||||
const control = getControlById(mc.controlId)
|
||||
return (
|
||||
<div key={mc.controlId} className="flex items-center justify-between p-2 bg-red-50 rounded-lg">
|
||||
<div>
|
||||
<span className="font-mono text-sm text-gray-600">{mc.controlId}</span>
|
||||
<span className="ml-2 text-sm text-gray-900">{control?.name.de}</span>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 text-xs rounded ${
|
||||
mc.priority === 'CRITICAL' ? 'bg-red-200 text-red-800' :
|
||||
mc.priority === 'HIGH' ? 'bg-orange-200 text-orange-800' :
|
||||
'bg-gray-200 text-gray-800'
|
||||
}`}>
|
||||
{mc.priority}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Partial Controls */}
|
||||
{partialControls.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">
|
||||
Teilweise umgesetzt ({partialControls.length})
|
||||
</h4>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{partialControls.map((pc) => {
|
||||
const control = getControlById(pc.controlId)
|
||||
return (
|
||||
<div key={pc.controlId} className="p-2 bg-yellow-50 rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm text-gray-600">{pc.controlId}</span>
|
||||
<span className="text-sm text-gray-900">{control?.name.de}</span>
|
||||
</div>
|
||||
<div className="text-xs text-yellow-700 mt-1">
|
||||
Fehlend: {pc.missingAspects.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommendations */}
|
||||
{recommendations.length > 0 && (
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900 mb-2">Empfehlungen</h4>
|
||||
<ul className="space-y-2">
|
||||
{recommendations.map((rec, index) => (
|
||||
<li key={index} className="flex items-start gap-2 text-sm text-gray-600">
|
||||
<svg className="w-4 h-4 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{rec}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXPORT PANEL
|
||||
// =============================================================================
|
||||
|
||||
function ExportPanel() {
|
||||
const { state, addExport } = useTOMGenerator()
|
||||
const [isExporting, setIsExporting] = useState<string | null>(null)
|
||||
|
||||
const handleExport = async (format: 'docx' | 'pdf' | 'json' | 'zip') => {
|
||||
setIsExporting(format)
|
||||
try {
|
||||
let blob: Blob
|
||||
let filename: string
|
||||
|
||||
switch (format) {
|
||||
case 'docx':
|
||||
blob = await generateDOCXBlob(state, { language: 'de' })
|
||||
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.docx`
|
||||
break
|
||||
case 'pdf':
|
||||
blob = await generatePDFBlob(state, { language: 'de' })
|
||||
filename = `TOM-Dokumentation-${new Date().toISOString().split('T')[0]}.pdf`
|
||||
break
|
||||
case 'json':
|
||||
blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' })
|
||||
filename = `TOM-Export-${new Date().toISOString().split('T')[0]}.json`
|
||||
break
|
||||
case 'zip':
|
||||
blob = await generateZIPBlob(state, { language: 'de' })
|
||||
filename = `TOM-Package-${new Date().toISOString().split('T')[0]}.zip`
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// Download
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = filename
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
// Record export
|
||||
addExport({
|
||||
id: `export-${Date.now()}`,
|
||||
format: format.toUpperCase() as 'DOCX' | 'PDF' | 'JSON' | 'ZIP',
|
||||
generatedAt: new Date(),
|
||||
filename,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error)
|
||||
} finally {
|
||||
setIsExporting(null)
|
||||
}
|
||||
}
|
||||
|
||||
const exportFormats = [
|
||||
{ id: 'docx', label: 'Word (.docx)', icon: '📄', description: 'Bearbeitbares Dokument' },
|
||||
{ id: 'pdf', label: 'PDF', icon: '📕', description: 'Druckversion' },
|
||||
{ id: 'json', label: 'JSON', icon: '💾', description: 'Maschinelles Format' },
|
||||
{ id: 'zip', label: 'ZIP-Paket', icon: '📦', description: 'Vollständiges Paket' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{exportFormats.map((format) => (
|
||||
<button
|
||||
key={format.id}
|
||||
onClick={() => handleExport(format.id as 'docx' | 'pdf' | 'json' | 'zip')}
|
||||
disabled={isExporting !== null}
|
||||
className={`p-4 border rounded-lg text-center transition-all ${
|
||||
isExporting === format.id
|
||||
? 'bg-blue-50 border-blue-300'
|
||||
: 'hover:bg-gray-50 hover:border-gray-300'
|
||||
} disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
<div className="text-3xl mb-2">{format.icon}</div>
|
||||
<div className="font-medium text-gray-900">{format.label}</div>
|
||||
<div className="text-xs text-gray-500">{format.description}</div>
|
||||
{isExporting === format.id && (
|
||||
<div className="mt-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mx-auto" />
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Export History */}
|
||||
{state.exports.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h4 className="font-medium text-gray-900 mb-2">Letzte Exporte</h4>
|
||||
<div className="space-y-2">
|
||||
{state.exports.slice(-5).reverse().map((exp) => (
|
||||
<div key={exp.id} className="flex items-center justify-between p-2 bg-gray-50 rounded-lg text-sm">
|
||||
<span className="font-medium">{exp.filename}</span>
|
||||
<span className="text-gray-500">
|
||||
{new Date(exp.generatedAt).toLocaleString('de-DE')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MAIN COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export function ReviewExportStep() {
|
||||
const { state, deriveTOMs, completeCurrentStep } = useTOMGenerator()
|
||||
const [activeTab, setActiveTab] = useState<'summary' | 'toms' | 'gaps' | 'export'>('summary')
|
||||
|
||||
// Derive TOMs if not already done
|
||||
useEffect(() => {
|
||||
if (state.derivedTOMs.length === 0 && state.companyProfile && state.dataProfile) {
|
||||
deriveTOMs()
|
||||
}
|
||||
}, [state, deriveTOMs])
|
||||
|
||||
// Mark step as complete when viewing
|
||||
useEffect(() => {
|
||||
completeCurrentStep({ reviewed: true })
|
||||
}, [completeCurrentStep])
|
||||
|
||||
// Statistics
|
||||
const stats = {
|
||||
totalTOMs: state.derivedTOMs.length,
|
||||
required: state.derivedTOMs.filter((t) => t.applicability === 'REQUIRED').length,
|
||||
implemented: state.derivedTOMs.filter((t) => t.implementationStatus === 'IMPLEMENTED').length,
|
||||
partial: state.derivedTOMs.filter((t) => t.implementationStatus === 'PARTIAL').length,
|
||||
documents: state.documents.length,
|
||||
score: state.gapAnalysis?.overallScore ?? 0,
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'summary', label: 'Zusammenfassung' },
|
||||
{ id: 'toms', label: 'TOMs-Tabelle' },
|
||||
{ id: 'gaps', label: 'Lückenanalyse' },
|
||||
{ id: 'export', label: 'Export' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Tabs */}
|
||||
<div className="border-b">
|
||||
<nav className="flex space-x-8">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
||||
className={`py-3 px-1 border-b-2 font-medium text-sm transition-all ${
|
||||
activeTab === tab.id
|
||||
? 'border-blue-500 text-blue-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="min-h-[400px]">
|
||||
{activeTab === 'summary' && (
|
||||
<div className="space-y-6">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
<SummaryCard
|
||||
title="Gesamt TOMs"
|
||||
value={stats.totalTOMs}
|
||||
variant="default"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Erforderlich"
|
||||
value={stats.required}
|
||||
variant="danger"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Umgesetzt"
|
||||
value={stats.implemented}
|
||||
variant="success"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Teilweise"
|
||||
value={stats.partial}
|
||||
variant="warning"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Dokumente"
|
||||
value={stats.documents}
|
||||
variant="default"
|
||||
/>
|
||||
<SummaryCard
|
||||
title="Score"
|
||||
value={`${stats.score}%`}
|
||||
variant={stats.score >= 80 ? 'success' : stats.score >= 50 ? 'warning' : 'danger'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Profile Summaries */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Company */}
|
||||
{state.companyProfile && (
|
||||
<div className="bg-white border rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">Unternehmen</h4>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">Name:</dt>
|
||||
<dd className="text-gray-900">{state.companyProfile.name}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">Branche:</dt>
|
||||
<dd className="text-gray-900">{state.companyProfile.industry}</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">Rolle:</dt>
|
||||
<dd className="text-gray-900">{state.companyProfile.role}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Risk */}
|
||||
{state.riskProfile && (
|
||||
<div className="bg-white border rounded-lg p-4">
|
||||
<h4 className="font-medium text-gray-900 mb-3">Schutzbedarf</h4>
|
||||
<dl className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">Level:</dt>
|
||||
<dd className={`font-medium ${
|
||||
state.riskProfile.protectionLevel === 'VERY_HIGH' ? 'text-red-600' :
|
||||
state.riskProfile.protectionLevel === 'HIGH' ? 'text-yellow-600' : 'text-green-600'
|
||||
}`}>
|
||||
{state.riskProfile.protectionLevel}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">DSFA erforderlich:</dt>
|
||||
<dd className={state.riskProfile.dsfaRequired ? 'text-red-600 font-medium' : 'text-gray-900'}>
|
||||
{state.riskProfile.dsfaRequired ? 'Ja' : 'Nein'}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<dt className="text-gray-500">CIA (V/I/V):</dt>
|
||||
<dd className="text-gray-900">
|
||||
{state.riskProfile.ciaAssessment.confidentiality}/
|
||||
{state.riskProfile.ciaAssessment.integrity}/
|
||||
{state.riskProfile.ciaAssessment.availability}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'toms' && <TOMsTable />}
|
||||
|
||||
{activeTab === 'gaps' && <GapAnalysisPanel />}
|
||||
|
||||
{activeTab === 'export' && <ExportPanel />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReviewExportStep
|
||||
@@ -0,0 +1,422 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// Step 5: Risk & Protection Level
|
||||
// CIA assessment and protection level determination
|
||||
// =============================================================================
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
|
||||
import {
|
||||
RiskProfile,
|
||||
CIARating,
|
||||
ProtectionLevel,
|
||||
calculateProtectionLevel,
|
||||
isDSFARequired,
|
||||
} from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
const CIA_LEVELS: { value: CIARating; label: string; description: string }[] = [
|
||||
{ value: 1, label: 'Sehr gering', description: 'Kein nennenswerter Schaden bei Verletzung' },
|
||||
{ value: 2, label: 'Gering', description: 'Begrenzter, beherrschbarer Schaden' },
|
||||
{ value: 3, label: 'Mittel', description: 'Erheblicher Schaden, aber kompensierbar' },
|
||||
{ value: 4, label: 'Hoch', description: 'Schwerwiegender Schaden, schwer kompensierbar' },
|
||||
{ value: 5, label: 'Sehr hoch', description: 'Existenzbedrohender oder irreversibler Schaden' },
|
||||
]
|
||||
|
||||
const REGULATORY_REQUIREMENTS = [
|
||||
'DSGVO',
|
||||
'BDSG',
|
||||
'MaRisk (Finanz)',
|
||||
'BAIT (Finanz)',
|
||||
'PSD2 (Zahlungsdienste)',
|
||||
'SGB (Gesundheit)',
|
||||
'MDR (Medizinprodukte)',
|
||||
'TISAX (Automotive)',
|
||||
'KRITIS (Kritische Infrastruktur)',
|
||||
'NIS2',
|
||||
'ISO 27001',
|
||||
'SOC 2',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// CIA SLIDER COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
interface CIASliderProps {
|
||||
label: string
|
||||
description: string
|
||||
value: CIARating
|
||||
onChange: (value: CIARating) => void
|
||||
}
|
||||
|
||||
function CIASlider({ label, description, value, onChange }: CIASliderProps) {
|
||||
const level = CIA_LEVELS.find((l) => l.value === value)
|
||||
|
||||
const getColor = (v: CIARating) => {
|
||||
if (v <= 2) return 'bg-green-500'
|
||||
if (v === 3) return 'bg-yellow-500'
|
||||
return 'bg-red-500'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<div>
|
||||
<h4 className="font-medium text-gray-900">{label}</h4>
|
||||
<p className="text-sm text-gray-500">{description}</p>
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium text-white ${getColor(value)}`}>
|
||||
{level?.label}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="5"
|
||||
value={value}
|
||||
onChange={(e) => onChange(parseInt(e.target.value) as CIARating)}
|
||||
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600"
|
||||
/>
|
||||
|
||||
<div className="flex justify-between mt-1 text-xs text-gray-400">
|
||||
<span>1</span>
|
||||
<span>2</span>
|
||||
<span>3</span>
|
||||
<span>4</span>
|
||||
<span>5</span>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mt-2 italic">{level?.description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PROTECTION LEVEL DISPLAY
|
||||
// =============================================================================
|
||||
|
||||
interface ProtectionLevelDisplayProps {
|
||||
level: ProtectionLevel
|
||||
}
|
||||
|
||||
function ProtectionLevelDisplay({ level }: ProtectionLevelDisplayProps) {
|
||||
const config: Record<ProtectionLevel, { label: string; color: string; bg: string; description: string }> = {
|
||||
NORMAL: {
|
||||
label: 'Normal',
|
||||
color: 'text-green-800',
|
||||
bg: 'bg-green-100',
|
||||
description: 'Standard-Schutzmaßnahmen ausreichend',
|
||||
},
|
||||
HIGH: {
|
||||
label: 'Hoch',
|
||||
color: 'text-yellow-800',
|
||||
bg: 'bg-yellow-100',
|
||||
description: 'Erweiterte Schutzmaßnahmen erforderlich',
|
||||
},
|
||||
VERY_HIGH: {
|
||||
label: 'Sehr hoch',
|
||||
color: 'text-red-800',
|
||||
bg: 'bg-red-100',
|
||||
description: 'Höchste Schutzmaßnahmen erforderlich',
|
||||
},
|
||||
}
|
||||
|
||||
const { label, color, bg, description } = config[level]
|
||||
|
||||
return (
|
||||
<div className={`${bg} rounded-lg p-4 border`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`text-2xl font-bold ${color}`}>{label}</div>
|
||||
</div>
|
||||
<p className={`text-sm ${color} mt-1`}>{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export function RiskProtectionStep() {
|
||||
const { state, setRiskProfile, completeCurrentStep } = useTOMGenerator()
|
||||
|
||||
const [formData, setFormData] = useState<Partial<RiskProfile>>({
|
||||
ciaAssessment: {
|
||||
confidentiality: 3,
|
||||
integrity: 3,
|
||||
availability: 3,
|
||||
justification: '',
|
||||
},
|
||||
protectionLevel: 'HIGH',
|
||||
specialRisks: [],
|
||||
regulatoryRequirements: ['DSGVO'],
|
||||
hasHighRiskProcessing: false,
|
||||
dsfaRequired: false,
|
||||
})
|
||||
|
||||
const [specialRiskInput, setSpecialRiskInput] = useState('')
|
||||
|
||||
// Load existing data
|
||||
useEffect(() => {
|
||||
if (state.riskProfile) {
|
||||
setFormData(state.riskProfile)
|
||||
}
|
||||
}, [state.riskProfile])
|
||||
|
||||
// Calculate protection level when CIA changes
|
||||
useEffect(() => {
|
||||
if (formData.ciaAssessment) {
|
||||
const level = calculateProtectionLevel(formData.ciaAssessment)
|
||||
const dsfaReq = isDSFARequired(state.dataProfile, {
|
||||
...formData,
|
||||
protectionLevel: level,
|
||||
} as RiskProfile)
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
protectionLevel: level,
|
||||
dsfaRequired: dsfaReq,
|
||||
}))
|
||||
}
|
||||
}, [formData.ciaAssessment, state.dataProfile])
|
||||
|
||||
// Handle CIA changes
|
||||
const handleCIAChange = (field: 'confidentiality' | 'integrity' | 'availability', value: CIARating) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
ciaAssessment: {
|
||||
...prev.ciaAssessment!,
|
||||
[field]: value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle regulatory requirements toggle
|
||||
const toggleRequirement = (req: string) => {
|
||||
setFormData((prev) => {
|
||||
const current = prev.regulatoryRequirements || []
|
||||
const updated = current.includes(req)
|
||||
? current.filter((r) => r !== req)
|
||||
: [...current, req]
|
||||
return { ...prev, regulatoryRequirements: updated }
|
||||
})
|
||||
}
|
||||
|
||||
// Handle special risk addition
|
||||
const addSpecialRisk = () => {
|
||||
if (specialRiskInput.trim()) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
specialRisks: [...(prev.specialRisks || []), specialRiskInput.trim()],
|
||||
}))
|
||||
setSpecialRiskInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeSpecialRisk = (index: number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
specialRisks: (prev.specialRisks || []).filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle submit
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const profile: RiskProfile = {
|
||||
ciaAssessment: formData.ciaAssessment!,
|
||||
protectionLevel: formData.protectionLevel || 'HIGH',
|
||||
specialRisks: formData.specialRisks || [],
|
||||
regulatoryRequirements: formData.regulatoryRequirements || [],
|
||||
hasHighRiskProcessing: formData.hasHighRiskProcessing || false,
|
||||
dsfaRequired: formData.dsfaRequired || false,
|
||||
}
|
||||
|
||||
setRiskProfile(profile)
|
||||
completeCurrentStep(profile)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* CIA Assessment */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">CIA-Bewertung</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Bewerten Sie die Schutzziele für Ihre Datenverarbeitung. Was passiert, wenn die Vertraulichkeit,
|
||||
Integrität oder Verfügbarkeit der Daten beeinträchtigt wird?
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<CIASlider
|
||||
label="Vertraulichkeit (Confidentiality)"
|
||||
description="Schutz vor unbefugtem Zugriff auf Daten"
|
||||
value={formData.ciaAssessment?.confidentiality || 3}
|
||||
onChange={(v) => handleCIAChange('confidentiality', v)}
|
||||
/>
|
||||
|
||||
<CIASlider
|
||||
label="Integrität (Integrity)"
|
||||
description="Schutz vor unbefugter Änderung von Daten"
|
||||
value={formData.ciaAssessment?.integrity || 3}
|
||||
onChange={(v) => handleCIAChange('integrity', v)}
|
||||
/>
|
||||
|
||||
<CIASlider
|
||||
label="Verfügbarkeit (Availability)"
|
||||
description="Sicherstellung des Zugriffs auf Daten"
|
||||
value={formData.ciaAssessment?.availability || 3}
|
||||
onChange={(v) => handleCIAChange('availability', v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Justification */}
|
||||
<div className="mt-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Begründung der Bewertung
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.ciaAssessment?.justification || ''}
|
||||
onChange={(e) =>
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
ciaAssessment: {
|
||||
...prev.ciaAssessment!,
|
||||
justification: e.target.value,
|
||||
},
|
||||
}))
|
||||
}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="Beschreiben Sie kurz, warum Sie diese Bewertung gewählt haben..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calculated Protection Level */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Ermittelter Schutzbedarf</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Basierend auf Ihrer CIA-Bewertung ergibt sich folgender Schutzbedarf:
|
||||
</p>
|
||||
|
||||
<ProtectionLevelDisplay level={formData.protectionLevel || 'HIGH'} />
|
||||
</div>
|
||||
|
||||
{/* DSFA Indicator */}
|
||||
{formData.dsfaRequired && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-6 h-6 text-red-500 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-medium text-red-900">DSFA erforderlich</h4>
|
||||
<p className="text-sm text-red-700 mt-1">
|
||||
Aufgrund Ihrer Datenverarbeitung (besondere Kategorien, Minderjährige oder sehr hoher Schutzbedarf)
|
||||
ist eine Datenschutz-Folgenabschätzung nach Art. 35 DSGVO erforderlich.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* High Risk Processing */}
|
||||
<div>
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasHighRiskProcessing || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasHighRiskProcessing: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Hochrisiko-Verarbeitung</span>
|
||||
<p className="text-sm text-gray-500">
|
||||
z.B. Profiling, automatisierte Entscheidungen, systematische Überwachung
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Special Risks */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Besondere Risiken</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Identifizieren Sie spezifische Risiken Ihrer Datenverarbeitung.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-2 mb-3">
|
||||
<input
|
||||
type="text"
|
||||
value={specialRiskInput}
|
||||
onChange={(e) => setSpecialRiskInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addSpecialRisk())}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. Cloud-Abhängigkeit, Insider-Bedrohungen"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addSpecialRisk}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{formData.specialRisks && formData.specialRisks.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.specialRisks.map((risk, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center gap-1 px-3 py-1 bg-red-100 text-red-800 rounded-full text-sm"
|
||||
>
|
||||
{risk}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeSpecialRisk(index)}
|
||||
className="hover:text-red-600"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Regulatory Requirements */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Regulatorische Anforderungen</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Welche regulatorischen Anforderungen gelten für Ihre Datenverarbeitung?
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{REGULATORY_REQUIREMENTS.map((req) => (
|
||||
<button
|
||||
key={req}
|
||||
type="button"
|
||||
onClick={() => toggleRequirement(req)}
|
||||
className={`px-4 py-2 rounded-full border text-sm font-medium transition-all ${
|
||||
formData.regulatoryRequirements?.includes(req)
|
||||
? 'bg-blue-100 border-blue-300 text-blue-800'
|
||||
: 'bg-white border-gray-300 text-gray-600 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
{req}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default RiskProtectionStep
|
||||
403
admin-v2/components/sdk/tom-generator/steps/ScopeRolesStep.tsx
Normal file
403
admin-v2/components/sdk/tom-generator/steps/ScopeRolesStep.tsx
Normal file
@@ -0,0 +1,403 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// Step 1: Scope & Roles
|
||||
// Company profile and role definition
|
||||
// =============================================================================
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
|
||||
import {
|
||||
CompanyProfile,
|
||||
CompanyRole,
|
||||
CompanySize,
|
||||
} from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
const COMPANY_SIZES: { value: CompanySize; label: string; description: string }[] = [
|
||||
{ value: 'MICRO', label: 'Kleinstunternehmen', description: '< 10 Mitarbeiter' },
|
||||
{ value: 'SMALL', label: 'Kleinunternehmen', description: '10-49 Mitarbeiter' },
|
||||
{ value: 'MEDIUM', label: 'Mittelunternehmen', description: '50-249 Mitarbeiter' },
|
||||
{ value: 'LARGE', label: 'Großunternehmen', description: '250-999 Mitarbeiter' },
|
||||
{ value: 'ENTERPRISE', label: 'Konzern', description: '1000+ Mitarbeiter' },
|
||||
]
|
||||
|
||||
const COMPANY_ROLES: { value: CompanyRole; label: string; description: string }[] = [
|
||||
{
|
||||
value: 'CONTROLLER',
|
||||
label: 'Verantwortlicher',
|
||||
description: 'Sie bestimmen Zweck und Mittel der Datenverarbeitung',
|
||||
},
|
||||
{
|
||||
value: 'PROCESSOR',
|
||||
label: 'Auftragsverarbeiter',
|
||||
description: 'Sie verarbeiten Daten im Auftrag eines Verantwortlichen',
|
||||
},
|
||||
{
|
||||
value: 'JOINT_CONTROLLER',
|
||||
label: 'Gemeinsam Verantwortlicher',
|
||||
description: 'Sie bestimmen gemeinsam mit anderen Zweck und Mittel',
|
||||
},
|
||||
]
|
||||
|
||||
const INDUSTRIES = [
|
||||
'Software / IT',
|
||||
'Finanzdienstleistungen',
|
||||
'Gesundheitswesen',
|
||||
'E-Commerce / Handel',
|
||||
'Beratung / Professional Services',
|
||||
'Produktion / Industrie',
|
||||
'Bildung / Forschung',
|
||||
'Öffentlicher Sektor',
|
||||
'Medien / Kommunikation',
|
||||
'Transport / Logistik',
|
||||
'Sonstige',
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export function ScopeRolesStep() {
|
||||
const { state, setCompanyProfile, completeCurrentStep } = useTOMGenerator()
|
||||
|
||||
const [formData, setFormData] = useState<Partial<CompanyProfile>>({
|
||||
id: '',
|
||||
name: '',
|
||||
industry: '',
|
||||
size: 'MEDIUM',
|
||||
role: 'CONTROLLER',
|
||||
products: [],
|
||||
dpoPerson: '',
|
||||
dpoEmail: '',
|
||||
itSecurityContact: '',
|
||||
})
|
||||
|
||||
const [productInput, setProductInput] = useState('')
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
// Load existing data
|
||||
useEffect(() => {
|
||||
if (state.companyProfile) {
|
||||
setFormData(state.companyProfile)
|
||||
}
|
||||
}, [state.companyProfile])
|
||||
|
||||
// Validation
|
||||
const validate = (): boolean => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
|
||||
if (!formData.name?.trim()) {
|
||||
newErrors.name = 'Unternehmensname ist erforderlich'
|
||||
}
|
||||
|
||||
if (!formData.industry) {
|
||||
newErrors.industry = 'Bitte wählen Sie eine Branche'
|
||||
}
|
||||
|
||||
if (!formData.role) {
|
||||
newErrors.role = 'Bitte wählen Sie eine Rolle'
|
||||
}
|
||||
|
||||
if (formData.dpoEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.dpoEmail)) {
|
||||
newErrors.dpoEmail = 'Bitte geben Sie eine gültige E-Mail-Adresse ein'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
// Handle form changes
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||
) => {
|
||||
const { name, value } = e.target
|
||||
setFormData((prev) => ({ ...prev, [name]: value }))
|
||||
// Clear error when field is edited
|
||||
if (errors[name]) {
|
||||
setErrors((prev) => ({ ...prev, [name]: '' }))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle product addition
|
||||
const addProduct = () => {
|
||||
if (productInput.trim()) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
products: [...(prev.products || []), productInput.trim()],
|
||||
}))
|
||||
setProductInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const removeProduct = (index: number) => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
products: (prev.products || []).filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle submit
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!validate()) return
|
||||
|
||||
const profile: CompanyProfile = {
|
||||
id: formData.id || `company-${Date.now()}`,
|
||||
name: formData.name!,
|
||||
industry: formData.industry!,
|
||||
size: formData.size!,
|
||||
role: formData.role!,
|
||||
products: formData.products || [],
|
||||
dpoPerson: formData.dpoPerson || null,
|
||||
dpoEmail: formData.dpoEmail || null,
|
||||
itSecurityContact: formData.itSecurityContact || null,
|
||||
}
|
||||
|
||||
setCompanyProfile(profile)
|
||||
completeCurrentStep(profile)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Company Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Unternehmensname <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name || ''}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
|
||||
errors.name ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="z.B. Muster GmbH"
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-red-500">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
{/* Industry */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Branche <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<select
|
||||
name="industry"
|
||||
value={formData.industry || ''}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
|
||||
errors.industry ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<option value="">Bitte wählen...</option>
|
||||
{INDUSTRIES.map((industry) => (
|
||||
<option key={industry} value={industry}>
|
||||
{industry}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.industry && <p className="mt-1 text-sm text-red-500">{errors.industry}</p>}
|
||||
</div>
|
||||
|
||||
{/* Company Size */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Unternehmensgröße <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{COMPANY_SIZES.map((size) => (
|
||||
<label
|
||||
key={size.value}
|
||||
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
|
||||
formData.size === size.value
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="size"
|
||||
value={size.value}
|
||||
checked={formData.size === size.value}
|
||||
onChange={handleChange}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div>
|
||||
<span className="font-medium text-gray-900">{size.label}</span>
|
||||
<p className="text-sm text-gray-500">{size.description}</p>
|
||||
</div>
|
||||
{formData.size === size.value && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Company Role */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ihre Rolle nach DSGVO <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
{COMPANY_ROLES.map((role) => (
|
||||
<label
|
||||
key={role.value}
|
||||
className={`relative flex items-start p-4 border rounded-lg cursor-pointer transition-all ${
|
||||
formData.role === role.value
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="role"
|
||||
value={role.value}
|
||||
checked={formData.role === role.value}
|
||||
onChange={handleChange}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="font-medium text-gray-900">{role.label}</span>
|
||||
<p className="text-sm text-gray-500 mt-1">{role.description}</p>
|
||||
</div>
|
||||
{formData.role === role.value && (
|
||||
<div className="flex-shrink-0 ml-3">
|
||||
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{errors.role && <p className="mt-1 text-sm text-red-500">{errors.role}</p>}
|
||||
</div>
|
||||
|
||||
{/* Products/Services */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Produkte / Services
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={productInput}
|
||||
onChange={(e) => setProductInput(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && (e.preventDefault(), addProduct())}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="z.B. Cloud CRM, API Services"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addProduct}
|
||||
className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
{formData.products && formData.products.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{formData.products.map((product, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="inline-flex items-center gap-1 px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm"
|
||||
>
|
||||
{product}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeProduct(index)}
|
||||
className="hover:text-blue-600"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contact Information */}
|
||||
<div className="border-t pt-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Kontaktinformationen</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Datenschutzbeauftragter
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="dpoPerson"
|
||||
value={formData.dpoPerson || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Name des DSB"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
E-Mail des DSB
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="dpoEmail"
|
||||
value={formData.dpoEmail || ''}
|
||||
onChange={handleChange}
|
||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
|
||||
errors.dpoEmail ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="dpo@example.de"
|
||||
/>
|
||||
{errors.dpoEmail && <p className="mt-1 text-sm text-red-500">{errors.dpoEmail}</p>}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
IT-Security Ansprechpartner
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="itSecurityContact"
|
||||
value={formData.itSecurityContact || ''}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Name oder Team"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-medium text-blue-900">Hinweis zur Rollenwahl</h4>
|
||||
<p className="text-sm text-blue-700 mt-1">
|
||||
Die Wahl Ihrer DSGVO-Rolle beeinflusst, welche TOMs für Sie relevant sind.
|
||||
Als <strong>Auftragsverarbeiter</strong> gelten zusätzliche Anforderungen nach Art. 28 DSGVO.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScopeRolesStep
|
||||
@@ -0,0 +1,417 @@
|
||||
'use client'
|
||||
|
||||
// =============================================================================
|
||||
// Step 4: Security Profile
|
||||
// Authentication, encryption, and security configuration
|
||||
// =============================================================================
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTOMGenerator } from '@/lib/sdk/tom-generator'
|
||||
import {
|
||||
SecurityProfile,
|
||||
AuthMethodType,
|
||||
BackupFrequency,
|
||||
} from '@/lib/sdk/tom-generator/types'
|
||||
|
||||
// =============================================================================
|
||||
// CONSTANTS
|
||||
// =============================================================================
|
||||
|
||||
const AUTH_METHODS: { value: AuthMethodType; label: string; description: string }[] = [
|
||||
{ value: 'PASSWORD', label: 'Passwort', description: 'Standard-Passwortauthentifizierung' },
|
||||
{ value: 'MFA', label: 'Multi-Faktor (MFA)', description: 'Zweiter Faktor erforderlich' },
|
||||
{ value: 'SSO', label: 'Single Sign-On', description: 'Zentralisierte Anmeldung' },
|
||||
{ value: 'CERTIFICATE', label: 'Zertifikat', description: 'Client-Zertifikate' },
|
||||
{ value: 'BIOMETRIC', label: 'Biometrisch', description: 'Fingerabdruck, Gesicht, etc.' },
|
||||
]
|
||||
|
||||
const BACKUP_FREQUENCIES: { value: BackupFrequency; label: string }[] = [
|
||||
{ value: 'HOURLY', label: 'Stündlich' },
|
||||
{ value: 'DAILY', label: 'Täglich' },
|
||||
{ value: 'WEEKLY', label: 'Wöchentlich' },
|
||||
{ value: 'MONTHLY', label: 'Monatlich' },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export function SecurityProfileStep() {
|
||||
const { state, setSecurityProfile, completeCurrentStep } = useTOMGenerator()
|
||||
|
||||
const [formData, setFormData] = useState<Partial<SecurityProfile>>({
|
||||
authMethods: [],
|
||||
hasMFA: false,
|
||||
hasSSO: false,
|
||||
hasIAM: false,
|
||||
hasPAM: false,
|
||||
hasEncryptionAtRest: false,
|
||||
hasEncryptionInTransit: false,
|
||||
hasLogging: false,
|
||||
logRetentionDays: 90,
|
||||
hasBackup: false,
|
||||
backupFrequency: 'DAILY',
|
||||
backupRetentionDays: 30,
|
||||
hasDRPlan: false,
|
||||
rtoHours: null,
|
||||
rpoHours: null,
|
||||
hasVulnerabilityManagement: false,
|
||||
hasPenetrationTests: false,
|
||||
hasSecurityTraining: false,
|
||||
})
|
||||
|
||||
// Load existing data
|
||||
useEffect(() => {
|
||||
if (state.securityProfile) {
|
||||
setFormData(state.securityProfile)
|
||||
}
|
||||
}, [state.securityProfile])
|
||||
|
||||
// Sync auth methods with boolean flags
|
||||
useEffect(() => {
|
||||
const authTypes = formData.authMethods?.map((m) => m.type) || []
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
hasMFA: authTypes.includes('MFA'),
|
||||
hasSSO: authTypes.includes('SSO'),
|
||||
}))
|
||||
}, [formData.authMethods])
|
||||
|
||||
// Handle auth method toggle
|
||||
const toggleAuthMethod = (method: AuthMethodType) => {
|
||||
setFormData((prev) => {
|
||||
const current = prev.authMethods || []
|
||||
const exists = current.some((m) => m.type === method)
|
||||
const updated = exists
|
||||
? current.filter((m) => m.type !== method)
|
||||
: [...current, { type: method, provider: null }]
|
||||
return { ...prev, authMethods: updated }
|
||||
})
|
||||
}
|
||||
|
||||
// Handle submit
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
const profile: SecurityProfile = {
|
||||
authMethods: formData.authMethods || [],
|
||||
hasMFA: formData.hasMFA || false,
|
||||
hasSSO: formData.hasSSO || false,
|
||||
hasIAM: formData.hasIAM || false,
|
||||
hasPAM: formData.hasPAM || false,
|
||||
hasEncryptionAtRest: formData.hasEncryptionAtRest || false,
|
||||
hasEncryptionInTransit: formData.hasEncryptionInTransit || false,
|
||||
hasLogging: formData.hasLogging || false,
|
||||
logRetentionDays: formData.logRetentionDays || 90,
|
||||
hasBackup: formData.hasBackup || false,
|
||||
backupFrequency: formData.backupFrequency || 'DAILY',
|
||||
backupRetentionDays: formData.backupRetentionDays || 30,
|
||||
hasDRPlan: formData.hasDRPlan || false,
|
||||
rtoHours: formData.rtoHours ?? null,
|
||||
rpoHours: formData.rpoHours ?? null,
|
||||
hasVulnerabilityManagement: formData.hasVulnerabilityManagement || false,
|
||||
hasPenetrationTests: formData.hasPenetrationTests || false,
|
||||
hasSecurityTraining: formData.hasSecurityTraining || false,
|
||||
}
|
||||
|
||||
setSecurityProfile(profile)
|
||||
completeCurrentStep(profile)
|
||||
}
|
||||
|
||||
const selectedAuthMethods = formData.authMethods?.map((m) => m.type) || []
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* Authentication Methods */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Authentifizierungsmethoden</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Welche Authentifizierungsmethoden werden verwendet?
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{AUTH_METHODS.map((method) => (
|
||||
<label
|
||||
key={method.value}
|
||||
className={`relative flex items-start p-3 border rounded-lg cursor-pointer transition-all ${
|
||||
selectedAuthMethods.includes(method.value)
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAuthMethods.includes(method.value)}
|
||||
onChange={() => toggleAuthMethod(method.value)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<span className="font-medium text-gray-900">{method.label}</span>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{method.description}</p>
|
||||
</div>
|
||||
{selectedAuthMethods.includes(method.value) && (
|
||||
<svg className="w-5 h-5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* MFA recommendation */}
|
||||
{!selectedAuthMethods.includes('MFA') && (
|
||||
<div className="mt-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<p className="text-sm text-yellow-800">
|
||||
<strong>Empfehlung:</strong> Multi-Faktor-Authentifizierung (MFA) wird für alle sensiblen
|
||||
Systeme dringend empfohlen und ist bei besonderen Datenkategorien oft erforderlich.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Identity & Access Management */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Identity & Access Management</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasIAM || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasIAM: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Identity & Access Management (IAM)</span>
|
||||
<p className="text-sm text-gray-500">Zentralisierte Benutzer- und Berechtigungsverwaltung</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasPAM || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasPAM: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Privileged Access Management (PAM)</span>
|
||||
<p className="text-sm text-gray-500">Kontrolle privilegierter Zugänge (Admin-Konten)</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Encryption */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Verschlüsselung</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasEncryptionAtRest || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasEncryptionAtRest: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Verschlüsselung ruhender Daten</span>
|
||||
<p className="text-sm text-gray-500">AES-256 oder vergleichbar für gespeicherte Daten</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasEncryptionInTransit || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasEncryptionInTransit: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Transportverschlüsselung</span>
|
||||
<p className="text-sm text-gray-500">TLS 1.2+ für alle Datenübertragungen</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logging */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Protokollierung</h3>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50 mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasLogging || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasLogging: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Audit-Logging aktiviert</span>
|
||||
<p className="text-sm text-gray-500">Protokollierung aller sicherheitsrelevanten Ereignisse</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{formData.hasLogging && (
|
||||
<div className="pl-8">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Log-Aufbewahrung (Tage)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={formData.logRetentionDays || 90}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, logRetentionDays: parseInt(e.target.value) || 90 }))}
|
||||
className="w-32 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Backup & DR */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Backup & Disaster Recovery</h3>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50 mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasBackup || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasBackup: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Regelmäßige Backups</span>
|
||||
<p className="text-sm text-gray-500">Automatisierte Datensicherung</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{formData.hasBackup && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-8 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Backup-Frequenz
|
||||
</label>
|
||||
<select
|
||||
value={formData.backupFrequency || 'DAILY'}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, backupFrequency: e.target.value as BackupFrequency }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{BACKUP_FREQUENCIES.map((freq) => (
|
||||
<option key={freq.value} value={freq.value}>{freq.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Backup-Aufbewahrung (Tage)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={formData.backupRetentionDays || 30}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, backupRetentionDays: parseInt(e.target.value) || 30 }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50 mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasDRPlan || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasDRPlan: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Disaster Recovery Plan vorhanden</span>
|
||||
<p className="text-sm text-gray-500">Dokumentierter Wiederherstellungsplan</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{formData.hasDRPlan && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 pl-8">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
RTO (Recovery Time Objective) in Stunden
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.5"
|
||||
value={formData.rtoHours ?? ''}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, rtoHours: e.target.value ? parseFloat(e.target.value) : null }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. 4"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Maximale Ausfallzeit</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
RPO (Recovery Point Objective) in Stunden
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.25"
|
||||
value={formData.rpoHours ?? ''}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, rpoHours: e.target.value ? parseFloat(e.target.value) : null }))}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="z.B. 1"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Maximaler Datenverlust</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Security Testing & Training */}
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">Sicherheitstests & Schulungen</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasVulnerabilityManagement || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasVulnerabilityManagement: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Schwachstellenmanagement</span>
|
||||
<p className="text-sm text-gray-500">Regelmäßige Schwachstellenscans und Patch-Management</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasPenetrationTests || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasPenetrationTests: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Penetrationstests</span>
|
||||
<p className="text-sm text-gray-500">Regelmäßige Sicherheitstests durch externe Prüfer</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer p-3 border rounded-lg hover:bg-gray-50">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.hasSecurityTraining || false}
|
||||
onChange={(e) => setFormData((prev) => ({ ...prev, hasSecurityTraining: e.target.checked }))}
|
||||
className="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-gray-700 font-medium">Security Awareness Training</span>
|
||||
<p className="text-sm text-gray-500">Regelmäßige Schulungen für alle Mitarbeiter</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default SecurityProfileStep
|
||||
Reference in New Issue
Block a user