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>
461 lines
18 KiB
TypeScript
461 lines
18 KiB
TypeScript
'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
|