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:
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
|
||||
Reference in New Issue
Block a user