refactor: Remove all SDK/compliance pages and API routes from admin-lehrer
SDK/compliance content belongs exclusively in admin-compliance (port 3007). Removed: - All (sdk)/ pages (document-crawler, dsb-portal, industry-templates, multi-tenant, sso) - All api/sdk/ proxy routes - All developers/sdk/ documentation pages - Unused lib/sdk/ modules (kept: catalog-manager + its deps for dashboard) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,256 +0,0 @@
|
|||||||
import { DevPortalLayout, CodeBlock, InfoBox, ParameterTable } from '@/components/developers/DevPortalLayout'
|
|
||||||
|
|
||||||
export default function SDKConfigurationPage() {
|
|
||||||
return (
|
|
||||||
<DevPortalLayout
|
|
||||||
title="SDK Konfiguration"
|
|
||||||
description="Alle Konfigurationsoptionen des AI Compliance SDK"
|
|
||||||
>
|
|
||||||
<h2>SDKProvider Props</h2>
|
|
||||||
<p>
|
|
||||||
Der SDKProvider akzeptiert folgende Konfigurationsoptionen:
|
|
||||||
</p>
|
|
||||||
<ParameterTable
|
|
||||||
parameters={[
|
|
||||||
{
|
|
||||||
name: 'tenantId',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
description: 'Ihre eindeutige Tenant-ID (erhalten nach Abo-Abschluss)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'apiKey',
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
description: 'API Key fuer authentifizierte Anfragen (nur serverseitig verwenden)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'userId',
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
description: 'Benutzer-ID fuer Audit-Trail und Checkpoints',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'enableBackendSync',
|
|
||||||
type: 'boolean',
|
|
||||||
required: false,
|
|
||||||
description: 'Aktiviert automatische Synchronisation mit dem Backend (default: false)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'apiBaseUrl',
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
description: 'Custom API URL fuer Self-Hosted Installationen',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'syncInterval',
|
|
||||||
type: 'number',
|
|
||||||
required: false,
|
|
||||||
description: 'Intervall fuer Auto-Sync in Millisekunden (default: 30000)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'enableOfflineSupport',
|
|
||||||
type: 'boolean',
|
|
||||||
required: false,
|
|
||||||
description: 'Aktiviert localStorage Fallback bei Offline (default: true)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'initialStep',
|
|
||||||
type: 'string',
|
|
||||||
required: false,
|
|
||||||
description: 'Initialer Schritt beim ersten Laden (default: "advisory-board")',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'onError',
|
|
||||||
type: '(error: Error) => void',
|
|
||||||
required: false,
|
|
||||||
description: 'Callback fuer Fehlerbehandlung',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'onStateChange',
|
|
||||||
type: '(state: SDKState) => void',
|
|
||||||
required: false,
|
|
||||||
description: 'Callback bei State-Aenderungen',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h2>Vollstaendiges Beispiel</h2>
|
|
||||||
<CodeBlock language="typescript" filename="app/layout.tsx">
|
|
||||||
{`'use client'
|
|
||||||
|
|
||||||
import { SDKProvider } from '@breakpilot/compliance-sdk'
|
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
|
|
||||||
export default function SDKLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SDKProvider
|
|
||||||
tenantId={process.env.NEXT_PUBLIC_TENANT_ID!}
|
|
||||||
userId="user-123"
|
|
||||||
enableBackendSync={true}
|
|
||||||
syncInterval={60000} // Sync alle 60 Sekunden
|
|
||||||
enableOfflineSupport={true}
|
|
||||||
initialStep="use-case-workshop"
|
|
||||||
onError={(error) => {
|
|
||||||
console.error('SDK Error:', error)
|
|
||||||
// Optional: Sentry oder anderes Error-Tracking
|
|
||||||
}}
|
|
||||||
onStateChange={(state) => {
|
|
||||||
console.log('State changed:', state.currentStep)
|
|
||||||
// Optional: Analytics-Events
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>
|
|
||||||
)
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Synchronisations-Strategien</h2>
|
|
||||||
|
|
||||||
<h3>1. Nur localStorage (Offline-Only)</h3>
|
|
||||||
<CodeBlock language="typescript" filename="Offline-Only">
|
|
||||||
{`<SDKProvider
|
|
||||||
tenantId="my-tenant"
|
|
||||||
enableBackendSync={false}
|
|
||||||
enableOfflineSupport={true}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>`}
|
|
||||||
</CodeBlock>
|
|
||||||
<p>
|
|
||||||
Ideal fuer: Lokale Entwicklung, Demos, Privacy-fokussierte Installationen.
|
|
||||||
Daten werden nur im Browser gespeichert.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>2. Backend-Sync mit Fallback</h3>
|
|
||||||
<CodeBlock language="typescript" filename="Backend + Fallback">
|
|
||||||
{`<SDKProvider
|
|
||||||
tenantId="my-tenant"
|
|
||||||
enableBackendSync={true}
|
|
||||||
enableOfflineSupport={true}
|
|
||||||
syncInterval={30000}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>`}
|
|
||||||
</CodeBlock>
|
|
||||||
<p>
|
|
||||||
Empfohlen fuer: Produktionsumgebungen. Daten werden mit dem Backend
|
|
||||||
synchronisiert, localStorage dient als Fallback bei Netzwerkproblemen.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>3. Nur Backend (kein lokaler Cache)</h3>
|
|
||||||
<CodeBlock language="typescript" filename="Backend-Only">
|
|
||||||
{`<SDKProvider
|
|
||||||
tenantId="my-tenant"
|
|
||||||
enableBackendSync={true}
|
|
||||||
enableOfflineSupport={false}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>`}
|
|
||||||
</CodeBlock>
|
|
||||||
<p>
|
|
||||||
Ideal fuer: Strenge Compliance-Anforderungen, Multi-User-Szenarien.
|
|
||||||
Daten werden nur im Backend gespeichert.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<InfoBox type="warning" title="Backend-Only Modus">
|
|
||||||
Im Backend-Only Modus ist eine aktive Internetverbindung erforderlich.
|
|
||||||
Bei Netzwerkproblemen koennen Daten verloren gehen.
|
|
||||||
</InfoBox>
|
|
||||||
|
|
||||||
<h2>API URL Konfiguration</h2>
|
|
||||||
|
|
||||||
<h3>Cloud-Version (Standard)</h3>
|
|
||||||
<p>Keine zusaetzliche Konfiguration erforderlich:</p>
|
|
||||||
<CodeBlock language="typescript" filename="Cloud">
|
|
||||||
{`<SDKProvider tenantId="my-tenant">
|
|
||||||
{/* Nutzt automatisch https://api.breakpilot.io/sdk/v1 */}
|
|
||||||
</SDKProvider>`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h3>Self-Hosted</h3>
|
|
||||||
<CodeBlock language="typescript" filename="Self-Hosted">
|
|
||||||
{`<SDKProvider
|
|
||||||
tenantId="my-tenant"
|
|
||||||
apiBaseUrl="https://your-server.com/sdk/v1"
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h3>Lokale Entwicklung</h3>
|
|
||||||
<CodeBlock language="typescript" filename="Local Dev">
|
|
||||||
{`<SDKProvider
|
|
||||||
tenantId="dev-tenant"
|
|
||||||
apiBaseUrl="http://localhost:8085/sdk/v1"
|
|
||||||
enableBackendSync={true}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Feature Flags</h2>
|
|
||||||
<p>
|
|
||||||
Das SDK unterstuetzt Feature Flags ueber Subscription-Levels:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="Feature Checks">
|
|
||||||
{`import { useSDK } from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
const { state } = useSDK()
|
|
||||||
|
|
||||||
// Subscription-basierte Features
|
|
||||||
const isEnterprise = state.subscription === 'ENTERPRISE'
|
|
||||||
const isProfessional = ['PROFESSIONAL', 'ENTERPRISE'].includes(state.subscription)
|
|
||||||
|
|
||||||
// Feature-Gates
|
|
||||||
const canExportPDF = isProfessional
|
|
||||||
const canUseRAG = isProfessional
|
|
||||||
const canCustomizeDSFA = isEnterprise
|
|
||||||
const canUseAPI = isProfessional
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{canExportPDF && <button>PDF Export</button>}
|
|
||||||
{canUseRAG && <RAGSearchPanel />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Logging & Debugging</h2>
|
|
||||||
<p>
|
|
||||||
Aktivieren Sie detailliertes Logging fuer die Entwicklung:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="Debug Mode">
|
|
||||||
{`// In Ihrer .env.local
|
|
||||||
NEXT_PUBLIC_SDK_DEBUG=true
|
|
||||||
|
|
||||||
// Oder programmatisch
|
|
||||||
<SDKProvider
|
|
||||||
tenantId="my-tenant"
|
|
||||||
onStateChange={(state) => {
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
console.log('[SDK] State Update:', {
|
|
||||||
phase: state.currentPhase,
|
|
||||||
step: state.currentStep,
|
|
||||||
useCases: state.useCases.length,
|
|
||||||
risks: state.risks.length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<InfoBox type="info" title="React DevTools">
|
|
||||||
Der SDK-State ist im React DevTools unter dem SDKProvider-Context sichtbar.
|
|
||||||
Installieren Sie die React Developer Tools Browser-Extension fuer einfaches Debugging.
|
|
||||||
</InfoBox>
|
|
||||||
</DevPortalLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,482 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Copy, Check } from 'lucide-react'
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
className="p-2 hover:bg-gray-700 rounded transition-colors"
|
|
||||||
title="Kopieren"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="w-4 h-4 text-green-400" />
|
|
||||||
) : (
|
|
||||||
<Copy className="w-4 h-4 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeBlock({ code }: { code: string }) {
|
|
||||||
return (
|
|
||||||
<div className="relative bg-gray-900 rounded-lg overflow-hidden">
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<CopyButton text={code} />
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function MethodCard({
|
|
||||||
name,
|
|
||||||
signature,
|
|
||||||
description,
|
|
||||||
params,
|
|
||||||
returns,
|
|
||||||
example,
|
|
||||||
}: {
|
|
||||||
name: string
|
|
||||||
signature: string
|
|
||||||
description: string
|
|
||||||
params?: { name: string; type: string; description: string }[]
|
|
||||||
returns?: string
|
|
||||||
example?: string
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
|
|
||||||
<code className="text-violet-600 font-mono font-semibold">{name}</code>
|
|
||||||
</div>
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="bg-gray-100 rounded-lg p-3 mb-4">
|
|
||||||
<code className="text-sm font-mono text-gray-800">{signature}</code>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 mb-4">{description}</p>
|
|
||||||
|
|
||||||
{params && params.length > 0 && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<h4 className="font-medium text-gray-900 mb-2">Parameter</h4>
|
|
||||||
<table className="min-w-full">
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
{params.map((param) => (
|
|
||||||
<tr key={param.name}>
|
|
||||||
<td className="py-2 pr-4">
|
|
||||||
<code className="text-sm text-violet-600">{param.name}</code>
|
|
||||||
</td>
|
|
||||||
<td className="py-2 pr-4">
|
|
||||||
<code className="text-sm text-gray-500">{param.type}</code>
|
|
||||||
</td>
|
|
||||||
<td className="py-2 text-sm text-gray-600">{param.description}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{returns && (
|
|
||||||
<div className="mb-4">
|
|
||||||
<h4 className="font-medium text-gray-900 mb-2">Rueckgabe</h4>
|
|
||||||
<code className="text-sm text-gray-600">{returns}</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{example && (
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium text-gray-900 mb-2">Beispiel</h4>
|
|
||||||
<CodeBlock code={example} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function APIReferencePage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">API Referenz</h1>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Vollstaendige Dokumentation aller Methoden und Konfigurationsoptionen des Consent SDK.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* ConsentManager */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">ConsentManager</h2>
|
|
||||||
<p className="text-gray-600 mb-6">
|
|
||||||
Die zentrale Klasse fuer das Consent Management. Verwaltet Einwilligungen, Script-Blocking und Events.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Constructor */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<MethodCard
|
|
||||||
name="constructor"
|
|
||||||
signature="new ConsentManager(config: ConsentConfig)"
|
|
||||||
description="Erstellt eine neue Instanz des ConsentManagers mit der angegebenen Konfiguration."
|
|
||||||
params={[
|
|
||||||
{
|
|
||||||
name: 'config',
|
|
||||||
type: 'ConsentConfig',
|
|
||||||
description: 'Konfigurationsobjekt fuer den Manager',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
example={`const consent = new ConsentManager({
|
|
||||||
apiEndpoint: 'https://api.example.com/consent',
|
|
||||||
siteId: 'my-site',
|
|
||||||
debug: true,
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="init"
|
|
||||||
signature="async init(): Promise<void>"
|
|
||||||
description="Initialisiert das SDK, laedt bestehenden Consent und startet das Script-Blocking. Zeigt den Banner an falls noetig."
|
|
||||||
example={`await consent.init();`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="hasConsent"
|
|
||||||
signature="hasConsent(category: ConsentCategory): boolean"
|
|
||||||
description="Prueft ob Einwilligung fuer eine Kategorie vorliegt."
|
|
||||||
params={[
|
|
||||||
{
|
|
||||||
name: 'category',
|
|
||||||
type: 'ConsentCategory',
|
|
||||||
description: 'essential | functional | analytics | marketing | social',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
returns="boolean - true wenn Einwilligung vorliegt"
|
|
||||||
example={`if (consent.hasConsent('analytics')) {
|
|
||||||
// Analytics laden
|
|
||||||
loadGoogleAnalytics();
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="setConsent"
|
|
||||||
signature="async setConsent(input: ConsentInput): Promise<void>"
|
|
||||||
description="Setzt die Einwilligungen und speichert sie lokal sowie auf dem Server."
|
|
||||||
params={[
|
|
||||||
{
|
|
||||||
name: 'input',
|
|
||||||
type: 'ConsentInput',
|
|
||||||
description: 'Objekt mit Kategorien und optionalen Vendors',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
example={`await consent.setConsent({
|
|
||||||
essential: true,
|
|
||||||
functional: true,
|
|
||||||
analytics: true,
|
|
||||||
marketing: false,
|
|
||||||
social: false,
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="acceptAll"
|
|
||||||
signature="async acceptAll(): Promise<void>"
|
|
||||||
description="Akzeptiert alle Consent-Kategorien und schliesst den Banner."
|
|
||||||
example={`document.getElementById('accept-all').addEventListener('click', async () => {
|
|
||||||
await consent.acceptAll();
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="rejectAll"
|
|
||||||
signature="async rejectAll(): Promise<void>"
|
|
||||||
description="Lehnt alle nicht-essentiellen Kategorien ab und schliesst den Banner."
|
|
||||||
example={`document.getElementById('reject-all').addEventListener('click', async () => {
|
|
||||||
await consent.rejectAll();
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="revokeAll"
|
|
||||||
signature="async revokeAll(): Promise<void>"
|
|
||||||
description="Widerruft alle Einwilligungen und loescht den gespeicherten Consent."
|
|
||||||
example={`document.getElementById('revoke').addEventListener('click', async () => {
|
|
||||||
await consent.revokeAll();
|
|
||||||
location.reload();
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="on"
|
|
||||||
signature="on<T>(event: ConsentEventType, callback: (data: T) => void): () => void"
|
|
||||||
description="Registriert einen Event-Listener. Gibt eine Unsubscribe-Funktion zurueck."
|
|
||||||
params={[
|
|
||||||
{
|
|
||||||
name: 'event',
|
|
||||||
type: 'ConsentEventType',
|
|
||||||
description: 'init | change | accept_all | reject_all | banner_show | banner_hide | etc.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'callback',
|
|
||||||
type: 'function',
|
|
||||||
description: 'Callback-Funktion die bei Event aufgerufen wird',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
returns="() => void - Funktion zum Entfernen des Listeners"
|
|
||||||
example={`const unsubscribe = consent.on('change', (state) => {
|
|
||||||
console.log('Consent geaendert:', state);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spaeter: Listener entfernen
|
|
||||||
unsubscribe();`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="getConsent"
|
|
||||||
signature="getConsent(): ConsentState | null"
|
|
||||||
description="Gibt den aktuellen Consent-Status zurueck oder null falls kein Consent vorliegt."
|
|
||||||
returns="ConsentState | null"
|
|
||||||
example={`const state = consent.getConsent();
|
|
||||||
if (state) {
|
|
||||||
console.log('Consent ID:', state.consentId);
|
|
||||||
console.log('Kategorien:', state.categories);
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MethodCard
|
|
||||||
name="exportConsent"
|
|
||||||
signature="async exportConsent(): Promise<string>"
|
|
||||||
description="Exportiert alle Consent-Daten als JSON-String (DSGVO Art. 20 Datenportabilitaet)."
|
|
||||||
returns="Promise<string> - JSON-formatierter Export"
|
|
||||||
example={`const exportData = await consent.exportConsent();
|
|
||||||
downloadAsFile(exportData, 'consent-export.json');`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Configuration */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Konfiguration</h2>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Option
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Typ
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Default
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Beschreibung
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">apiEndpoint</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">string</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">erforderlich</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">URL des Consent-Backends</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">siteId</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">string</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">erforderlich</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Eindeutige Site-ID</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">debug</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">boolean</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">false</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Aktiviert Debug-Logging</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">language</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">string</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">'de'</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Sprache fuer UI-Texte</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">consent.rememberDays</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">number</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">365</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Gueltigkeitsdauer in Tagen</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">consent.recheckAfterDays</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">number</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-500">180</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Erneute Abfrage nach X Tagen</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Events */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Events</h2>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Event
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Daten
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Beschreibung
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">init</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">ConsentState | null</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">SDK initialisiert</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">change</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">ConsentState</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Consent geaendert</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">accept_all</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">ConsentState</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Alle akzeptiert</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">reject_all</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">ConsentState</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Alle abgelehnt</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">banner_show</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">undefined</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Banner angezeigt</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">banner_hide</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">undefined</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Banner versteckt</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-violet-600">error</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<code className="text-sm text-gray-500">Error</code>
|
|
||||||
</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Fehler aufgetreten</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Types */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 mb-6">TypeScript Types</h2>
|
|
||||||
<CodeBlock
|
|
||||||
code={`// Consent-Kategorien
|
|
||||||
type ConsentCategory = 'essential' | 'functional' | 'analytics' | 'marketing' | 'social';
|
|
||||||
|
|
||||||
// Consent-Status
|
|
||||||
interface ConsentState {
|
|
||||||
categories: Record<ConsentCategory, boolean>;
|
|
||||||
vendors: Record<string, boolean>;
|
|
||||||
timestamp: string;
|
|
||||||
version: string;
|
|
||||||
consentId?: string;
|
|
||||||
expiresAt?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Konfiguration
|
|
||||||
interface ConsentConfig {
|
|
||||||
apiEndpoint: string;
|
|
||||||
siteId: string;
|
|
||||||
debug?: boolean;
|
|
||||||
language?: string;
|
|
||||||
fallbackLanguage?: string;
|
|
||||||
ui?: ConsentUIConfig;
|
|
||||||
consent?: ConsentBehaviorConfig;
|
|
||||||
onConsentChange?: (state: ConsentState) => void;
|
|
||||||
onBannerShow?: () => void;
|
|
||||||
onBannerHide?: () => void;
|
|
||||||
onError?: (error: Error) => void;
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Copy, Check } from 'lucide-react'
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
|
|
||||||
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-900 rounded-lg overflow-hidden">
|
|
||||||
{filename && (
|
|
||||||
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
|
|
||||||
{filename}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<CopyButton text={code} />
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AngularIntegrationPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-red-500 flex items-center justify-center">
|
|
||||||
<span className="text-white font-bold">A</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Angular Integration</h1>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Service und Module fuer Angular 14+ Projekte.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Installation */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
|
|
||||||
<CodeBlock code="npm install @breakpilot/consent-sdk" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Module Setup */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Module Setup</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="app.module.ts"
|
|
||||||
code={`import { NgModule } from '@angular/core';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
import { ConsentModule } from '@breakpilot/consent-sdk/angular';
|
|
||||||
import { environment } from '../environments/environment';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
ConsentModule.forRoot({
|
|
||||||
apiEndpoint: environment.consentApi,
|
|
||||||
siteId: 'my-site',
|
|
||||||
debug: !environment.production,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AppModule {}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Standalone Setup */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Standalone Setup (Angular 15+)</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="app.config.ts"
|
|
||||||
code={`import { ApplicationConfig } from '@angular/core';
|
|
||||||
import { provideConsent } from '@breakpilot/consent-sdk/angular';
|
|
||||||
import { environment } from '../environments/environment';
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
|
||||||
providers: [
|
|
||||||
provideConsent({
|
|
||||||
apiEndpoint: environment.consentApi,
|
|
||||||
siteId: 'my-site',
|
|
||||||
debug: !environment.production,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Service Usage */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Service Usage</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="components/analytics.component.ts"
|
|
||||||
code={`import { Component, OnInit } from '@angular/core';
|
|
||||||
import { ConsentService } from '@breakpilot/consent-sdk/angular';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-analytics',
|
|
||||||
template: \`
|
|
||||||
<div *ngIf="hasAnalyticsConsent$ | async">
|
|
||||||
<!-- Analytics Code hier -->
|
|
||||||
</div>
|
|
||||||
\`,
|
|
||||||
})
|
|
||||||
export class AnalyticsComponent implements OnInit {
|
|
||||||
hasAnalyticsConsent$ = this.consentService.hasConsent$('analytics');
|
|
||||||
|
|
||||||
constructor(private consentService: ConsentService) {}
|
|
||||||
|
|
||||||
async loadAnalytics() {
|
|
||||||
if (await this.consentService.hasConsent('analytics')) {
|
|
||||||
// Load analytics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Cookie Banner */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Cookie Banner Component</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="components/cookie-banner.component.ts"
|
|
||||||
code={`import { Component } from '@angular/core';
|
|
||||||
import { ConsentService } from '@breakpilot/consent-sdk/angular';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-cookie-banner',
|
|
||||||
template: \`
|
|
||||||
<div
|
|
||||||
*ngIf="isBannerVisible$ | async"
|
|
||||||
class="fixed bottom-0 inset-x-0 bg-white border-t shadow-lg p-4 z-50"
|
|
||||||
>
|
|
||||||
<div class="max-w-4xl mx-auto flex items-center justify-between">
|
|
||||||
<p class="text-sm text-gray-600">
|
|
||||||
Wir verwenden Cookies um Ihr Erlebnis zu verbessern.
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button (click)="rejectAll()" class="px-4 py-2 border rounded">
|
|
||||||
Ablehnen
|
|
||||||
</button>
|
|
||||||
<button (click)="showSettings()" class="px-4 py-2 border rounded">
|
|
||||||
Einstellungen
|
|
||||||
</button>
|
|
||||||
<button (click)="acceptAll()" class="px-4 py-2 bg-blue-600 text-white rounded">
|
|
||||||
Alle akzeptieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
\`,
|
|
||||||
})
|
|
||||||
export class CookieBannerComponent {
|
|
||||||
isBannerVisible$ = this.consentService.isBannerVisible$;
|
|
||||||
|
|
||||||
constructor(private consentService: ConsentService) {}
|
|
||||||
|
|
||||||
async acceptAll() {
|
|
||||||
await this.consentService.acceptAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
async rejectAll() {
|
|
||||||
await this.consentService.rejectAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
showSettings() {
|
|
||||||
this.consentService.showSettings();
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Directive */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">ConsentGate Directive</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="template.html"
|
|
||||||
code={`<!-- Zeigt Element nur wenn Consent vorhanden -->
|
|
||||||
<iframe
|
|
||||||
*consentGate="'social'"
|
|
||||||
src="https://www.youtube.com/embed/VIDEO_ID"
|
|
||||||
width="560"
|
|
||||||
height="315"
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
<!-- Mit Custom Fallback -->
|
|
||||||
<div *consentGate="'analytics'; else noConsent">
|
|
||||||
<app-analytics-dashboard></app-analytics-dashboard>
|
|
||||||
</div>
|
|
||||||
<ng-template #noConsent>
|
|
||||||
<div class="bg-gray-100 p-4 rounded-lg text-center">
|
|
||||||
<p>Bitte stimmen Sie Statistik-Cookies zu.</p>
|
|
||||||
<button (click)="showSettings()">Einstellungen</button>
|
|
||||||
</div>
|
|
||||||
</ng-template>`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Service API */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Service API</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Property/Method
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Typ
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Beschreibung
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">consent$</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Observable<ConsentState></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Observable des aktuellen Consent</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">hasConsent$()</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Observable<boolean></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Reaktive Consent-Pruefung</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">hasConsent()</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Promise<boolean></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Async Consent-Pruefung</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">isBannerVisible$</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Observable<boolean></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Banner-Sichtbarkeit</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">acceptAll()</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Promise<void></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Akzeptiert alle</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">rejectAll()</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Promise<void></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Lehnt alle ab</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">setConsent()</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Promise<void></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Setzt spezifische Kategorien</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { ChevronRight } from 'lucide-react'
|
|
||||||
|
|
||||||
const frameworks = [
|
|
||||||
{
|
|
||||||
name: 'React',
|
|
||||||
href: '/developers/sdk/consent/frameworks/react',
|
|
||||||
logo: '/logos/react.svg',
|
|
||||||
description: 'Hooks und Provider fuer React 17+ und Next.js',
|
|
||||||
features: ['ConsentProvider', 'useConsent Hook', 'ConsentGate Component'],
|
|
||||||
color: 'bg-cyan-500',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Vue 3',
|
|
||||||
href: '/developers/sdk/consent/frameworks/vue',
|
|
||||||
logo: '/logos/vue.svg',
|
|
||||||
description: 'Composables und Plugin fuer Vue 3 und Nuxt',
|
|
||||||
features: ['Vue Plugin', 'useConsent Composable', 'ConsentGate Component'],
|
|
||||||
color: 'bg-emerald-500',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Angular',
|
|
||||||
href: '/developers/sdk/consent/frameworks/angular',
|
|
||||||
logo: '/logos/angular.svg',
|
|
||||||
description: 'Service und Module fuer Angular 14+',
|
|
||||||
features: ['ConsentService', 'ConsentModule', 'Dependency Injection'],
|
|
||||||
color: 'bg-red-500',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function FrameworksPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Framework Integration</h1>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Das Consent SDK bietet native Integrationen fuer alle gaengigen Frontend-Frameworks.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{frameworks.map((framework) => (
|
|
||||||
<Link
|
|
||||||
key={framework.name}
|
|
||||||
href={framework.href}
|
|
||||||
className="block bg-white rounded-xl border border-gray-200 p-6 hover:border-violet-300 hover:shadow-md transition-all group"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className={`w-12 h-12 rounded-xl ${framework.color} flex items-center justify-center shrink-0`}>
|
|
||||||
<span className="text-white font-bold text-lg">{framework.name[0]}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 group-hover:text-violet-600 transition-colors">
|
|
||||||
{framework.name}
|
|
||||||
</h2>
|
|
||||||
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-violet-600 transition-colors" />
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 mt-1">{framework.description}</p>
|
|
||||||
<div className="flex flex-wrap gap-2 mt-3">
|
|
||||||
{framework.features.map((feature) => (
|
|
||||||
<span
|
|
||||||
key={feature}
|
|
||||||
className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-md"
|
|
||||||
>
|
|
||||||
{feature}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Vanilla JS Note */}
|
|
||||||
<div className="mt-8 p-4 bg-blue-50 border border-blue-200 rounded-xl">
|
|
||||||
<h3 className="font-medium text-blue-900">Vanilla JavaScript</h3>
|
|
||||||
<p className="text-sm text-blue-700 mt-1">
|
|
||||||
Sie koennen das SDK auch ohne Framework verwenden. Importieren Sie einfach den ConsentManager direkt
|
|
||||||
aus dem Hauptpaket. Siehe{' '}
|
|
||||||
<Link href="/developers/sdk/consent/installation" className="underline">
|
|
||||||
Installation
|
|
||||||
</Link>{' '}
|
|
||||||
fuer Details.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Copy, Check } from 'lucide-react'
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
|
|
||||||
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-900 rounded-lg overflow-hidden">
|
|
||||||
{filename && (
|
|
||||||
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
|
|
||||||
{filename}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<CopyButton text={code} />
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ReactIntegrationPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-cyan-500 flex items-center justify-center">
|
|
||||||
<span className="text-white font-bold">R</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">React Integration</h1>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Hooks und Provider fuer React 17+ und Next.js Projekte.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Installation */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
|
|
||||||
<CodeBlock code="npm install @breakpilot/consent-sdk" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Provider Setup */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Provider Setup</h2>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Umschliessen Sie Ihre App mit dem ConsentProvider:
|
|
||||||
</p>
|
|
||||||
<CodeBlock
|
|
||||||
filename="app/layout.tsx"
|
|
||||||
code={`import { ConsentProvider } from '@breakpilot/consent-sdk/react';
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html lang="de">
|
|
||||||
<body>
|
|
||||||
<ConsentProvider
|
|
||||||
config={{
|
|
||||||
apiEndpoint: process.env.NEXT_PUBLIC_CONSENT_API!,
|
|
||||||
siteId: 'my-site',
|
|
||||||
debug: process.env.NODE_ENV === 'development',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ConsentProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* useConsent Hook */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">useConsent Hook</h2>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Verwenden Sie den Hook in jeder Komponente:
|
|
||||||
</p>
|
|
||||||
<CodeBlock
|
|
||||||
filename="components/Analytics.tsx"
|
|
||||||
code={`import { useConsent } from '@breakpilot/consent-sdk/react';
|
|
||||||
|
|
||||||
export function Analytics() {
|
|
||||||
const { hasConsent, acceptAll, rejectAll, showSettings } = useConsent();
|
|
||||||
|
|
||||||
if (!hasConsent('analytics')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Script
|
|
||||||
src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
|
|
||||||
strategy="afterInteractive"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ConsentGate */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">ConsentGate Component</h2>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Zeigt Inhalte nur wenn Consent vorhanden ist:
|
|
||||||
</p>
|
|
||||||
<CodeBlock
|
|
||||||
filename="components/YouTubeEmbed.tsx"
|
|
||||||
code={`import { ConsentGate } from '@breakpilot/consent-sdk/react';
|
|
||||||
|
|
||||||
export function YouTubeEmbed({ videoId }: { videoId: string }) {
|
|
||||||
return (
|
|
||||||
<ConsentGate
|
|
||||||
category="social"
|
|
||||||
fallback={
|
|
||||||
<div className="bg-gray-100 p-4 rounded-lg text-center">
|
|
||||||
<p>Video erfordert Ihre Zustimmung.</p>
|
|
||||||
<button onClick={() => showSettings()}>
|
|
||||||
Einstellungen oeffnen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
src={\`https://www.youtube.com/embed/\${videoId}\`}
|
|
||||||
width="560"
|
|
||||||
height="315"
|
|
||||||
allowFullScreen
|
|
||||||
/>
|
|
||||||
</ConsentGate>
|
|
||||||
);
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Custom Banner */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Custom Cookie Banner</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="components/CookieBanner.tsx"
|
|
||||||
code={`import { useConsent } from '@breakpilot/consent-sdk/react';
|
|
||||||
|
|
||||||
export function CookieBanner() {
|
|
||||||
const {
|
|
||||||
isBannerVisible,
|
|
||||||
acceptAll,
|
|
||||||
rejectAll,
|
|
||||||
showSettings,
|
|
||||||
} = useConsent();
|
|
||||||
|
|
||||||
if (!isBannerVisible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-0 inset-x-0 bg-white border-t shadow-lg p-4">
|
|
||||||
<div className="max-w-4xl mx-auto flex items-center justify-between">
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
Wir verwenden Cookies um Ihr Erlebnis zu verbessern.
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={rejectAll}
|
|
||||||
className="px-4 py-2 text-sm border rounded"
|
|
||||||
>
|
|
||||||
Ablehnen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={showSettings}
|
|
||||||
className="px-4 py-2 text-sm border rounded"
|
|
||||||
>
|
|
||||||
Einstellungen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={acceptAll}
|
|
||||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded"
|
|
||||||
>
|
|
||||||
Alle akzeptieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Hook API */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Hook API</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Property
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Typ
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Beschreibung
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">hasConsent</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">(category) => boolean</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Prueft Consent fuer Kategorie</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">consent</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">ConsentState | null</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Aktueller Consent-Status</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">acceptAll</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">() => Promise</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Akzeptiert alle Kategorien</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">rejectAll</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">() => Promise</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Lehnt alle ab (ausser essential)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">setConsent</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">(input) => Promise</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Setzt spezifische Kategorien</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">isBannerVisible</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">boolean</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Banner sichtbar?</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">showBanner</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">() => void</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Zeigt den Banner</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">showSettings</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">() => void</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Oeffnet Einstellungen</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Copy, Check } from 'lucide-react'
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
|
|
||||||
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-900 rounded-lg overflow-hidden">
|
|
||||||
{filename && (
|
|
||||||
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
|
|
||||||
{filename}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<CopyButton text={code} />
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function VueIntegrationPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-emerald-500 flex items-center justify-center">
|
|
||||||
<span className="text-white font-bold">V</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Vue 3 Integration</h1>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Composables und Plugin fuer Vue 3 und Nuxt Projekte.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Installation */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
|
|
||||||
<CodeBlock code="npm install @breakpilot/consent-sdk" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Plugin Setup */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Plugin Setup</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="main.ts"
|
|
||||||
code={`import { createApp } from 'vue';
|
|
||||||
import App from './App.vue';
|
|
||||||
import { ConsentPlugin } from '@breakpilot/consent-sdk/vue';
|
|
||||||
|
|
||||||
const app = createApp(App);
|
|
||||||
|
|
||||||
app.use(ConsentPlugin, {
|
|
||||||
apiEndpoint: import.meta.env.VITE_CONSENT_API,
|
|
||||||
siteId: 'my-site',
|
|
||||||
debug: import.meta.env.DEV,
|
|
||||||
});
|
|
||||||
|
|
||||||
app.mount('#app');`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Composable */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">useConsent Composable</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="components/Analytics.vue"
|
|
||||||
code={`<script setup lang="ts">
|
|
||||||
import { useConsent } from '@breakpilot/consent-sdk/vue';
|
|
||||||
|
|
||||||
const { hasConsent, acceptAll, rejectAll } = useConsent();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="hasConsent('analytics')">
|
|
||||||
<!-- Analytics Code hier -->
|
|
||||||
</div>
|
|
||||||
</template>`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Cookie Banner */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Cookie Banner Component</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="components/CookieBanner.vue"
|
|
||||||
code={`<script setup lang="ts">
|
|
||||||
import { useConsent } from '@breakpilot/consent-sdk/vue';
|
|
||||||
|
|
||||||
const {
|
|
||||||
isBannerVisible,
|
|
||||||
acceptAll,
|
|
||||||
rejectAll,
|
|
||||||
showSettings,
|
|
||||||
} = useConsent();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Transition name="slide">
|
|
||||||
<div
|
|
||||||
v-if="isBannerVisible"
|
|
||||||
class="fixed bottom-0 inset-x-0 bg-white border-t shadow-lg p-4 z-50"
|
|
||||||
>
|
|
||||||
<div class="max-w-4xl mx-auto flex items-center justify-between">
|
|
||||||
<p class="text-sm text-gray-600">
|
|
||||||
Wir verwenden Cookies um Ihr Erlebnis zu verbessern.
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
|
||||||
@click="rejectAll"
|
|
||||||
class="px-4 py-2 text-sm border rounded hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
Ablehnen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="showSettings"
|
|
||||||
class="px-4 py-2 text-sm border rounded hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
Einstellungen
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="acceptAll"
|
|
||||||
class="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
Alle akzeptieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.slide-enter-active,
|
|
||||||
.slide-leave-active {
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
.slide-enter-from,
|
|
||||||
.slide-leave-to {
|
|
||||||
transform: translateY(100%);
|
|
||||||
}
|
|
||||||
</style>`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ConsentGate */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">ConsentGate Component</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="components/YouTubeEmbed.vue"
|
|
||||||
code={`<script setup lang="ts">
|
|
||||||
import { ConsentGate } from '@breakpilot/consent-sdk/vue';
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
videoId: string;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ConsentGate category="social">
|
|
||||||
<template #default>
|
|
||||||
<iframe
|
|
||||||
:src="\`https://www.youtube.com/embed/\${videoId}\`"
|
|
||||||
width="560"
|
|
||||||
height="315"
|
|
||||||
allowfullscreen
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #fallback>
|
|
||||||
<div class="bg-gray-100 p-4 rounded-lg text-center">
|
|
||||||
<p>Video erfordert Ihre Zustimmung.</p>
|
|
||||||
<button class="mt-2 px-4 py-2 bg-blue-600 text-white rounded">
|
|
||||||
Zustimmen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ConsentGate>
|
|
||||||
</template>`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Nuxt */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Nuxt 3 Setup</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="plugins/consent.client.ts"
|
|
||||||
code={`import { ConsentPlugin } from '@breakpilot/consent-sdk/vue';
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.use(ConsentPlugin, {
|
|
||||||
apiEndpoint: useRuntimeConfig().public.consentApi,
|
|
||||||
siteId: 'my-site',
|
|
||||||
});
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Composable API */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Composable API</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Property
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Typ
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Beschreibung
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">hasConsent</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">(category) => boolean</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Reaktive Consent-Pruefung</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">consent</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Ref<ConsentState></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Reaktiver Consent-Status</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">isBannerVisible</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">Ref<boolean></code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Reaktive Banner-Sichtbarkeit</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">acceptAll</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">() => Promise</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Akzeptiert alle</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">rejectAll</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">() => Promise</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Lehnt alle ab</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">setConsent</code></td>
|
|
||||||
<td className="px-6 py-4"><code className="text-gray-500">(input) => Promise</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Setzt spezifische Kategorien</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Copy, Check, Info, AlertTriangle } from 'lucide-react'
|
|
||||||
|
|
||||||
type PackageManager = 'npm' | 'yarn' | 'pnpm'
|
|
||||||
|
|
||||||
const installCommands: Record<PackageManager, string> = {
|
|
||||||
npm: 'npm install @breakpilot/consent-sdk',
|
|
||||||
yarn: 'yarn add @breakpilot/consent-sdk',
|
|
||||||
pnpm: 'pnpm add @breakpilot/consent-sdk',
|
|
||||||
}
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
className="p-2 hover:bg-gray-700 rounded transition-colors"
|
|
||||||
title="Kopieren"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="w-4 h-4 text-green-400" />
|
|
||||||
) : (
|
|
||||||
<Copy className="w-4 h-4 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeBlock({ code, language = 'typescript' }: { code: string; language?: string }) {
|
|
||||||
return (
|
|
||||||
<div className="relative bg-gray-900 rounded-lg overflow-hidden">
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<CopyButton text={code} />
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function InfoBox({ type = 'info', children }: { type?: 'info' | 'warning'; children: React.ReactNode }) {
|
|
||||||
const styles = {
|
|
||||||
info: 'bg-blue-50 border-blue-200 text-blue-800',
|
|
||||||
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
|
|
||||||
}
|
|
||||||
const Icon = type === 'warning' ? AlertTriangle : Info
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`p-4 border rounded-lg ${styles[type]} flex items-start gap-3`}>
|
|
||||||
<Icon className="w-5 h-5 shrink-0 mt-0.5" />
|
|
||||||
<div className="text-sm">{children}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function InstallationPage() {
|
|
||||||
const [selectedPM, setSelectedPM] = useState<PackageManager>('npm')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Installation</h1>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Installieren Sie das Consent SDK in Ihrem Projekt.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Package Installation */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">NPM Package</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<div className="px-4 py-3 border-b border-gray-200 flex gap-1 bg-gray-50">
|
|
||||||
{(['npm', 'yarn', 'pnpm'] as const).map((pm) => (
|
|
||||||
<button
|
|
||||||
key={pm}
|
|
||||||
onClick={() => setSelectedPM(pm)}
|
|
||||||
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
|
|
||||||
selectedPM === pm
|
|
||||||
? 'bg-white text-gray-900 shadow-sm border border-gray-200'
|
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{pm}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-900 px-4 py-4 flex items-center justify-between">
|
|
||||||
<code className="text-green-400 font-mono text-sm">
|
|
||||||
$ {installCommands[selectedPM]}
|
|
||||||
</code>
|
|
||||||
<CopyButton text={installCommands[selectedPM]} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Framework-specific */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Framework-spezifische Imports</h2>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-gray-900 mb-2">Vanilla JavaScript</h3>
|
|
||||||
<CodeBlock
|
|
||||||
code={`import { ConsentManager } from '@breakpilot/consent-sdk';
|
|
||||||
|
|
||||||
const consent = new ConsentManager({
|
|
||||||
apiEndpoint: 'https://api.example.com/consent',
|
|
||||||
siteId: 'your-site-id',
|
|
||||||
});
|
|
||||||
|
|
||||||
await consent.init();`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-gray-900 mb-2">React</h3>
|
|
||||||
<CodeBlock
|
|
||||||
code={`import { ConsentProvider, useConsent } from '@breakpilot/consent-sdk/react';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
return (
|
|
||||||
<ConsentProvider
|
|
||||||
config={{
|
|
||||||
apiEndpoint: 'https://api.example.com/consent',
|
|
||||||
siteId: 'your-site-id',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<YourApp />
|
|
||||||
</ConsentProvider>
|
|
||||||
);
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-gray-900 mb-2">Vue 3</h3>
|
|
||||||
<CodeBlock
|
|
||||||
code={`import { createApp } from 'vue';
|
|
||||||
import { ConsentPlugin } from '@breakpilot/consent-sdk/vue';
|
|
||||||
|
|
||||||
const app = createApp(App);
|
|
||||||
|
|
||||||
app.use(ConsentPlugin, {
|
|
||||||
apiEndpoint: 'https://api.example.com/consent',
|
|
||||||
siteId: 'your-site-id',
|
|
||||||
});`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Script Blocking Setup */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Script Blocking einrichten</h2>
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Um Third-Party Scripts automatisch zu blockieren, verwenden Sie das{' '}
|
|
||||||
<code className="px-1.5 py-0.5 bg-gray-100 rounded text-sm">data-consent</code> Attribut:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<CodeBlock
|
|
||||||
language="html"
|
|
||||||
code={`<!-- Analytics Script (blockiert bis Consent) -->
|
|
||||||
<script
|
|
||||||
data-consent="analytics"
|
|
||||||
data-src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
|
|
||||||
type="text/plain"
|
|
||||||
></script>
|
|
||||||
|
|
||||||
<!-- Marketing Script (blockiert bis Consent) -->
|
|
||||||
<script data-consent="marketing" type="text/plain">
|
|
||||||
fbq('init', 'YOUR_PIXEL_ID');
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Embedded iFrame (blockiert bis Consent) -->
|
|
||||||
<iframe
|
|
||||||
data-consent="social"
|
|
||||||
data-src="https://www.youtube.com/embed/VIDEO_ID"
|
|
||||||
width="560"
|
|
||||||
height="315"
|
|
||||||
></iframe>`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Requirements */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Anforderung
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Minimum
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">Node.js</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">>= 18.0.0</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">React (optional)</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">>= 17.0.0</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">Vue (optional)</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">>= 3.0.0</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">TypeScript (optional)</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">>= 4.7.0</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Browser Support */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Browser-Unterstuetzung</h2>
|
|
||||||
<InfoBox type="info">
|
|
||||||
Das SDK unterstuetzt alle modernen Browser mit ES2017+ Unterstuetzung.
|
|
||||||
Fuer aeltere Browser wird ein automatischer Fallback fuer Crypto-Funktionen bereitgestellt.
|
|
||||||
</InfoBox>
|
|
||||||
<div className="mt-4 bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Browser
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Minimum Version
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">Chrome</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">>= 60</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">Firefox</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">>= 55</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">Safari</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">>= 11</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-900">Edge</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">>= 79 (Chromium)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Next Steps */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Naechste Schritte</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<a
|
|
||||||
href="/developers/sdk/consent/api-reference"
|
|
||||||
className="p-4 bg-white rounded-xl border border-gray-200 hover:border-violet-300 hover:shadow-md transition-all"
|
|
||||||
>
|
|
||||||
<h3 className="font-medium text-gray-900">API Referenz</h3>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
|
||||||
Vollstaendige Dokumentation aller Methoden und Konfigurationsoptionen.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="/developers/sdk/consent/frameworks"
|
|
||||||
className="p-4 bg-white rounded-xl border border-gray-200 hover:border-violet-300 hover:shadow-md transition-all"
|
|
||||||
>
|
|
||||||
<h3 className="font-medium text-gray-900">Framework Integration</h3>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">
|
|
||||||
Detaillierte Anleitungen fuer React, Vue und Angular.
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Copy, Check, Smartphone } from 'lucide-react'
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
|
|
||||||
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-900 rounded-lg overflow-hidden">
|
|
||||||
{filename && (
|
|
||||||
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
|
|
||||||
{filename}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<CopyButton text={code} />
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AndroidSDKPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-green-600 flex items-center justify-center">
|
|
||||||
<Smartphone className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Android SDK (Kotlin)</h1>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Native Kotlin SDK fuer Android API 26+ mit Jetpack Compose Unterstuetzung.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Requirements */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Kotlin Version</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">1.9+</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Min SDK</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">API 26 (Android 8.0)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Compile SDK</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">34+</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Installation */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="build.gradle.kts (Module)"
|
|
||||||
code={`dependencies {
|
|
||||||
implementation("com.breakpilot:consent-sdk:1.0.0")
|
|
||||||
|
|
||||||
// Fuer Jetpack Compose
|
|
||||||
implementation("com.breakpilot:consent-sdk-compose:1.0.0")
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Basic Setup */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Grundlegende Einrichtung</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="MyApplication.kt"
|
|
||||||
code={`import android.app.Application
|
|
||||||
import com.breakpilot.consent.ConsentManager
|
|
||||||
|
|
||||||
class MyApplication : Application() {
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
// Consent Manager konfigurieren
|
|
||||||
ConsentManager.configure(
|
|
||||||
context = this,
|
|
||||||
config = ConsentConfig(
|
|
||||||
apiEndpoint = "https://api.example.com/consent",
|
|
||||||
siteId = "my-android-app"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialisieren
|
|
||||||
lifecycleScope.launch {
|
|
||||||
ConsentManager.initialize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Jetpack Compose */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Jetpack Compose Integration</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="MainActivity.kt"
|
|
||||||
code={`import androidx.compose.runtime.*
|
|
||||||
import com.breakpilot.consent.compose.*
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MainScreen() {
|
|
||||||
val consent = rememberConsentState()
|
|
||||||
|
|
||||||
Column {
|
|
||||||
// Consent-abhaengige UI
|
|
||||||
if (consent.hasConsent(ConsentCategory.ANALYTICS)) {
|
|
||||||
AnalyticsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
Button(onClick = { consent.acceptAll() }) {
|
|
||||||
Text("Alle akzeptieren")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(onClick = { consent.rejectAll() }) {
|
|
||||||
Text("Alle ablehnen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consent Banner (automatisch angezeigt wenn noetig)
|
|
||||||
ConsentBanner()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConsentGate Composable
|
|
||||||
@Composable
|
|
||||||
fun ProtectedContent() {
|
|
||||||
ConsentGate(
|
|
||||||
category = ConsentCategory.MARKETING,
|
|
||||||
fallback = {
|
|
||||||
Text("Marketing-Zustimmung erforderlich")
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
MarketingContent()
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Traditional Android */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">View-basierte Integration</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="MainActivity.kt"
|
|
||||||
code={`import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.breakpilot.consent.ConsentManager
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
|
|
||||||
// Auf Consent-Aenderungen reagieren
|
|
||||||
lifecycleScope.launch {
|
|
||||||
ConsentManager.consentFlow.collect { state ->
|
|
||||||
updateUI(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Banner anzeigen wenn noetig
|
|
||||||
if (ConsentManager.needsConsent()) {
|
|
||||||
ConsentManager.showBanner(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateUI(state: ConsentState?) {
|
|
||||||
if (state?.hasConsent(ConsentCategory.ANALYTICS) == true) {
|
|
||||||
loadAnalytics()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onAcceptAllClick(view: View) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
ConsentManager.acceptAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* API Reference */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">API Referenz</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Methode</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">configure()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">SDK konfigurieren</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">initialize()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">SDK initialisieren (suspend)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">hasConsent()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Consent fuer Kategorie pruefen</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">consentFlow</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Flow fuer reaktive Updates</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">acceptAll()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Alle akzeptieren (suspend)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">rejectAll()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Alle ablehnen (suspend)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">setConsent()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Kategorien setzen (suspend)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">showBanner()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Banner als DialogFragment</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Copy, Check, Smartphone } from 'lucide-react'
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
|
|
||||||
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-900 rounded-lg overflow-hidden">
|
|
||||||
{filename && (
|
|
||||||
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
|
|
||||||
{filename}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<CopyButton text={code} />
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function FlutterSDKPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-blue-500 flex items-center justify-center">
|
|
||||||
<Smartphone className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Flutter SDK</h1>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Cross-Platform SDK fuer Flutter 3.16+ mit iOS, Android und Web Support.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Requirements */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Dart Version</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">3.0+</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Flutter Version</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">3.16+</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Plattformen</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">iOS, Android, Web</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Installation */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="pubspec.yaml"
|
|
||||||
code={`dependencies:
|
|
||||||
consent_sdk: ^1.0.0`}
|
|
||||||
/>
|
|
||||||
<div className="mt-4">
|
|
||||||
<CodeBlock code="flutter pub get" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Basic Setup */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Grundlegende Einrichtung</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="main.dart"
|
|
||||||
code={`import 'package:flutter/material.dart';
|
|
||||||
import 'package:consent_sdk/consent_sdk.dart';
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
|
||||||
|
|
||||||
// Consent SDK initialisieren
|
|
||||||
await ConsentManager.instance.initialize(
|
|
||||||
config: ConsentConfig(
|
|
||||||
apiEndpoint: 'https://api.example.com/consent',
|
|
||||||
siteId: 'my-flutter-app',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
runApp(const MyApp());
|
|
||||||
}
|
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
|
||||||
const MyApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return MaterialApp(
|
|
||||||
home: const ConsentWrapper(
|
|
||||||
child: HomeScreen(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Widget Usage */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Widget Integration</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="home_screen.dart"
|
|
||||||
code={`import 'package:flutter/material.dart';
|
|
||||||
import 'package:consent_sdk/consent_sdk.dart';
|
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
|
||||||
const HomeScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Column(
|
|
||||||
children: [
|
|
||||||
// StreamBuilder fuer reaktive Updates
|
|
||||||
StreamBuilder<ConsentState?>(
|
|
||||||
stream: ConsentManager.instance.consentStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final consent = snapshot.data;
|
|
||||||
|
|
||||||
if (consent?.hasConsent(ConsentCategory.analytics) ?? false) {
|
|
||||||
return const AnalyticsWidget();
|
|
||||||
}
|
|
||||||
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
// ConsentGate Widget
|
|
||||||
ConsentGate(
|
|
||||||
category: ConsentCategory.marketing,
|
|
||||||
fallback: const Center(
|
|
||||||
child: Text('Marketing-Zustimmung erforderlich'),
|
|
||||||
),
|
|
||||||
child: const MarketingWidget(),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Buttons
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => ConsentManager.instance.acceptAll(),
|
|
||||||
child: const Text('Alle akzeptieren'),
|
|
||||||
),
|
|
||||||
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => ConsentManager.instance.rejectAll(),
|
|
||||||
child: const Text('Alle ablehnen'),
|
|
||||||
),
|
|
||||||
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => ConsentManager.instance.showSettings(context),
|
|
||||||
child: const Text('Einstellungen'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Custom Banner */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Custom Cookie Banner</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="cookie_banner.dart"
|
|
||||||
code={`import 'package:flutter/material.dart';
|
|
||||||
import 'package:consent_sdk/consent_sdk.dart';
|
|
||||||
|
|
||||||
class CustomCookieBanner extends StatelessWidget {
|
|
||||||
const CustomCookieBanner({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return StreamBuilder<bool>(
|
|
||||||
stream: ConsentManager.instance.isBannerVisibleStream,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (!(snapshot.data ?? false)) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.1),
|
|
||||||
blurRadius: 10,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Wir verwenden Cookies um Ihr Erlebnis zu verbessern.',
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => ConsentManager.instance.rejectAll(),
|
|
||||||
child: const Text('Ablehnen'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => ConsentManager.instance.showSettings(context),
|
|
||||||
child: const Text('Einstellungen'),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => ConsentManager.instance.acceptAll(),
|
|
||||||
child: const Text('Alle akzeptieren'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* API Reference */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">API Referenz</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Methode/Property</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">initialize()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">SDK initialisieren (Future)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">hasConsent()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Consent pruefen</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">consentStream</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Stream fuer Consent-Updates</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">isBannerVisibleStream</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Stream fuer Banner-Sichtbarkeit</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">acceptAll()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Alle akzeptieren (Future)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">rejectAll()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Alle ablehnen (Future)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">setConsent()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Kategorien setzen (Future)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">showSettings()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Einstellungs-Dialog oeffnen</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Copy, Check, Apple } from 'lucide-react'
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
|
|
||||||
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-900 rounded-lg overflow-hidden">
|
|
||||||
{filename && (
|
|
||||||
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
|
|
||||||
{filename}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute top-2 right-2">
|
|
||||||
<CopyButton text={code} />
|
|
||||||
</div>
|
|
||||||
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function iOSSDKPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<div className="flex items-center gap-3 mb-2">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-gray-900 flex items-center justify-center">
|
|
||||||
<Apple className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">iOS SDK (Swift)</h1>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Native Swift SDK fuer iOS 15+ und iPadOS mit SwiftUI-Unterstuetzung.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Requirements */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Swift Version</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">5.9+</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">iOS Deployment Target</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">iOS 15.0+</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Xcode Version</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">15.0+</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Installation */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
|
|
||||||
<h3 className="font-medium text-gray-900 mb-2">Swift Package Manager</h3>
|
|
||||||
<CodeBlock
|
|
||||||
filename="Package.swift"
|
|
||||||
code={`dependencies: [
|
|
||||||
.package(url: "https://github.com/breakpilot/consent-sdk-ios.git", from: "1.0.0")
|
|
||||||
]`}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-gray-600 mt-4">
|
|
||||||
Oder in Xcode: File → Add Package Dependencies → URL eingeben
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Basic Usage */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Grundlegende Verwendung</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="AppDelegate.swift"
|
|
||||||
code={`import ConsentSDK
|
|
||||||
|
|
||||||
@main
|
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
||||||
func application(
|
|
||||||
_ application: UIApplication,
|
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
|
||||||
) -> Bool {
|
|
||||||
|
|
||||||
// Consent Manager konfigurieren
|
|
||||||
ConsentManager.shared.configure(
|
|
||||||
apiEndpoint: "https://api.example.com/consent",
|
|
||||||
siteId: "my-ios-app"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialisieren
|
|
||||||
Task {
|
|
||||||
await ConsentManager.shared.initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* SwiftUI Integration */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">SwiftUI Integration</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="ContentView.swift"
|
|
||||||
code={`import SwiftUI
|
|
||||||
import ConsentSDK
|
|
||||||
|
|
||||||
struct ContentView: View {
|
|
||||||
@StateObject private var consent = ConsentManager.shared
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
if consent.hasConsent(.analytics) {
|
|
||||||
AnalyticsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
Button("Alle akzeptieren") {
|
|
||||||
Task {
|
|
||||||
await consent.acceptAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.consentBanner() // Zeigt Banner automatisch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consent Gate Modifier
|
|
||||||
struct ProtectedView: View {
|
|
||||||
var body: some View {
|
|
||||||
Text("Geschuetzter Inhalt")
|
|
||||||
.requiresConsent(.marketing) {
|
|
||||||
// Fallback wenn kein Consent
|
|
||||||
Text("Marketing-Zustimmung erforderlich")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* UIKit Integration */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">UIKit Integration</h2>
|
|
||||||
<CodeBlock
|
|
||||||
filename="ViewController.swift"
|
|
||||||
code={`import UIKit
|
|
||||||
import ConsentSDK
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
class ViewController: UIViewController {
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
// Reaktiv auf Consent-Aenderungen reagieren
|
|
||||||
ConsentManager.shared.$consent
|
|
||||||
.receive(on: DispatchQueue.main)
|
|
||||||
.sink { [weak self] state in
|
|
||||||
self?.updateUI(consent: state)
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateUI(consent: ConsentState?) {
|
|
||||||
if consent?.hasConsent(.analytics) == true {
|
|
||||||
loadAnalytics()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func acceptAllTapped(_ sender: UIButton) {
|
|
||||||
Task {
|
|
||||||
await ConsentManager.shared.acceptAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Consent Categories */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Consent-Kategorien</h2>
|
|
||||||
<CodeBlock
|
|
||||||
code={`// Verfuegbare Kategorien
|
|
||||||
enum ConsentCategory {
|
|
||||||
case essential // Immer aktiv
|
|
||||||
case functional // Funktionale Features
|
|
||||||
case analytics // Statistik & Analyse
|
|
||||||
case marketing // Werbung & Tracking
|
|
||||||
case social // Social Media Integration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consent pruefen
|
|
||||||
if ConsentManager.shared.hasConsent(.analytics) {
|
|
||||||
// Analytics laden
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mehrere Kategorien pruefen
|
|
||||||
if ConsentManager.shared.hasConsent([.analytics, .marketing]) {
|
|
||||||
// Beide Kategorien haben Consent
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* API Reference */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">API Referenz</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Methode</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">configure()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">SDK konfigurieren</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">initialize()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">SDK initialisieren (async)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">hasConsent(_:)</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Consent fuer Kategorie pruefen</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">acceptAll()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Alle Kategorien akzeptieren (async)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">rejectAll()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Alle ablehnen (async)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">setConsent(_:)</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Spezifische Kategorien setzen (async)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">showBanner()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Banner anzeigen</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4"><code className="text-violet-600">exportConsent()</code></td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Consent-Daten exportieren (DSGVO)</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { ChevronRight, Apple, Smartphone } from 'lucide-react'
|
|
||||||
|
|
||||||
const platforms = [
|
|
||||||
{
|
|
||||||
name: 'iOS (Swift)',
|
|
||||||
href: '/developers/sdk/consent/mobile/ios',
|
|
||||||
description: 'Native Swift SDK fuer iOS 15+ und iPadOS',
|
|
||||||
features: ['Swift 5.9+', 'iOS 15.0+', 'SwiftUI Support', 'Combine Integration'],
|
|
||||||
color: 'bg-gray-900',
|
|
||||||
icon: Apple,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Android (Kotlin)',
|
|
||||||
href: '/developers/sdk/consent/mobile/android',
|
|
||||||
description: 'Native Kotlin SDK fuer Android API 26+',
|
|
||||||
features: ['Kotlin 1.9+', 'API 26+', 'Jetpack Compose', 'Coroutines'],
|
|
||||||
color: 'bg-green-600',
|
|
||||||
icon: Smartphone,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Flutter',
|
|
||||||
href: '/developers/sdk/consent/mobile/flutter',
|
|
||||||
description: 'Cross-Platform SDK fuer Flutter 3.16+',
|
|
||||||
features: ['Dart 3.0+', 'Flutter 3.16+', 'iOS & Android', 'Web Support'],
|
|
||||||
color: 'bg-blue-500',
|
|
||||||
icon: Smartphone,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function MobileSDKsPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Mobile SDKs</h1>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Native SDKs fuer iOS, Android und Flutter mit vollstaendiger DSGVO-Konformitaet.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
{platforms.map((platform) => (
|
|
||||||
<Link
|
|
||||||
key={platform.name}
|
|
||||||
href={platform.href}
|
|
||||||
className="block bg-white rounded-xl border border-gray-200 p-6 hover:border-violet-300 hover:shadow-md transition-all group"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className={`w-12 h-12 rounded-xl ${platform.color} flex items-center justify-center shrink-0`}>
|
|
||||||
<platform.icon className="w-6 h-6 text-white" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 group-hover:text-violet-600 transition-colors">
|
|
||||||
{platform.name}
|
|
||||||
</h2>
|
|
||||||
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-violet-600 transition-colors" />
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 mt-1">{platform.description}</p>
|
|
||||||
<div className="flex flex-wrap gap-2 mt-3">
|
|
||||||
{platform.features.map((feature) => (
|
|
||||||
<span
|
|
||||||
key={feature}
|
|
||||||
className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-md"
|
|
||||||
>
|
|
||||||
{feature}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cross-Platform Note */}
|
|
||||||
<div className="mt-8 p-4 bg-blue-50 border border-blue-200 rounded-xl">
|
|
||||||
<h3 className="font-medium text-blue-900">Cross-Platform Konsistenz</h3>
|
|
||||||
<p className="text-sm text-blue-700 mt-1">
|
|
||||||
Alle Mobile SDKs bieten dieselbe API-Oberflaeche wie das Web SDK.
|
|
||||||
Consent-Daten werden ueber die API synchronisiert, sodass Benutzer auf allen Geraeten
|
|
||||||
denselben Consent-Status haben.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React, { useState } from 'react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import {
|
|
||||||
Shield, Code, Download, Smartphone, FileCode, Lock,
|
|
||||||
ChevronRight, Copy, Check, Zap, Globe, Layers,
|
|
||||||
BookOpen, Terminal
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
|
|
||||||
type Framework = 'npm' | 'yarn' | 'pnpm'
|
|
||||||
|
|
||||||
const installCommands: Record<Framework, string> = {
|
|
||||||
npm: 'npm install @breakpilot/consent-sdk',
|
|
||||||
yarn: 'yarn add @breakpilot/consent-sdk',
|
|
||||||
pnpm: 'pnpm add @breakpilot/consent-sdk',
|
|
||||||
}
|
|
||||||
|
|
||||||
function CopyButton({ text }: { text: string }) {
|
|
||||||
const [copied, setCopied] = useState(false)
|
|
||||||
|
|
||||||
const handleCopy = async () => {
|
|
||||||
await navigator.clipboard.writeText(text)
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
className="p-2 hover:bg-gray-700 rounded transition-colors"
|
|
||||||
title="Kopieren"
|
|
||||||
>
|
|
||||||
{copied ? (
|
|
||||||
<Check className="w-4 h-4 text-green-400" />
|
|
||||||
) : (
|
|
||||||
<Copy className="w-4 h-4 text-gray-400" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ConsentSDKHubPage() {
|
|
||||||
const [selectedPM, setSelectedPM] = useState<Framework>('npm')
|
|
||||||
|
|
||||||
const quickLinks = [
|
|
||||||
{
|
|
||||||
title: 'Installation',
|
|
||||||
description: 'SDK in wenigen Minuten einrichten',
|
|
||||||
href: '/developers/sdk/consent/installation',
|
|
||||||
icon: Download,
|
|
||||||
color: 'bg-blue-500',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'API Referenz',
|
|
||||||
description: 'Vollstaendige API-Dokumentation',
|
|
||||||
href: '/developers/sdk/consent/api-reference',
|
|
||||||
icon: FileCode,
|
|
||||||
color: 'bg-purple-500',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Frameworks',
|
|
||||||
description: 'React, Vue, Angular Integration',
|
|
||||||
href: '/developers/sdk/consent/frameworks',
|
|
||||||
icon: Layers,
|
|
||||||
color: 'bg-green-500',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Mobile SDKs',
|
|
||||||
description: 'iOS, Android, Flutter',
|
|
||||||
href: '/developers/sdk/consent/mobile',
|
|
||||||
icon: Smartphone,
|
|
||||||
color: 'bg-orange-500',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Sicherheit',
|
|
||||||
description: 'Best Practices & Compliance',
|
|
||||||
href: '/developers/sdk/consent/security',
|
|
||||||
icon: Lock,
|
|
||||||
color: 'bg-red-500',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const features = [
|
|
||||||
{
|
|
||||||
title: 'DSGVO & TTDSG Konform',
|
|
||||||
description: 'Vollstaendige Unterstuetzung fuer EU-Datenschutzverordnungen mit revisionssicherer Consent-Speicherung.',
|
|
||||||
icon: Shield,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Google Consent Mode v2',
|
|
||||||
description: 'Native Integration mit automatischer Synchronisation zu Google Analytics und Ads.',
|
|
||||||
icon: Globe,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Script Blocking',
|
|
||||||
description: 'Automatisches Blockieren von Third-Party Scripts bis zur Einwilligung.',
|
|
||||||
icon: Code,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Multi-Platform',
|
|
||||||
description: 'Unterstuetzung fuer Web, PWA, iOS, Android und Flutter aus einer Codebasis.',
|
|
||||||
icon: Smartphone,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-5xl mx-auto px-8 py-12">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-12">
|
|
||||||
<div className="flex items-center gap-3 mb-4">
|
|
||||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-violet-600 to-purple-600 flex items-center justify-center">
|
|
||||||
<Shield className="w-7 h-7 text-white" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Consent SDK</h1>
|
|
||||||
<div className="flex items-center gap-2 mt-1">
|
|
||||||
<span className="px-2 py-0.5 bg-green-100 text-green-800 text-xs font-medium rounded-full">
|
|
||||||
v1.0.0
|
|
||||||
</span>
|
|
||||||
<span className="text-sm text-gray-500">DSGVO/TTDSG Compliant</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-lg text-gray-600 max-w-3xl">
|
|
||||||
Das Consent SDK ermoeglicht DSGVO-konforme Einwilligungsverwaltung fuer Web, PWA und Mobile Apps.
|
|
||||||
Mit nativer Unterstuetzung fuer React, Vue, Angular und Mobile Platforms.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Install */}
|
|
||||||
<div className="mb-12 bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
||||||
<h2 className="font-semibold text-gray-900">Schnellinstallation</h2>
|
|
||||||
<div className="flex gap-1 bg-gray-100 rounded-lg p-1">
|
|
||||||
{(['npm', 'yarn', 'pnpm'] as const).map((pm) => (
|
|
||||||
<button
|
|
||||||
key={pm}
|
|
||||||
onClick={() => setSelectedPM(pm)}
|
|
||||||
className={`px-3 py-1 text-sm rounded-md transition-colors ${
|
|
||||||
selectedPM === pm
|
|
||||||
? 'bg-white text-gray-900 shadow-sm'
|
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{pm}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-900 px-6 py-4 flex items-center justify-between">
|
|
||||||
<code className="text-green-400 font-mono text-sm">
|
|
||||||
$ {installCommands[selectedPM]}
|
|
||||||
</code>
|
|
||||||
<CopyButton text={installCommands[selectedPM]} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Links */}
|
|
||||||
<div className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Dokumentation</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{quickLinks.map((link) => (
|
|
||||||
<Link
|
|
||||||
key={link.href}
|
|
||||||
href={link.href}
|
|
||||||
className="group p-4 bg-white rounded-xl border border-gray-200 hover:border-violet-300 hover:shadow-md transition-all"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className={`w-10 h-10 rounded-lg ${link.color} flex items-center justify-center shrink-0`}>
|
|
||||||
<link.icon className="w-5 h-5 text-white" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-medium text-gray-900 group-hover:text-violet-600 transition-colors flex items-center gap-1">
|
|
||||||
{link.title}
|
|
||||||
<ChevronRight className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">{link.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Quick Start Code */}
|
|
||||||
<div className="mb-12 bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
|
||||||
<h2 className="font-semibold text-gray-900">Schnellstart</h2>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-900 p-6">
|
|
||||||
<pre className="text-sm text-gray-300 font-mono overflow-x-auto">
|
|
||||||
{`import { ConsentManager } from '@breakpilot/consent-sdk';
|
|
||||||
|
|
||||||
// Manager initialisieren
|
|
||||||
const consent = new ConsentManager({
|
|
||||||
apiEndpoint: 'https://api.example.com/consent',
|
|
||||||
siteId: 'your-site-id',
|
|
||||||
});
|
|
||||||
|
|
||||||
// SDK starten
|
|
||||||
await consent.init();
|
|
||||||
|
|
||||||
// Consent pruefen
|
|
||||||
if (consent.hasConsent('analytics')) {
|
|
||||||
// Analytics laden
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events abonnieren
|
|
||||||
consent.on('change', (state) => {
|
|
||||||
console.log('Consent geaendert:', state);
|
|
||||||
});`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Features */}
|
|
||||||
<div className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Features</h2>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{features.map((feature) => (
|
|
||||||
<div
|
|
||||||
key={feature.title}
|
|
||||||
className="p-4 bg-white rounded-xl border border-gray-200"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-violet-100 flex items-center justify-center shrink-0">
|
|
||||||
<feature.icon className="w-5 h-5 text-violet-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-gray-900">{feature.title}</h3>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">{feature.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Compliance Notice */}
|
|
||||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-xl">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<Shield className="w-5 h-5 text-blue-600 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-blue-900">DSGVO & TTDSG Compliance</h3>
|
|
||||||
<p className="text-sm text-blue-700 mt-1">
|
|
||||||
Das Consent SDK erfuellt alle Anforderungen der DSGVO (Art. 6, 7, 13, 14, 17, 20) und des TTDSG (§ 25).
|
|
||||||
Alle Einwilligungen werden revisionssicher gespeichert und koennen jederzeit exportiert werden.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import React from 'react'
|
|
||||||
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
|
|
||||||
import { Shield, Lock, Eye, Database, Key, AlertTriangle, CheckCircle } from 'lucide-react'
|
|
||||||
|
|
||||||
function SecurityCard({
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
icon: Icon,
|
|
||||||
items,
|
|
||||||
}: {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
icon: React.ComponentType<{ className?: string }>
|
|
||||||
items: string[]
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-violet-100 flex items-center justify-center shrink-0">
|
|
||||||
<Icon className="w-5 h-5 text-violet-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-gray-900">{title}</h3>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">{description}</p>
|
|
||||||
<ul className="mt-3 space-y-1">
|
|
||||||
{items.map((item, i) => (
|
|
||||||
<li key={i} className="flex items-center gap-2 text-sm text-gray-600">
|
|
||||||
<CheckCircle className="w-4 h-4 text-green-500 shrink-0" />
|
|
||||||
{item}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SecurityPage() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 flex">
|
|
||||||
<SDKDocsSidebar />
|
|
||||||
|
|
||||||
<main className="flex-1 ml-64">
|
|
||||||
<div className="max-w-4xl mx-auto px-8 py-12">
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Sicherheit & Compliance</h1>
|
|
||||||
<p className="text-lg text-gray-600 mb-8">
|
|
||||||
Best Practices fuer sichere Implementierung und DSGVO-konforme Nutzung des Consent SDK.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Security Features */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-6">Sicherheits-Features</h2>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<SecurityCard
|
|
||||||
title="Datenverschluesselung"
|
|
||||||
description="Alle Daten werden verschluesselt uebertragen und gespeichert."
|
|
||||||
icon={Lock}
|
|
||||||
items={[
|
|
||||||
'TLS 1.3 fuer alle API-Kommunikation',
|
|
||||||
'HMAC-Signatur fuer lokale Storage-Integritaet',
|
|
||||||
'Keine Klartextspeicherung sensibler Daten',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SecurityCard
|
|
||||||
title="Datenschutzkonformes Fingerprinting"
|
|
||||||
description="Anonymisiertes Fingerprinting ohne invasive Techniken."
|
|
||||||
icon={Eye}
|
|
||||||
items={[
|
|
||||||
'Kein Canvas/WebGL/Audio Fingerprinting',
|
|
||||||
'Nur anonymisierte Browser-Eigenschaften',
|
|
||||||
'SHA-256 Hash der Komponenten',
|
|
||||||
'Nicht eindeutig identifizierend',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SecurityCard
|
|
||||||
title="Sichere Speicherung"
|
|
||||||
description="Lokale Speicherung mit Manipulationsschutz."
|
|
||||||
icon={Database}
|
|
||||||
items={[
|
|
||||||
'Signierte localStorage-Eintraege',
|
|
||||||
'Automatische Signaturverifikation',
|
|
||||||
'HttpOnly Cookies fuer SSR',
|
|
||||||
'SameSite=Lax gegen CSRF',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SecurityCard
|
|
||||||
title="API-Sicherheit"
|
|
||||||
description="Sichere Backend-Kommunikation."
|
|
||||||
icon={Key}
|
|
||||||
items={[
|
|
||||||
'Request-Signierung mit Timestamp',
|
|
||||||
'Credentials-Include fuer Session-Cookies',
|
|
||||||
'CORS-Konfiguration erforderlich',
|
|
||||||
'Rate-Limiting auf Server-Seite',
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* DSGVO Compliance */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-6">DSGVO Compliance</h2>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
DSGVO Artikel
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
Anforderung
|
|
||||||
</th>
|
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
|
|
||||||
SDK-Unterstuetzung
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200">
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 6</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Rechtmaessigkeit der Verarbeitung</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
|
|
||||||
Vollstaendig
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 7</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Bedingungen fuer Einwilligung</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
|
|
||||||
Vollstaendig
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 13/14</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Informationspflichten</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
|
|
||||||
Vollstaendig
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 17</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Recht auf Loeschung</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
|
|
||||||
Vollstaendig
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 20</td>
|
|
||||||
<td className="px-6 py-4 text-sm text-gray-600">Datenportabilitaet</td>
|
|
||||||
<td className="px-6 py-4">
|
|
||||||
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
|
|
||||||
Vollstaendig
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* TTDSG Compliance */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-6">TTDSG Compliance</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center shrink-0">
|
|
||||||
<Shield className="w-5 h-5 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold text-gray-900">§ 25 TTDSG - Schutz der Privatsphaere</h3>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
|
||||||
Das SDK erfuellt alle Anforderungen des § 25 TTDSG (Telemediengesetz):
|
|
||||||
</p>
|
|
||||||
<ul className="mt-3 space-y-2">
|
|
||||||
<li className="flex items-start gap-2 text-sm text-gray-600">
|
|
||||||
<CheckCircle className="w-4 h-4 text-green-500 shrink-0 mt-0.5" />
|
|
||||||
<span>
|
|
||||||
<strong>Einwilligung vor Speicherung:</strong> Cookies und localStorage werden erst nach
|
|
||||||
Einwilligung gesetzt (ausser technisch notwendige).
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2 text-sm text-gray-600">
|
|
||||||
<CheckCircle className="w-4 h-4 text-green-500 shrink-0 mt-0.5" />
|
|
||||||
<span>
|
|
||||||
<strong>Informierte Einwilligung:</strong> Klare Kategorisierung und Beschreibung
|
|
||||||
aller Cookies und Tracker.
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-start gap-2 text-sm text-gray-600">
|
|
||||||
<CheckCircle className="w-4 h-4 text-green-500 shrink-0 mt-0.5" />
|
|
||||||
<span>
|
|
||||||
<strong>Widerrufsrecht:</strong> Jederzeit widerrufbare Einwilligung mit einem Klick.
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Best Practices */}
|
|
||||||
<section className="mb-12">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-6">Best Practices</h2>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
|
|
||||||
<h3 className="font-medium text-green-900 flex items-center gap-2">
|
|
||||||
<CheckCircle className="w-5 h-5" />
|
|
||||||
Empfohlen
|
|
||||||
</h3>
|
|
||||||
<ul className="mt-2 space-y-1 text-sm text-green-800">
|
|
||||||
<li>• HTTPS fuer alle API-Aufrufe verwenden</li>
|
|
||||||
<li>• Consent-Banner vor dem Laden von Third-Party Scripts anzeigen</li>
|
|
||||||
<li>• Alle Kategorien klar und verstaendlich beschreiben</li>
|
|
||||||
<li>• Ablehnen-Button gleichwertig zum Akzeptieren-Button darstellen</li>
|
|
||||||
<li>• Consent-Aenderungen serverseitig protokollieren</li>
|
|
||||||
<li>• Regelmaessige Ueberpruefung der Consent-Gultigkeit (recheckAfterDays)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
|
|
||||||
<h3 className="font-medium text-red-900 flex items-center gap-2">
|
|
||||||
<AlertTriangle className="w-5 h-5" />
|
|
||||||
Vermeiden
|
|
||||||
</h3>
|
|
||||||
<ul className="mt-2 space-y-1 text-sm text-red-800">
|
|
||||||
<li>• Dark Patterns (versteckte Ablehnen-Buttons)</li>
|
|
||||||
<li>• Pre-checked Consent-Optionen</li>
|
|
||||||
<li>• Tracking vor Einwilligung</li>
|
|
||||||
<li>• Cookie-Walls ohne echte Alternative</li>
|
|
||||||
<li>• Unklare oder irrefuehrende Kategoriebezeichnungen</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Audit Trail */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 mb-6">Audit Trail</h2>
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<p className="text-gray-600 mb-4">
|
|
||||||
Das SDK speichert fuer jeden Consent-Vorgang revisionssichere Daten:
|
|
||||||
</p>
|
|
||||||
<div className="bg-gray-50 rounded-lg p-4 font-mono text-sm">
|
|
||||||
<pre className="text-gray-700">
|
|
||||||
{`{
|
|
||||||
"consentId": "consent_abc123...",
|
|
||||||
"timestamp": "2024-01-15T10:30:00.000Z",
|
|
||||||
"categories": {
|
|
||||||
"essential": true,
|
|
||||||
"analytics": true,
|
|
||||||
"marketing": false
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"userAgent": "Mozilla/5.0...",
|
|
||||||
"language": "de-DE",
|
|
||||||
"platform": "web",
|
|
||||||
"screenResolution": "1920x1080"
|
|
||||||
},
|
|
||||||
"signature": "sha256=...",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}`}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500 mt-4">
|
|
||||||
Diese Daten werden sowohl lokal als auch auf dem Server gespeichert und koennen
|
|
||||||
jederzeit fuer Audits exportiert werden.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
import { DevPortalLayout, CodeBlock, InfoBox, ParameterTable } from '@/components/developers/DevPortalLayout'
|
|
||||||
|
|
||||||
export default function SDKInstallationPage() {
|
|
||||||
return (
|
|
||||||
<DevPortalLayout
|
|
||||||
title="SDK Installation"
|
|
||||||
description="Installationsanleitung fuer das AI Compliance SDK"
|
|
||||||
>
|
|
||||||
<h2>Voraussetzungen</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Node.js 18 oder hoeher</li>
|
|
||||||
<li>React 18+ / Next.js 14+</li>
|
|
||||||
<li>TypeScript 5.0+ (empfohlen)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Installation</h2>
|
|
||||||
<p>
|
|
||||||
Installieren Sie das SDK ueber Ihren bevorzugten Paketmanager:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="bash" filename="npm">
|
|
||||||
{`npm install @breakpilot/compliance-sdk`}
|
|
||||||
</CodeBlock>
|
|
||||||
<CodeBlock language="bash" filename="yarn">
|
|
||||||
{`yarn add @breakpilot/compliance-sdk`}
|
|
||||||
</CodeBlock>
|
|
||||||
<CodeBlock language="bash" filename="pnpm">
|
|
||||||
{`pnpm add @breakpilot/compliance-sdk`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Peer Dependencies</h2>
|
|
||||||
<p>
|
|
||||||
Das SDK hat folgende Peer Dependencies, die automatisch installiert werden sollten:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="json" filename="package.json">
|
|
||||||
{`{
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">=18.0.0",
|
|
||||||
"react-dom": ">=18.0.0"
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Zusaetzliche Pakete (optional)</h2>
|
|
||||||
<p>
|
|
||||||
Fuer erweiterte Funktionen koennen Sie folgende Pakete installieren:
|
|
||||||
</p>
|
|
||||||
<ParameterTable
|
|
||||||
parameters={[
|
|
||||||
{
|
|
||||||
name: 'jspdf',
|
|
||||||
type: 'npm package',
|
|
||||||
required: false,
|
|
||||||
description: 'Fuer PDF-Export (wird automatisch geladen wenn verfuegbar)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'jszip',
|
|
||||||
type: 'npm package',
|
|
||||||
required: false,
|
|
||||||
description: 'Fuer ZIP-Export aller Dokumente',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h2>TypeScript Konfiguration</h2>
|
|
||||||
<p>
|
|
||||||
Das SDK ist vollstaendig in TypeScript geschrieben. Stellen Sie sicher,
|
|
||||||
dass Ihre tsconfig.json folgende Optionen enthaelt:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="json" filename="tsconfig.json">
|
|
||||||
{`{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true
|
|
||||||
}
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Next.js Integration</h2>
|
|
||||||
<p>
|
|
||||||
Fuer Next.js 14+ mit App Router, fuegen Sie den Provider in Ihr Root-Layout ein:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="app/layout.tsx">
|
|
||||||
{`import { SDKProvider } from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html lang="de">
|
|
||||||
<body>
|
|
||||||
<SDKProvider
|
|
||||||
tenantId={process.env.NEXT_PUBLIC_TENANT_ID!}
|
|
||||||
apiKey={process.env.BREAKPILOT_API_KEY}
|
|
||||||
enableBackendSync={true}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
)
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<InfoBox type="warning" title="Wichtig fuer Server Components">
|
|
||||||
Der SDKProvider ist ein Client-Component. Wenn Sie Server Components
|
|
||||||
verwenden, wrappen Sie nur die Teile der App, die das SDK benoetigen.
|
|
||||||
</InfoBox>
|
|
||||||
|
|
||||||
<h2>Umgebungsvariablen</h2>
|
|
||||||
<p>
|
|
||||||
Erstellen Sie eine .env.local Datei mit folgenden Variablen:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="bash" filename=".env.local">
|
|
||||||
{`# Pflicht
|
|
||||||
NEXT_PUBLIC_TENANT_ID=your-tenant-id
|
|
||||||
|
|
||||||
# Optional (fuer Backend-Sync)
|
|
||||||
BREAKPILOT_API_KEY=sk_live_...
|
|
||||||
|
|
||||||
# Optional (fuer Self-Hosted)
|
|
||||||
NEXT_PUBLIC_SDK_API_URL=https://your-server.com/sdk/v1`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<InfoBox type="info" title="API Key Sicherheit">
|
|
||||||
Der API Key sollte niemals im Frontend-Code oder in NEXT_PUBLIC_ Variablen
|
|
||||||
erscheinen. Verwenden Sie Server-Side API Routes fuer authentifizierte Anfragen.
|
|
||||||
</InfoBox>
|
|
||||||
|
|
||||||
<h2>Verifizierung</h2>
|
|
||||||
<p>
|
|
||||||
Testen Sie die Installation mit einer einfachen Komponente:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="app/test/page.tsx">
|
|
||||||
{`'use client'
|
|
||||||
|
|
||||||
import { useSDK } from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
export default function TestPage() {
|
|
||||||
const { state, completionPercentage } = useSDK()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>SDK Test</h1>
|
|
||||||
<p>Fortschritt: {completionPercentage}%</p>
|
|
||||||
<p>Aktuelle Phase: {state.currentPhase}</p>
|
|
||||||
<p>Use Cases: {state.useCases.length}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Fehlerbehebung</h2>
|
|
||||||
|
|
||||||
<h3>Error: useSDK must be used within SDKProvider</h3>
|
|
||||||
<p>
|
|
||||||
Stellen Sie sicher, dass der SDKProvider das gesamte Layout umschliesst
|
|
||||||
und dass Sie {'\'use client\''} in Client-Komponenten verwenden.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Error: Module not found</h3>
|
|
||||||
<p>
|
|
||||||
Loeschen Sie node_modules und package-lock.json, dann reinstallieren:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="bash" filename="Terminal">
|
|
||||||
{`rm -rf node_modules package-lock.json
|
|
||||||
npm install`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h3>TypeScript Errors</h3>
|
|
||||||
<p>
|
|
||||||
Stellen Sie sicher, dass TypeScript 5.0+ installiert ist:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="bash" filename="Terminal">
|
|
||||||
{`npm install typescript@latest`}
|
|
||||||
</CodeBlock>
|
|
||||||
</DevPortalLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import { DevPortalLayout, CodeBlock, InfoBox } from '@/components/developers/DevPortalLayout'
|
|
||||||
|
|
||||||
export default function SDKOverviewPage() {
|
|
||||||
return (
|
|
||||||
<DevPortalLayout
|
|
||||||
title="SDK Documentation"
|
|
||||||
description="TypeScript SDK für React und Next.js Integration"
|
|
||||||
>
|
|
||||||
<h2>Übersicht</h2>
|
|
||||||
<p>
|
|
||||||
Das AI Compliance SDK ist ein TypeScript-Paket für die Integration des
|
|
||||||
Compliance-Workflows in React und Next.js Anwendungen. Es bietet:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>React Context Provider für State Management</li>
|
|
||||||
<li>Hooks für einfachen Zugriff auf Compliance-Daten</li>
|
|
||||||
<li>Automatische Synchronisation mit dem Backend</li>
|
|
||||||
<li>Offline-Support mit localStorage Fallback</li>
|
|
||||||
<li>Export-Funktionen (PDF, JSON, ZIP)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>Kernkomponenten</h2>
|
|
||||||
|
|
||||||
<h3>SDKProvider</h3>
|
|
||||||
<p>
|
|
||||||
Der Provider wrappet Ihre App und stellt den SDK-Kontext bereit:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="app/layout.tsx">
|
|
||||||
{`import { SDKProvider } from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
export default function Layout({ children }) {
|
|
||||||
return (
|
|
||||||
<SDKProvider
|
|
||||||
tenantId="your-tenant"
|
|
||||||
enableBackendSync={true}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SDKProvider>
|
|
||||||
)
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h3>useSDK Hook</h3>
|
|
||||||
<p>
|
|
||||||
Der Haupt-Hook für den Zugriff auf alle SDK-Funktionen:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="component.tsx">
|
|
||||||
{`import { useSDK } from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
const {
|
|
||||||
// State
|
|
||||||
state,
|
|
||||||
dispatch,
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
currentStep,
|
|
||||||
goToStep,
|
|
||||||
goToNextStep,
|
|
||||||
goToPreviousStep,
|
|
||||||
canGoNext,
|
|
||||||
canGoPrevious,
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
completionPercentage,
|
|
||||||
phase1Completion,
|
|
||||||
phase2Completion,
|
|
||||||
|
|
||||||
// Checkpoints
|
|
||||||
validateCheckpoint,
|
|
||||||
overrideCheckpoint,
|
|
||||||
getCheckpointStatus,
|
|
||||||
|
|
||||||
// Data Updates
|
|
||||||
updateUseCase,
|
|
||||||
addRisk,
|
|
||||||
updateControl,
|
|
||||||
|
|
||||||
// Persistence
|
|
||||||
saveState,
|
|
||||||
loadState,
|
|
||||||
|
|
||||||
// Demo Data
|
|
||||||
seedDemoData,
|
|
||||||
clearDemoData,
|
|
||||||
isDemoDataLoaded,
|
|
||||||
|
|
||||||
// Sync
|
|
||||||
syncState,
|
|
||||||
forceSyncToServer,
|
|
||||||
isOnline,
|
|
||||||
|
|
||||||
// Export
|
|
||||||
exportState,
|
|
||||||
|
|
||||||
// Command Bar
|
|
||||||
isCommandBarOpen,
|
|
||||||
setCommandBarOpen,
|
|
||||||
} = useSDK()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Progress: {completionPercentage}%</h1>
|
|
||||||
<button onClick={() => goToStep('risks')}>
|
|
||||||
Zur Risikoanalyse
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Types</h2>
|
|
||||||
<p>
|
|
||||||
Das SDK exportiert alle TypeScript-Types für volle Typsicherheit:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="types.ts">
|
|
||||||
{`import type {
|
|
||||||
// Core Types
|
|
||||||
SDKState,
|
|
||||||
SDKAction,
|
|
||||||
SDKStep,
|
|
||||||
SDKPhase,
|
|
||||||
|
|
||||||
// Use Cases
|
|
||||||
UseCaseAssessment,
|
|
||||||
AssessmentResult,
|
|
||||||
|
|
||||||
// Risk Management
|
|
||||||
Risk,
|
|
||||||
RiskSeverity,
|
|
||||||
RiskMitigation,
|
|
||||||
|
|
||||||
// Controls & Evidence
|
|
||||||
Control,
|
|
||||||
Evidence,
|
|
||||||
Requirement,
|
|
||||||
|
|
||||||
// Checkpoints
|
|
||||||
Checkpoint,
|
|
||||||
CheckpointStatus,
|
|
||||||
ValidationError,
|
|
||||||
|
|
||||||
// DSFA
|
|
||||||
DSFA,
|
|
||||||
DSFASection,
|
|
||||||
DSFAApproval,
|
|
||||||
|
|
||||||
// TOMs & VVT
|
|
||||||
TOM,
|
|
||||||
ProcessingActivity,
|
|
||||||
RetentionPolicy,
|
|
||||||
|
|
||||||
// AI Act
|
|
||||||
AIActResult,
|
|
||||||
AIActRiskCategory,
|
|
||||||
} from '@breakpilot/compliance-sdk'`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Utility Functions</h2>
|
|
||||||
<p>
|
|
||||||
Hilfreiche Funktionen für die Arbeit mit dem SDK:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="utils.ts">
|
|
||||||
{`import {
|
|
||||||
// Step Navigation
|
|
||||||
getStepById,
|
|
||||||
getStepByUrl,
|
|
||||||
getNextStep,
|
|
||||||
getPreviousStep,
|
|
||||||
getStepsForPhase,
|
|
||||||
|
|
||||||
// Risk Calculation
|
|
||||||
calculateRiskScore,
|
|
||||||
getRiskSeverityFromScore,
|
|
||||||
calculateResidualRisk,
|
|
||||||
|
|
||||||
// Progress
|
|
||||||
getCompletionPercentage,
|
|
||||||
getPhaseCompletionPercentage,
|
|
||||||
} from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
// Beispiel: Risk Score berechnen
|
|
||||||
const inherentRisk = calculateRiskScore(4, 5) // likelihood * impact = 20
|
|
||||||
const severity = getRiskSeverityFromScore(20) // 'CRITICAL'`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>API Client</h2>
|
|
||||||
<p>
|
|
||||||
Für direkten API-Zugriff ohne React Context:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="api.ts">
|
|
||||||
{`import {
|
|
||||||
getSDKApiClient,
|
|
||||||
SDKApiClient,
|
|
||||||
} from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
const client = getSDKApiClient('your-tenant-id')
|
|
||||||
|
|
||||||
// State laden
|
|
||||||
const state = await client.getState()
|
|
||||||
|
|
||||||
// State speichern
|
|
||||||
await client.saveState(updatedState)
|
|
||||||
|
|
||||||
// Checkpoint validieren
|
|
||||||
const result = await client.validateCheckpoint('CP-UC', state)
|
|
||||||
|
|
||||||
// Export
|
|
||||||
const blob = await client.exportState('pdf')`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>RAG & LLM Client</h2>
|
|
||||||
<p>
|
|
||||||
Zugriff auf die RAG-Suche und Dokumentengenerierung:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="rag.ts">
|
|
||||||
{`import {
|
|
||||||
getSDKBackendClient,
|
|
||||||
isLegalQuery,
|
|
||||||
} from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
const client = getSDKBackendClient()
|
|
||||||
|
|
||||||
// RAG-Suche
|
|
||||||
const results = await client.search('DSGVO Art. 5', 5)
|
|
||||||
console.log(results) // SearchResult[]
|
|
||||||
|
|
||||||
// Dokumentengenerierung
|
|
||||||
const dsfa = await client.generateDSFA(context)
|
|
||||||
const toms = await client.generateTOM(context)
|
|
||||||
const vvt = await client.generateVVT(context)
|
|
||||||
|
|
||||||
// Prüfen ob eine Query rechtliche Inhalte betrifft
|
|
||||||
if (isLegalQuery('Einwilligung DSGVO')) {
|
|
||||||
// RAG-Suche durchführen
|
|
||||||
}`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<h2>Export</h2>
|
|
||||||
<p>
|
|
||||||
Exportieren Sie Compliance-Daten in verschiedenen Formaten:
|
|
||||||
</p>
|
|
||||||
<CodeBlock language="typescript" filename="export.ts">
|
|
||||||
{`import { exportToPDF, exportToZIP, downloadExport } from '@breakpilot/compliance-sdk'
|
|
||||||
|
|
||||||
// PDF Export
|
|
||||||
const pdfBlob = await exportToPDF(state)
|
|
||||||
downloadExport(pdfBlob, 'compliance-report.pdf')
|
|
||||||
|
|
||||||
// ZIP Export (alle Dokumente)
|
|
||||||
const zipBlob = await exportToZIP(state)
|
|
||||||
downloadExport(zipBlob, 'compliance-export.zip')
|
|
||||||
|
|
||||||
// Über den Hook
|
|
||||||
const { exportState } = useSDK()
|
|
||||||
const blob = await exportState('pdf') // 'json' | 'pdf' | 'zip'`}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
<InfoBox type="success" title="Weitere Dokumentation">
|
|
||||||
<ul className="list-disc list-inside space-y-1">
|
|
||||||
<li>
|
|
||||||
<Link href="/developers/sdk/installation" className="text-blue-600 hover:underline">
|
|
||||||
Installation Guide
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link href="/developers/sdk/configuration" className="text-blue-600 hover:underline">
|
|
||||||
Konfigurationsoptionen
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link href="/developers/guides/phase1" className="text-blue-600 hover:underline">
|
|
||||||
Phase 1 Workflow Guide
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</InfoBox>
|
|
||||||
</DevPortalLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,839 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TYPES
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface CrawlSource {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
source_type: string
|
|
||||||
path: string
|
|
||||||
file_extensions: string[]
|
|
||||||
max_depth: number
|
|
||||||
exclude_patterns: string[]
|
|
||||||
enabled: boolean
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CrawlJob {
|
|
||||||
id: string
|
|
||||||
source_id: string
|
|
||||||
source_name?: string
|
|
||||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
|
|
||||||
job_type: 'full' | 'delta'
|
|
||||||
files_found: number
|
|
||||||
files_processed: number
|
|
||||||
files_new: number
|
|
||||||
files_changed: number
|
|
||||||
files_skipped: number
|
|
||||||
files_error: number
|
|
||||||
error_message?: string
|
|
||||||
started_at?: string
|
|
||||||
completed_at?: string
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CrawlDocument {
|
|
||||||
id: string
|
|
||||||
file_name: string
|
|
||||||
file_extension: string
|
|
||||||
file_size_bytes: number
|
|
||||||
classification: string | null
|
|
||||||
classification_confidence: number | null
|
|
||||||
classification_corrected: boolean
|
|
||||||
extraction_status: string
|
|
||||||
archived: boolean
|
|
||||||
ipfs_cid: string | null
|
|
||||||
first_seen_at: string
|
|
||||||
last_seen_at: string
|
|
||||||
version_count: number
|
|
||||||
source_name?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OnboardingReport {
|
|
||||||
id: string
|
|
||||||
total_documents_found: number
|
|
||||||
classification_breakdown: Record<string, number>
|
|
||||||
gaps: GapItem[]
|
|
||||||
compliance_score: number
|
|
||||||
gap_summary?: { critical: number; high: number; medium: number }
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GapItem {
|
|
||||||
id: string
|
|
||||||
category: string
|
|
||||||
description: string
|
|
||||||
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM'
|
|
||||||
regulation: string
|
|
||||||
requiredAction: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// API HELPERS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const TENANT_ID = '00000000-0000-0000-0000-000000000001' // Default tenant
|
|
||||||
|
|
||||||
async function api(path: string, options: RequestInit = {}) {
|
|
||||||
const res = await fetch(`/api/sdk/v1/crawler/${path}`, {
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-ID': TENANT_ID,
|
|
||||||
...options.headers,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (res.status === 204) return null
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CLASSIFICATION LABELS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const CLASSIFICATION_LABELS: Record<string, { label: string; color: string }> = {
|
|
||||||
VVT: { label: 'VVT', color: 'bg-blue-100 text-blue-700' },
|
|
||||||
TOM: { label: 'TOM', color: 'bg-green-100 text-green-700' },
|
|
||||||
DSE: { label: 'DSE', color: 'bg-purple-100 text-purple-700' },
|
|
||||||
AVV: { label: 'AVV', color: 'bg-orange-100 text-orange-700' },
|
|
||||||
DSFA: { label: 'DSFA', color: 'bg-red-100 text-red-700' },
|
|
||||||
Loeschkonzept: { label: 'Loeschkonzept', color: 'bg-yellow-100 text-yellow-700' },
|
|
||||||
Einwilligung: { label: 'Einwilligung', color: 'bg-pink-100 text-pink-700' },
|
|
||||||
Vertrag: { label: 'Vertrag', color: 'bg-indigo-100 text-indigo-700' },
|
|
||||||
Richtlinie: { label: 'Richtlinie', color: 'bg-teal-100 text-teal-700' },
|
|
||||||
Schulungsnachweis: { label: 'Schulung', color: 'bg-cyan-100 text-cyan-700' },
|
|
||||||
Sonstiges: { label: 'Sonstiges', color: 'bg-gray-100 text-gray-700' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALL_CLASSIFICATIONS = Object.keys(CLASSIFICATION_LABELS)
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TAB: QUELLEN (Sources)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function SourcesTab() {
|
|
||||||
const [sources, setSources] = useState<CrawlSource[]>([])
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [showForm, setShowForm] = useState(false)
|
|
||||||
const [formName, setFormName] = useState('')
|
|
||||||
const [formPath, setFormPath] = useState('')
|
|
||||||
const [testResult, setTestResult] = useState<Record<string, string>>({})
|
|
||||||
|
|
||||||
const loadSources = useCallback(async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const data = await api('sources')
|
|
||||||
setSources(data || [])
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
setLoading(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => { loadSources() }, [loadSources])
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
if (!formName || !formPath) return
|
|
||||||
await api('sources', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ name: formName, path: formPath }),
|
|
||||||
})
|
|
||||||
setFormName('')
|
|
||||||
setFormPath('')
|
|
||||||
setShowForm(false)
|
|
||||||
loadSources()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDelete = async (id: string) => {
|
|
||||||
await api(`sources/${id}`, { method: 'DELETE' })
|
|
||||||
loadSources()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggle = async (source: CrawlSource) => {
|
|
||||||
await api(`sources/${source.id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({ enabled: !source.enabled }),
|
|
||||||
})
|
|
||||||
loadSources()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleTest = async (id: string) => {
|
|
||||||
setTestResult(prev => ({ ...prev, [id]: 'testing...' }))
|
|
||||||
const result = await api(`sources/${id}/test`, { method: 'POST' })
|
|
||||||
setTestResult(prev => ({ ...prev, [id]: result?.message || 'Fehler' }))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Crawl-Quellen</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowForm(!showForm)}
|
|
||||||
className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700"
|
|
||||||
>
|
|
||||||
+ Neue Quelle
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showForm && (
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
||||||
<input
|
|
||||||
value={formName}
|
|
||||||
onChange={e => setFormName(e.target.value)}
|
|
||||||
placeholder="z.B. Compliance-Ordner"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Pfad (relativ zu /data/crawl)</label>
|
|
||||||
<input
|
|
||||||
value={formPath}
|
|
||||||
onChange={e => setFormPath(e.target.value)}
|
|
||||||
placeholder="z.B. compliance-docs"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button onClick={handleCreate} className="px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700">
|
|
||||||
Erstellen
|
|
||||||
</button>
|
|
||||||
<button onClick={() => setShowForm(false)} className="px-4 py-2 text-gray-600 text-sm hover:text-gray-800">
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="text-center py-12 text-gray-500">Laden...</div>
|
|
||||||
) : sources.length === 0 ? (
|
|
||||||
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
|
|
||||||
<p className="text-lg font-medium">Keine Quellen konfiguriert</p>
|
|
||||||
<p className="text-sm mt-1">Erstellen Sie eine Crawl-Quelle um Dokumente zu scannen.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{sources.map(s => (
|
|
||||||
<div key={s.id} className="bg-white rounded-xl border border-gray-200 p-5 flex items-center gap-4">
|
|
||||||
<div className={`w-3 h-3 rounded-full ${s.enabled ? 'bg-green-500' : 'bg-gray-300'}`} />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="font-medium text-gray-900">{s.name}</div>
|
|
||||||
<div className="text-sm text-gray-500 truncate">{s.path}</div>
|
|
||||||
<div className="text-xs text-gray-400 mt-1">
|
|
||||||
Tiefe: {s.max_depth} | Formate: {(typeof s.file_extensions === 'string' ? JSON.parse(s.file_extensions) : s.file_extensions).join(', ')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{testResult[s.id] && (
|
|
||||||
<span className="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">{testResult[s.id]}</span>
|
|
||||||
)}
|
|
||||||
<button onClick={() => handleTest(s.id)} className="text-sm text-blue-600 hover:text-blue-800">Testen</button>
|
|
||||||
<button onClick={() => handleToggle(s)} className="text-sm text-gray-600 hover:text-gray-800">
|
|
||||||
{s.enabled ? 'Deaktivieren' : 'Aktivieren'}
|
|
||||||
</button>
|
|
||||||
<button onClick={() => handleDelete(s.id)} className="text-sm text-red-600 hover:text-red-800">Loeschen</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TAB: CRAWL-JOBS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function JobsTab() {
|
|
||||||
const [jobs, setJobs] = useState<CrawlJob[]>([])
|
|
||||||
const [sources, setSources] = useState<CrawlSource[]>([])
|
|
||||||
const [selectedSource, setSelectedSource] = useState('')
|
|
||||||
const [jobType, setJobType] = useState<'full' | 'delta'>('full')
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const [j, s] = await Promise.all([api('jobs'), api('sources')])
|
|
||||||
setJobs(j || [])
|
|
||||||
setSources(s || [])
|
|
||||||
if (!selectedSource && s?.length > 0) setSelectedSource(s[0].id)
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
setLoading(false)
|
|
||||||
}, [selectedSource])
|
|
||||||
|
|
||||||
useEffect(() => { loadData() }, [loadData])
|
|
||||||
|
|
||||||
// Auto-refresh running jobs
|
|
||||||
useEffect(() => {
|
|
||||||
const hasRunning = jobs.some(j => j.status === 'running' || j.status === 'pending')
|
|
||||||
if (!hasRunning) return
|
|
||||||
const interval = setInterval(loadData, 3000)
|
|
||||||
return () => clearInterval(interval)
|
|
||||||
}, [jobs, loadData])
|
|
||||||
|
|
||||||
const handleTrigger = async () => {
|
|
||||||
if (!selectedSource) return
|
|
||||||
await api('jobs', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ source_id: selectedSource, job_type: jobType }),
|
|
||||||
})
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = async (id: string) => {
|
|
||||||
await api(`jobs/${id}/cancel`, { method: 'POST' })
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusColor = (s: string) => {
|
|
||||||
switch (s) {
|
|
||||||
case 'completed': return 'bg-green-100 text-green-700'
|
|
||||||
case 'running': return 'bg-blue-100 text-blue-700'
|
|
||||||
case 'pending': return 'bg-yellow-100 text-yellow-700'
|
|
||||||
case 'failed': return 'bg-red-100 text-red-700'
|
|
||||||
case 'cancelled': return 'bg-gray-100 text-gray-600'
|
|
||||||
default: return 'bg-gray-100 text-gray-700'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Trigger form */}
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<h3 className="font-medium text-gray-900 mb-4">Neuen Crawl starten</h3>
|
|
||||||
<div className="flex gap-4 items-end">
|
|
||||||
<div className="flex-1">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Quelle</label>
|
|
||||||
<select
|
|
||||||
value={selectedSource}
|
|
||||||
onChange={e => setSelectedSource(e.target.value)}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
{sources.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Typ</label>
|
|
||||||
<select
|
|
||||||
value={jobType}
|
|
||||||
onChange={e => setJobType(e.target.value as 'full' | 'delta')}
|
|
||||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
<option value="full">Voll-Scan</option>
|
|
||||||
<option value="delta">Delta-Scan</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={handleTrigger}
|
|
||||||
disabled={!selectedSource}
|
|
||||||
className="px-6 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
Crawl starten
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Job list */}
|
|
||||||
{loading ? (
|
|
||||||
<div className="text-center py-8 text-gray-500">Laden...</div>
|
|
||||||
) : jobs.length === 0 ? (
|
|
||||||
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
|
|
||||||
Noch keine Crawl-Jobs ausgefuehrt.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{jobs.map(job => (
|
|
||||||
<div key={job.id} className="bg-white rounded-xl border border-gray-200 p-5">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className={`px-2 py-1 text-xs font-medium rounded ${statusColor(job.status)}`}>
|
|
||||||
{job.status}
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium text-gray-900">{job.source_name || 'Quelle'}</span>
|
|
||||||
<span className="text-xs text-gray-400">{job.job_type === 'delta' ? 'Delta' : 'Voll'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{(job.status === 'running' || job.status === 'pending') && (
|
|
||||||
<button onClick={() => handleCancel(job.id)} className="text-xs text-red-600 hover:text-red-800">
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<span className="text-xs text-gray-400">
|
|
||||||
{new Date(job.created_at).toLocaleString('de-DE')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress */}
|
|
||||||
{job.status === 'running' && job.files_found > 0 && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-purple-600 rounded-full transition-all"
|
|
||||||
style={{ width: `${(job.files_processed / job.files_found) * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
|
||||||
{job.files_processed} / {job.files_found} Dateien verarbeitet
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="grid grid-cols-6 gap-2 text-center">
|
|
||||||
<div className="bg-gray-50 rounded-lg p-2">
|
|
||||||
<div className="text-lg font-bold text-gray-900">{job.files_found}</div>
|
|
||||||
<div className="text-xs text-gray-500">Gefunden</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 rounded-lg p-2">
|
|
||||||
<div className="text-lg font-bold text-gray-900">{job.files_processed}</div>
|
|
||||||
<div className="text-xs text-gray-500">Verarbeitet</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-green-50 rounded-lg p-2">
|
|
||||||
<div className="text-lg font-bold text-green-700">{job.files_new}</div>
|
|
||||||
<div className="text-xs text-green-600">Neu</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-blue-50 rounded-lg p-2">
|
|
||||||
<div className="text-lg font-bold text-blue-700">{job.files_changed}</div>
|
|
||||||
<div className="text-xs text-blue-600">Geaendert</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 rounded-lg p-2">
|
|
||||||
<div className="text-lg font-bold text-gray-500">{job.files_skipped}</div>
|
|
||||||
<div className="text-xs text-gray-500">Uebersprungen</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-red-50 rounded-lg p-2">
|
|
||||||
<div className="text-lg font-bold text-red-700">{job.files_error}</div>
|
|
||||||
<div className="text-xs text-red-600">Fehler</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TAB: DOKUMENTE
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function DocumentsTab() {
|
|
||||||
const [docs, setDocs] = useState<CrawlDocument[]>([])
|
|
||||||
const [total, setTotal] = useState(0)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [filterClass, setFilterClass] = useState('')
|
|
||||||
const [archiving, setArchiving] = useState<Record<string, boolean>>({})
|
|
||||||
|
|
||||||
const loadDocs = useCallback(async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const params = filterClass ? `?classification=${filterClass}` : ''
|
|
||||||
const data = await api(`documents${params}`)
|
|
||||||
setDocs(data?.documents || [])
|
|
||||||
setTotal(data?.total || 0)
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
setLoading(false)
|
|
||||||
}, [filterClass])
|
|
||||||
|
|
||||||
useEffect(() => { loadDocs() }, [loadDocs])
|
|
||||||
|
|
||||||
const handleReclassify = async (docId: string, newClass: string) => {
|
|
||||||
await api(`documents/${docId}/classify`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify({ classification: newClass }),
|
|
||||||
})
|
|
||||||
loadDocs()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleArchive = async (docId: string) => {
|
|
||||||
setArchiving(prev => ({ ...prev, [docId]: true }))
|
|
||||||
try {
|
|
||||||
await api(`documents/${docId}/archive`, { method: 'POST' })
|
|
||||||
loadDocs()
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
setArchiving(prev => ({ ...prev, [docId]: false }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatSize = (bytes: number) => {
|
|
||||||
if (bytes < 1024) return `${bytes} B`
|
|
||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
||||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">{total} Dokumente</h2>
|
|
||||||
<select
|
|
||||||
value={filterClass}
|
|
||||||
onChange={e => setFilterClass(e.target.value)}
|
|
||||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
||||||
>
|
|
||||||
<option value="">Alle Kategorien</option>
|
|
||||||
{ALL_CLASSIFICATIONS.map(c => (
|
|
||||||
<option key={c} value={c}>{CLASSIFICATION_LABELS[c]?.label || c}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="text-center py-12 text-gray-500">Laden...</div>
|
|
||||||
) : docs.length === 0 ? (
|
|
||||||
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
|
|
||||||
Keine Dokumente gefunden. Starten Sie zuerst einen Crawl-Job.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
||||||
<table className="w-full text-sm">
|
|
||||||
<thead className="bg-gray-50 text-gray-600">
|
|
||||||
<tr>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">Datei</th>
|
|
||||||
<th className="text-left px-4 py-3 font-medium">Kategorie</th>
|
|
||||||
<th className="text-center px-4 py-3 font-medium">Konfidenz</th>
|
|
||||||
<th className="text-right px-4 py-3 font-medium">Groesse</th>
|
|
||||||
<th className="text-center px-4 py-3 font-medium">Archiv</th>
|
|
||||||
<th className="text-right px-4 py-3 font-medium">Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-100">
|
|
||||||
{docs.map(doc => {
|
|
||||||
const cls = CLASSIFICATION_LABELS[doc.classification || ''] || CLASSIFICATION_LABELS['Sonstiges']
|
|
||||||
return (
|
|
||||||
<tr key={doc.id} className="hover:bg-gray-50">
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<div className="font-medium text-gray-900 truncate max-w-xs">{doc.file_name}</div>
|
|
||||||
<div className="text-xs text-gray-400">{doc.source_name}</div>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3">
|
|
||||||
<select
|
|
||||||
value={doc.classification || 'Sonstiges'}
|
|
||||||
onChange={e => handleReclassify(doc.id, e.target.value)}
|
|
||||||
className={`px-2 py-1 text-xs font-medium rounded border-0 ${cls.color}`}
|
|
||||||
>
|
|
||||||
{ALL_CLASSIFICATIONS.map(c => (
|
|
||||||
<option key={c} value={c}>{CLASSIFICATION_LABELS[c].label}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
{doc.classification_corrected && (
|
|
||||||
<span className="ml-1 text-xs text-orange-500" title="Manuell korrigiert">*</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-center">
|
|
||||||
{doc.classification_confidence != null && (
|
|
||||||
<div className="inline-flex items-center gap-1">
|
|
||||||
<div className="w-12 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-purple-500 rounded-full"
|
|
||||||
style={{ width: `${doc.classification_confidence * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500">
|
|
||||||
{(doc.classification_confidence * 100).toFixed(0)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-right text-gray-500">{formatSize(doc.file_size_bytes)}</td>
|
|
||||||
<td className="px-4 py-3 text-center">
|
|
||||||
{doc.archived ? (
|
|
||||||
<span className="text-green-600 text-xs font-medium" title={doc.ipfs_cid || ''}>IPFS</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-gray-400 text-xs">-</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-3 text-right">
|
|
||||||
{!doc.archived && (
|
|
||||||
<button
|
|
||||||
onClick={() => handleArchive(doc.id)}
|
|
||||||
disabled={archiving[doc.id]}
|
|
||||||
className="text-xs text-purple-600 hover:text-purple-800 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{archiving[doc.id] ? 'Archiviert...' : 'Archivieren'}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TAB: ONBOARDING-REPORT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function ReportTab() {
|
|
||||||
const [reports, setReports] = useState<OnboardingReport[]>([])
|
|
||||||
const [activeReport, setActiveReport] = useState<OnboardingReport | null>(null)
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [generating, setGenerating] = useState(false)
|
|
||||||
|
|
||||||
const loadReports = useCallback(async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const data = await api('reports')
|
|
||||||
setReports(data || [])
|
|
||||||
if (data?.length > 0 && !activeReport) {
|
|
||||||
const detail = await api(`reports/${data[0].id}`)
|
|
||||||
setActiveReport(detail)
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
setLoading(false)
|
|
||||||
}, [activeReport])
|
|
||||||
|
|
||||||
useEffect(() => { loadReports() }, [loadReports])
|
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
|
||||||
setGenerating(true)
|
|
||||||
try {
|
|
||||||
const result = await api('reports/generate', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({}),
|
|
||||||
})
|
|
||||||
setActiveReport(result)
|
|
||||||
loadReports()
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
setGenerating(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSelectReport = async (id: string) => {
|
|
||||||
const detail = await api(`reports/${id}`)
|
|
||||||
setActiveReport(detail)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compliance score ring
|
|
||||||
const ComplianceRing = ({ score }: { score: number }) => {
|
|
||||||
const radius = 50
|
|
||||||
const circumference = 2 * Math.PI * radius
|
|
||||||
const offset = circumference - (score / 100) * circumference
|
|
||||||
const color = score >= 75 ? '#16a34a' : score >= 50 ? '#f59e0b' : '#dc2626'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative w-36 h-36">
|
|
||||||
<svg className="w-full h-full -rotate-90">
|
|
||||||
<circle cx="68" cy="68" r={radius} fill="none" stroke="#e5e7eb" strokeWidth="8" />
|
|
||||||
<circle
|
|
||||||
cx="68" cy="68" r={radius} fill="none"
|
|
||||||
stroke={color} strokeWidth="8"
|
|
||||||
strokeDasharray={circumference}
|
|
||||||
strokeDashoffset={offset}
|
|
||||||
strokeLinecap="round"
|
|
||||||
className="transition-all duration-1000"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
||||||
<span className="text-3xl font-bold" style={{ color }}>{score.toFixed(0)}%</span>
|
|
||||||
<span className="text-xs text-gray-500">Compliance</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Onboarding-Report</h2>
|
|
||||||
<button
|
|
||||||
onClick={handleGenerate}
|
|
||||||
disabled={generating}
|
|
||||||
className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{generating ? 'Generiere...' : 'Neuen Report erstellen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Report selector */}
|
|
||||||
{reports.length > 1 && (
|
|
||||||
<div className="flex gap-2 overflow-x-auto pb-2">
|
|
||||||
{reports.map(r => (
|
|
||||||
<button
|
|
||||||
key={r.id}
|
|
||||||
onClick={() => handleSelectReport(r.id)}
|
|
||||||
className={`px-3 py-1.5 text-xs rounded-lg border whitespace-nowrap ${
|
|
||||||
activeReport?.id === r.id
|
|
||||||
? 'bg-purple-50 border-purple-300 text-purple-700'
|
|
||||||
: 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{new Date(r.created_at).toLocaleString('de-DE')} — {r.compliance_score.toFixed(0)}%
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="text-center py-12 text-gray-500">Laden...</div>
|
|
||||||
) : !activeReport ? (
|
|
||||||
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
|
|
||||||
<p className="text-lg font-medium">Kein Report vorhanden</p>
|
|
||||||
<p className="text-sm mt-1">Fuehren Sie zuerst einen Crawl durch und generieren Sie dann einen Report.</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Score + Stats */}
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<div className="flex items-center gap-8">
|
|
||||||
<ComplianceRing score={activeReport.compliance_score} />
|
|
||||||
<div className="flex-1 grid grid-cols-3 gap-4">
|
|
||||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
|
||||||
<div className="text-3xl font-bold text-gray-900">{activeReport.total_documents_found}</div>
|
|
||||||
<div className="text-sm text-gray-500">Dokumente gefunden</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
|
||||||
<div className="text-3xl font-bold text-gray-900">
|
|
||||||
{Object.keys(activeReport.classification_breakdown || {}).length}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">Kategorien abgedeckt</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-4 bg-gray-50 rounded-xl">
|
|
||||||
<div className="text-3xl font-bold text-red-600">
|
|
||||||
{(activeReport.gaps || []).length}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-500">Luecken identifiziert</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Classification breakdown */}
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<h3 className="font-medium text-gray-900 mb-4">Dokumenten-Verteilung</h3>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{Object.entries(activeReport.classification_breakdown || {}).map(([cat, count]) => {
|
|
||||||
const cls = CLASSIFICATION_LABELS[cat] || CLASSIFICATION_LABELS['Sonstiges']
|
|
||||||
return (
|
|
||||||
<span key={cat} className={`px-3 py-1.5 text-sm font-medium rounded-lg ${cls.color}`}>
|
|
||||||
{cls.label}: {count as number}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
{Object.keys(activeReport.classification_breakdown || {}).length === 0 && (
|
|
||||||
<span className="text-gray-400 text-sm">Keine Dokumente klassifiziert</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Gap summary */}
|
|
||||||
{activeReport.gap_summary && (
|
|
||||||
<div className="grid grid-cols-3 gap-4">
|
|
||||||
<div className="text-center p-4 bg-red-50 rounded-xl border border-red-100">
|
|
||||||
<div className="text-3xl font-bold text-red-600">{activeReport.gap_summary.critical}</div>
|
|
||||||
<div className="text-sm text-red-600 font-medium">Kritisch</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-4 bg-orange-50 rounded-xl border border-orange-100">
|
|
||||||
<div className="text-3xl font-bold text-orange-600">{activeReport.gap_summary.high}</div>
|
|
||||||
<div className="text-sm text-orange-600 font-medium">Hoch</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center p-4 bg-yellow-50 rounded-xl border border-yellow-100">
|
|
||||||
<div className="text-3xl font-bold text-yellow-600">{activeReport.gap_summary.medium}</div>
|
|
||||||
<div className="text-sm text-yellow-600 font-medium">Mittel</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Gap details */}
|
|
||||||
{(activeReport.gaps || []).length > 0 && (
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<h3 className="font-medium text-gray-900 mb-4">Compliance-Luecken</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{activeReport.gaps.map((gap) => (
|
|
||||||
<div
|
|
||||||
key={gap.id}
|
|
||||||
className={`p-4 rounded-lg border-l-4 ${
|
|
||||||
gap.severity === 'CRITICAL'
|
|
||||||
? 'bg-red-50 border-red-500'
|
|
||||||
: gap.severity === 'HIGH'
|
|
||||||
? 'bg-orange-50 border-orange-500'
|
|
||||||
: 'bg-yellow-50 border-yellow-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<div className="font-medium text-gray-900">{gap.category}</div>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">{gap.description}</p>
|
|
||||||
</div>
|
|
||||||
<span className={`px-2 py-1 text-xs font-medium rounded ${
|
|
||||||
gap.severity === 'CRITICAL' ? 'bg-red-100 text-red-700'
|
|
||||||
: gap.severity === 'HIGH' ? 'bg-orange-100 text-orange-700'
|
|
||||||
: 'bg-yellow-100 text-yellow-700'
|
|
||||||
}`}>
|
|
||||||
{gap.severity}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-xs text-gray-500">
|
|
||||||
Regulierung: {gap.regulation} | Aktion: {gap.requiredAction}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MAIN PAGE
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
type Tab = 'sources' | 'jobs' | 'documents' | 'report'
|
|
||||||
|
|
||||||
export default function DocumentCrawlerPage() {
|
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('sources')
|
|
||||||
|
|
||||||
const tabs: { id: Tab; label: string }[] = [
|
|
||||||
{ id: 'sources', label: 'Quellen' },
|
|
||||||
{ id: 'jobs', label: 'Crawl-Jobs' },
|
|
||||||
{ id: 'documents', label: 'Dokumente' },
|
|
||||||
{ id: 'report', label: 'Onboarding-Report' },
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-6xl mx-auto space-y-6">
|
|
||||||
{/* Header */}
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Document Crawler & Auto-Onboarding</h1>
|
|
||||||
<p className="mt-1 text-gray-500">
|
|
||||||
Automatisches Scannen von Dateisystemen, KI-Klassifizierung, IPFS-Archivierung und Compliance Gap-Analyse.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabs */}
|
|
||||||
<div className="border-b border-gray-200">
|
|
||||||
<nav className="flex gap-6">
|
|
||||||
{tabs.map(tab => (
|
|
||||||
<button
|
|
||||||
key={tab.id}
|
|
||||||
onClick={() => setActiveTab(tab.id)}
|
|
||||||
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
|
|
||||||
activeTab === tab.id
|
|
||||||
? 'border-purple-600 text-purple-600'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab content */}
|
|
||||||
{activeTab === 'sources' && <SourcesTab />}
|
|
||||||
{activeTab === 'jobs' && <JobsTab />}
|
|
||||||
{activeTab === 'documents' && <DocumentsTab />}
|
|
||||||
{activeTab === 'report' && <ReportTab />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,879 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Branchenspezifische Module (Phase 3.3)
|
|
||||||
*
|
|
||||||
* Industry-specific compliance template packages:
|
|
||||||
* - Browse industry templates (grid view)
|
|
||||||
* - View full detail with VVT, TOM, Risk tabs
|
|
||||||
* - Apply template packages to current compliance setup
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react'
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TYPES
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface IndustrySummary {
|
|
||||||
slug: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
icon: string
|
|
||||||
regulation_count: number
|
|
||||||
template_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IndustryTemplate {
|
|
||||||
slug: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
icon: string
|
|
||||||
regulations: string[]
|
|
||||||
vvt_templates: VVTTemplate[]
|
|
||||||
tom_recommendations: TOMRecommendation[]
|
|
||||||
risk_scenarios: RiskScenario[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VVTTemplate {
|
|
||||||
name: string
|
|
||||||
purpose: string
|
|
||||||
legal_basis: string
|
|
||||||
data_categories: string[]
|
|
||||||
data_subjects: string[]
|
|
||||||
retention_period: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TOMRecommendation {
|
|
||||||
category: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
priority: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RiskScenario {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
likelihood: string
|
|
||||||
impact: string
|
|
||||||
mitigation: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CONSTANTS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
type DetailTab = 'vvt' | 'tom' | 'risks'
|
|
||||||
|
|
||||||
const DETAIL_TABS: { key: DetailTab; label: string }[] = [
|
|
||||||
{ key: 'vvt', label: 'VVT-Vorlagen' },
|
|
||||||
{ key: 'tom', label: 'TOM-Empfehlungen' },
|
|
||||||
{ key: 'risks', label: 'Risiko-Szenarien' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const PRIORITY_COLORS: Record<string, { bg: string; text: string; border: string }> = {
|
|
||||||
critical: { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-200' },
|
|
||||||
high: { bg: 'bg-orange-50', text: 'text-orange-700', border: 'border-orange-200' },
|
|
||||||
medium: { bg: 'bg-yellow-50', text: 'text-yellow-700', border: 'border-yellow-200' },
|
|
||||||
low: { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-200' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRIORITY_LABELS: Record<string, string> = {
|
|
||||||
critical: 'Kritisch',
|
|
||||||
high: 'Hoch',
|
|
||||||
medium: 'Mittel',
|
|
||||||
low: 'Niedrig',
|
|
||||||
}
|
|
||||||
|
|
||||||
const LIKELIHOOD_COLORS: Record<string, string> = {
|
|
||||||
low: 'bg-green-500',
|
|
||||||
medium: 'bg-yellow-500',
|
|
||||||
high: 'bg-orange-500',
|
|
||||||
}
|
|
||||||
|
|
||||||
const IMPACT_COLORS: Record<string, string> = {
|
|
||||||
low: 'bg-green-500',
|
|
||||||
medium: 'bg-yellow-500',
|
|
||||||
high: 'bg-orange-500',
|
|
||||||
critical: 'bg-red-600',
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOM_CATEGORY_ICONS: Record<string, string> = {
|
|
||||||
'Zutrittskontrolle': '\uD83D\uDEAA',
|
|
||||||
'Zugangskontrolle': '\uD83D\uDD10',
|
|
||||||
'Zugriffskontrolle': '\uD83D\uDC65',
|
|
||||||
'Trennungskontrolle': '\uD83D\uDDC2\uFE0F',
|
|
||||||
'Pseudonymisierung': '\uD83C\uDFAD',
|
|
||||||
'Verschluesselung': '\uD83D\uDD12',
|
|
||||||
'Integritaet': '\u2705',
|
|
||||||
'Verfuegbarkeit': '\u2B06\uFE0F',
|
|
||||||
'Belastbarkeit': '\uD83D\uDEE1\uFE0F',
|
|
||||||
'Wiederherstellung': '\uD83D\uDD04',
|
|
||||||
'Datenschutz-Management': '\uD83D\uDCCB',
|
|
||||||
'Auftragsverarbeitung': '\uD83D\uDCDD',
|
|
||||||
'Incident Response': '\uD83D\uDEA8',
|
|
||||||
'Schulung': '\uD83C\uDF93',
|
|
||||||
'Netzwerksicherheit': '\uD83C\uDF10',
|
|
||||||
'Datensicherung': '\uD83D\uDCBE',
|
|
||||||
'Monitoring': '\uD83D\uDCCA',
|
|
||||||
'Physische Sicherheit': '\uD83C\uDFE2',
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// SKELETON COMPONENTS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function GridSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
{[1, 2, 3, 4].map((i) => (
|
|
||||||
<div key={i} className="bg-white rounded-xl border border-slate-200 p-6 animate-pulse">
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-14 h-14 rounded-xl bg-slate-200" />
|
|
||||||
<div className="flex-1 space-y-3">
|
|
||||||
<div className="h-5 bg-slate-200 rounded w-2/3" />
|
|
||||||
<div className="h-4 bg-slate-100 rounded w-full" />
|
|
||||||
<div className="h-4 bg-slate-100 rounded w-4/5" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 mt-5">
|
|
||||||
<div className="h-6 bg-slate-100 rounded-full w-28" />
|
|
||||||
<div className="h-6 bg-slate-100 rounded-full w-24" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DetailSkeleton() {
|
|
||||||
return (
|
|
||||||
<div className="space-y-6 animate-pulse">
|
|
||||||
<div className="bg-white rounded-xl border p-6 space-y-4">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="w-16 h-16 rounded-xl bg-slate-200" />
|
|
||||||
<div className="space-y-2 flex-1">
|
|
||||||
<div className="h-6 bg-slate-200 rounded w-1/3" />
|
|
||||||
<div className="h-4 bg-slate-100 rounded w-2/3" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="h-7 bg-slate-100 rounded-full w-20" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white rounded-xl border p-6 space-y-4">
|
|
||||||
<div className="flex gap-2 border-b pb-4">
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="h-9 bg-slate-100 rounded-lg w-32" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{[1, 2, 3].map((i) => (
|
|
||||||
<div key={i} className="h-28 bg-slate-50 rounded-lg" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MAIN PAGE COMPONENT
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export default function IndustryTemplatesPage() {
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// State
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const [industries, setIndustries] = useState<IndustrySummary[]>([])
|
|
||||||
const [selectedDetail, setSelectedDetail] = useState<IndustryTemplate | null>(null)
|
|
||||||
const [selectedSlug, setSelectedSlug] = useState<string | null>(null)
|
|
||||||
const [activeTab, setActiveTab] = useState<DetailTab>('vvt')
|
|
||||||
const [loading, setLoading] = useState(true)
|
|
||||||
const [detailLoading, setDetailLoading] = useState(false)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
const [detailError, setDetailError] = useState<string | null>(null)
|
|
||||||
const [applying, setApplying] = useState(false)
|
|
||||||
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Data fetching
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const loadIndustries = useCallback(async () => {
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/sdk/v1/industry/templates')
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
|
||||||
}
|
|
||||||
const data = await res.json()
|
|
||||||
setIndustries(Array.isArray(data) ? data : data.industries || data.templates || [])
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load industries:', err)
|
|
||||||
setError('Branchenvorlagen konnten nicht geladen werden. Bitte pruefen Sie die Verbindung zum Backend.')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const loadDetail = useCallback(async (slug: string) => {
|
|
||||||
setDetailLoading(true)
|
|
||||||
setDetailError(null)
|
|
||||||
setSelectedSlug(slug)
|
|
||||||
setActiveTab('vvt')
|
|
||||||
try {
|
|
||||||
const [detailRes, vvtRes, tomRes, risksRes] = await Promise.all([
|
|
||||||
fetch(`/api/sdk/v1/industry/templates/${slug}`),
|
|
||||||
fetch(`/api/sdk/v1/industry/templates/${slug}/vvt`),
|
|
||||||
fetch(`/api/sdk/v1/industry/templates/${slug}/tom`),
|
|
||||||
fetch(`/api/sdk/v1/industry/templates/${slug}/risks`),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!detailRes.ok) {
|
|
||||||
throw new Error(`HTTP ${detailRes.status}: ${detailRes.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const detail: IndustryTemplate = await detailRes.json()
|
|
||||||
|
|
||||||
// Merge sub-resources if the detail endpoint did not include them
|
|
||||||
if (vvtRes.ok) {
|
|
||||||
const vvtData = await vvtRes.json()
|
|
||||||
detail.vvt_templates = vvtData.vvt_templates || vvtData.templates || vvtData || []
|
|
||||||
}
|
|
||||||
if (tomRes.ok) {
|
|
||||||
const tomData = await tomRes.json()
|
|
||||||
detail.tom_recommendations = tomData.tom_recommendations || tomData.recommendations || tomData || []
|
|
||||||
}
|
|
||||||
if (risksRes.ok) {
|
|
||||||
const risksData = await risksRes.json()
|
|
||||||
detail.risk_scenarios = risksData.risk_scenarios || risksData.scenarios || risksData || []
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedDetail(detail)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load industry detail:', err)
|
|
||||||
setDetailError('Details konnten nicht geladen werden. Bitte versuchen Sie es erneut.')
|
|
||||||
} finally {
|
|
||||||
setDetailLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadIndustries()
|
|
||||||
}, [loadIndustries])
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Handlers
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const handleBackToGrid = useCallback(() => {
|
|
||||||
setSelectedSlug(null)
|
|
||||||
setSelectedDetail(null)
|
|
||||||
setDetailError(null)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleApplyPackage = useCallback(async () => {
|
|
||||||
if (!selectedDetail) return
|
|
||||||
setApplying(true)
|
|
||||||
try {
|
|
||||||
// Placeholder: In production this would POST to an import endpoint
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500))
|
|
||||||
setToastMessage(
|
|
||||||
`Branchenpaket "${selectedDetail.name}" wurde erfolgreich angewendet. ` +
|
|
||||||
`${selectedDetail.vvt_templates?.length || 0} VVT-Vorlagen, ` +
|
|
||||||
`${selectedDetail.tom_recommendations?.length || 0} TOM-Empfehlungen und ` +
|
|
||||||
`${selectedDetail.risk_scenarios?.length || 0} Risiko-Szenarien wurden importiert.`
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
setToastMessage('Fehler beim Anwenden des Branchenpakets. Bitte versuchen Sie es erneut.')
|
|
||||||
} finally {
|
|
||||||
setApplying(false)
|
|
||||||
}
|
|
||||||
}, [selectedDetail])
|
|
||||||
|
|
||||||
// Auto-dismiss toast
|
|
||||||
useEffect(() => {
|
|
||||||
if (!toastMessage) return
|
|
||||||
const timer = setTimeout(() => setToastMessage(null), 6000)
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}, [toastMessage])
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: Header
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderHeader = () => (
|
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white text-lg">
|
|
||||||
{'\uD83C\uDFED'}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-slate-900">Branchenspezifische Module</h1>
|
|
||||||
<p className="text-slate-500 mt-0.5">
|
|
||||||
Vorkonfigurierte Compliance-Pakete nach Branche
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: Error
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderError = (message: string, onRetry: () => void) => (
|
|
||||||
<div className="bg-red-50 border border-red-200 rounded-xl p-5 flex items-start gap-3">
|
|
||||||
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-red-700 font-medium">Fehler</p>
|
|
||||||
<p className="text-red-600 text-sm mt-1">{message}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onRetry}
|
|
||||||
className="px-4 py-1.5 text-sm font-medium text-red-700 bg-red-100 hover:bg-red-200 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Erneut versuchen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: Industry Grid
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderGrid = () => (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
||||||
{industries.map((industry) => (
|
|
||||||
<button
|
|
||||||
key={industry.slug}
|
|
||||||
onClick={() => loadDetail(industry.slug)}
|
|
||||||
className="bg-white rounded-xl border border-slate-200 p-6 text-left hover:border-emerald-300 hover:shadow-md transition-all duration-200 group"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-100 flex items-center justify-center text-3xl flex-shrink-0 group-hover:from-emerald-100 group-hover:to-teal-100 transition-colors">
|
|
||||||
{industry.icon}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="text-lg font-semibold text-slate-900 group-hover:text-emerald-700 transition-colors">
|
|
||||||
{industry.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-slate-500 mt-1 line-clamp-2">
|
|
||||||
{industry.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2 mt-4">
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-100">
|
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
||||||
</svg>
|
|
||||||
{industry.regulation_count} Regulierungen
|
|
||||||
</span>
|
|
||||||
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-teal-50 text-teal-700 border border-teal-100">
|
|
||||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
|
|
||||||
</svg>
|
|
||||||
{industry.template_count} Vorlagen
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: Detail View - Header
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderDetailHeader = () => {
|
|
||||||
if (!selectedDetail) return null
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
|
||||||
<button
|
|
||||||
onClick={handleBackToGrid}
|
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-slate-500 hover:text-emerald-600 transition-colors mb-4"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
Zurueck zur Uebersicht
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-4">
|
|
||||||
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-100 flex items-center justify-center text-4xl flex-shrink-0">
|
|
||||||
{selectedDetail.icon}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<h2 className="text-xl font-bold text-slate-900">{selectedDetail.name}</h2>
|
|
||||||
<p className="text-slate-500 mt-1">{selectedDetail.description}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Regulation Badges */}
|
|
||||||
{selectedDetail.regulations && selectedDetail.regulations.length > 0 && (
|
|
||||||
<div className="mt-4">
|
|
||||||
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-2">
|
|
||||||
Relevante Regulierungen
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{selectedDetail.regulations.map((reg) => (
|
|
||||||
<span
|
|
||||||
key={reg}
|
|
||||||
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200"
|
|
||||||
>
|
|
||||||
{reg}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Summary stats */}
|
|
||||||
<div className="grid grid-cols-3 gap-4 mt-5 pt-5 border-t border-slate-100">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-2xl font-bold text-emerald-600">
|
|
||||||
{selectedDetail.vvt_templates?.length || 0}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-500 mt-0.5">VVT-Vorlagen</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-2xl font-bold text-teal-600">
|
|
||||||
{selectedDetail.tom_recommendations?.length || 0}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-500 mt-0.5">TOM-Empfehlungen</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-2xl font-bold text-amber-600">
|
|
||||||
{selectedDetail.risk_scenarios?.length || 0}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-500 mt-0.5">Risiko-Szenarien</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: VVT Tab
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderVVTTab = () => {
|
|
||||||
const templates = selectedDetail?.vvt_templates || []
|
|
||||||
if (templates.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-12 text-slate-400">
|
|
||||||
<p className="text-lg">Keine VVT-Vorlagen verfuegbar</p>
|
|
||||||
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine Verarbeitungsvorlagen definiert.</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{templates.map((vvt, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="bg-white border border-slate-200 rounded-lg p-5 hover:border-emerald-200 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h4 className="font-semibold text-slate-900">{vvt.name}</h4>
|
|
||||||
<p className="text-sm text-slate-500 mt-1">{vvt.purpose}</p>
|
|
||||||
</div>
|
|
||||||
<span className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-slate-100 text-slate-600 flex-shrink-0">
|
|
||||||
{vvt.retention_period}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 pt-3 border-t border-slate-100">
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
||||||
{/* Legal Basis */}
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1">Rechtsgrundlage</p>
|
|
||||||
<p className="text-sm text-slate-700">{vvt.legal_basis}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Retention Period (mobile only, since shown in badge on desktop) */}
|
|
||||||
<div className="sm:hidden">
|
|
||||||
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1">Aufbewahrungsfrist</p>
|
|
||||||
<p className="text-sm text-slate-700">{vvt.retention_period}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Data Categories */}
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">Datenkategorien</p>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{vvt.data_categories.map((cat) => (
|
|
||||||
<span
|
|
||||||
key={cat}
|
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-emerald-50 text-emerald-700 border border-emerald-100"
|
|
||||||
>
|
|
||||||
{cat}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Data Subjects */}
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">Betroffene</p>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{vvt.data_subjects.map((sub) => (
|
|
||||||
<span
|
|
||||||
key={sub}
|
|
||||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-teal-50 text-teal-700 border border-teal-100"
|
|
||||||
>
|
|
||||||
{sub}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: TOM Tab
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderTOMTab = () => {
|
|
||||||
const recommendations = selectedDetail?.tom_recommendations || []
|
|
||||||
if (recommendations.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-12 text-slate-400">
|
|
||||||
<p className="text-lg">Keine TOM-Empfehlungen verfuegbar</p>
|
|
||||||
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine technisch-organisatorischen Massnahmen definiert.</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group by category
|
|
||||||
const grouped: Record<string, TOMRecommendation[]> = {}
|
|
||||||
recommendations.forEach((tom) => {
|
|
||||||
if (!grouped[tom.category]) {
|
|
||||||
grouped[tom.category] = []
|
|
||||||
}
|
|
||||||
grouped[tom.category].push(tom)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{Object.entries(grouped).map(([category, items]) => {
|
|
||||||
const icon = TOM_CATEGORY_ICONS[category] || '\uD83D\uDD27'
|
|
||||||
return (
|
|
||||||
<div key={category}>
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<span className="text-lg">{icon}</span>
|
|
||||||
<h4 className="font-semibold text-slate-800">{category}</h4>
|
|
||||||
<span className="text-xs text-slate-400 ml-1">({items.length})</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-3 ml-7">
|
|
||||||
{items.map((tom, idx) => {
|
|
||||||
const prio = PRIORITY_COLORS[tom.priority] || PRIORITY_COLORS.medium
|
|
||||||
const prioLabel = PRIORITY_LABELS[tom.priority] || tom.priority
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="bg-white border border-slate-200 rounded-lg p-4 hover:border-emerald-200 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-3">
|
|
||||||
<div className="flex-1">
|
|
||||||
<h5 className="font-medium text-slate-900">{tom.name}</h5>
|
|
||||||
<p className="text-sm text-slate-500 mt-1">{tom.description}</p>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold border flex-shrink-0 ${prio.bg} ${prio.text} ${prio.border}`}
|
|
||||||
>
|
|
||||||
{prioLabel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: Risk Tab
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderRiskTab = () => {
|
|
||||||
const scenarios = selectedDetail?.risk_scenarios || []
|
|
||||||
if (scenarios.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="text-center py-12 text-slate-400">
|
|
||||||
<p className="text-lg">Keine Risiko-Szenarien verfuegbar</p>
|
|
||||||
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine Risiko-Szenarien definiert.</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{scenarios.map((risk, idx) => {
|
|
||||||
const likelihoodColor = LIKELIHOOD_COLORS[risk.likelihood] || 'bg-slate-400'
|
|
||||||
const impactColor = IMPACT_COLORS[risk.impact] || 'bg-slate-400'
|
|
||||||
const likelihoodLabel = PRIORITY_LABELS[risk.likelihood] || risk.likelihood
|
|
||||||
const impactLabel = PRIORITY_LABELS[risk.impact] || risk.impact
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="bg-white border border-slate-200 rounded-lg p-5 hover:border-emerald-200 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between gap-4">
|
|
||||||
<h4 className="font-semibold text-slate-900">{risk.name}</h4>
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
|
||||||
{/* Likelihood badge */}
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className={`w-2.5 h-2.5 rounded-full ${likelihoodColor}`} />
|
|
||||||
<span className="text-xs text-slate-500">
|
|
||||||
Wahrsch.: <span className="font-medium text-slate-700">{likelihoodLabel}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-slate-300">|</span>
|
|
||||||
{/* Impact badge */}
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span className={`w-2.5 h-2.5 rounded-full ${impactColor}`} />
|
|
||||||
<span className="text-xs text-slate-500">
|
|
||||||
Auswirkung: <span className="font-medium text-slate-700">{impactLabel}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-slate-500 mt-2">{risk.description}</p>
|
|
||||||
|
|
||||||
{/* Mitigation */}
|
|
||||||
<div className="mt-3 pt-3 border-t border-slate-100">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<svg className="w-4 h-4 text-emerald-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
||||||
</svg>
|
|
||||||
<div>
|
|
||||||
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider">Massnahme</p>
|
|
||||||
<p className="text-sm text-slate-700 mt-0.5">{risk.mitigation}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: Detail Tabs + Content
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderDetailContent = () => {
|
|
||||||
if (!selectedDetail) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
||||||
{/* Tab Navigation */}
|
|
||||||
<div className="flex border-b border-slate-200 bg-slate-50">
|
|
||||||
{DETAIL_TABS.map((tab) => {
|
|
||||||
const isActive = activeTab === tab.key
|
|
||||||
let count = 0
|
|
||||||
if (tab.key === 'vvt') count = selectedDetail.vvt_templates?.length || 0
|
|
||||||
if (tab.key === 'tom') count = selectedDetail.tom_recommendations?.length || 0
|
|
||||||
if (tab.key === 'risks') count = selectedDetail.risk_scenarios?.length || 0
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={tab.key}
|
|
||||||
onClick={() => setActiveTab(tab.key)}
|
|
||||||
className={`flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${
|
|
||||||
isActive
|
|
||||||
? 'text-emerald-700 bg-white border-b-2 border-emerald-500'
|
|
||||||
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
{count > 0 && (
|
|
||||||
<span
|
|
||||||
className={`ml-1.5 inline-flex items-center justify-center px-1.5 py-0.5 rounded-full text-xs ${
|
|
||||||
isActive
|
|
||||||
? 'bg-emerald-100 text-emerald-700'
|
|
||||||
: 'bg-slate-200 text-slate-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tab Content */}
|
|
||||||
<div className="p-6">
|
|
||||||
{activeTab === 'vvt' && renderVVTTab()}
|
|
||||||
{activeTab === 'tom' && renderTOMTab()}
|
|
||||||
{activeTab === 'risks' && renderRiskTab()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Apply Button */}
|
|
||||||
<div className="px-6 py-4 border-t border-slate-200 bg-slate-50">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-sm text-slate-500">
|
|
||||||
Importiert alle Vorlagen, Empfehlungen und Szenarien in Ihr System.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={handleApplyPackage}
|
|
||||||
disabled={applying}
|
|
||||||
className={`inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-semibold text-white transition-all ${
|
|
||||||
applying
|
|
||||||
? 'bg-emerald-400 cursor-not-allowed'
|
|
||||||
: 'bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 shadow-sm hover:shadow-md'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{applying ? (
|
|
||||||
<>
|
|
||||||
<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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
|
||||||
</svg>
|
|
||||||
Wird angewendet...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
||||||
</svg>
|
|
||||||
Branchenpaket anwenden
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: Toast
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderToast = () => {
|
|
||||||
if (!toastMessage) return null
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-6 right-6 z-50 max-w-md animate-slide-up">
|
|
||||||
<div className="bg-slate-900 text-white rounded-xl shadow-2xl px-5 py-4 flex items-start gap-3">
|
|
||||||
<svg className="w-5 h-5 text-emerald-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<p className="text-sm leading-relaxed">{toastMessage}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => setToastMessage(null)}
|
|
||||||
className="text-slate-400 hover:text-white flex-shrink-0 ml-2"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Render: Empty state
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const renderEmptyState = () => (
|
|
||||||
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
|
|
||||||
<div className="w-16 h-16 rounded-2xl bg-emerald-50 flex items-center justify-center text-3xl mx-auto mb-4">
|
|
||||||
{'\uD83C\uDFED'}
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold text-slate-900">Keine Branchenvorlagen verfuegbar</h3>
|
|
||||||
<p className="text-slate-500 mt-2 max-w-md mx-auto">
|
|
||||||
Es sind derzeit keine branchenspezifischen Compliance-Pakete im System hinterlegt.
|
|
||||||
Bitte kontaktieren Sie den Administrator oder versuchen Sie es spaeter erneut.
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={loadIndustries}
|
|
||||||
className="mt-4 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors"
|
|
||||||
>
|
|
||||||
Erneut laden
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Main Render
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Inline keyframe for toast animation */}
|
|
||||||
<style>{`
|
|
||||||
@keyframes slide-up {
|
|
||||||
from { opacity: 0; transform: translateY(16px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
.animate-slide-up {
|
|
||||||
animation: slide-up 0.3s ease-out;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
{renderHeader()}
|
|
||||||
|
|
||||||
{/* Error state */}
|
|
||||||
{error && renderError(error, loadIndustries)}
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
{loading ? (
|
|
||||||
selectedSlug ? <DetailSkeleton /> : <GridSkeleton />
|
|
||||||
) : selectedSlug ? (
|
|
||||||
// Detail View
|
|
||||||
<div className="space-y-6">
|
|
||||||
{detailLoading ? (
|
|
||||||
<DetailSkeleton />
|
|
||||||
) : detailError ? (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
onClick={handleBackToGrid}
|
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-slate-500 hover:text-emerald-600 transition-colors"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
Zurueck zur Uebersicht
|
|
||||||
</button>
|
|
||||||
{renderError(detailError, () => loadDetail(selectedSlug))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{renderDetailHeader()}
|
|
||||||
{renderDetailContent()}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : industries.length === 0 && !error ? (
|
|
||||||
renderEmptyState()
|
|
||||||
) : (
|
|
||||||
renderGrid()
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Toast notification */}
|
|
||||||
{renderToast()}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,225 +0,0 @@
|
|||||||
/**
|
|
||||||
* Compliance Advisor Chat API
|
|
||||||
*
|
|
||||||
* Connects the ComplianceAdvisorWidget to:
|
|
||||||
* 1. RAG legal corpus search (klausur-service) for context
|
|
||||||
* 2. Ollama LLM (32B) for generating answers
|
|
||||||
*
|
|
||||||
* Streams the LLM response back as plain text.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
|
|
||||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
|
||||||
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
|
||||||
|
|
||||||
// SOUL system prompt (from agent-core/soul/compliance-advisor.soul.md)
|
|
||||||
const SYSTEM_PROMPT = `# Compliance Advisor Agent
|
|
||||||
|
|
||||||
## Identitaet
|
|
||||||
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
|
|
||||||
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
|
|
||||||
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
|
|
||||||
offiziellen Quellen und gibst praxisnahe Hinweise.
|
|
||||||
|
|
||||||
## Kernprinzipien
|
|
||||||
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
|
|
||||||
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
|
|
||||||
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
|
|
||||||
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
|
|
||||||
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
|
|
||||||
|
|
||||||
## Kompetenzbereich
|
|
||||||
- DSGVO Art. 1-99 + Erwaegsgruende
|
|
||||||
- BDSG (Bundesdatenschutzgesetz)
|
|
||||||
- AI Act (EU KI-Verordnung)
|
|
||||||
- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
|
|
||||||
- ePrivacy-Richtlinie
|
|
||||||
- DSK-Kurzpapiere (Nr. 1-20)
|
|
||||||
- SDM (Standard-Datenschutzmodell) V3.0
|
|
||||||
- BSI-Grundschutz (Basis-Kenntnisse)
|
|
||||||
- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
|
|
||||||
- ISO 27001/27701 (Ueberblick)
|
|
||||||
- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
|
|
||||||
- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
|
|
||||||
- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
|
|
||||||
|
|
||||||
## RAG-Nutzung
|
|
||||||
Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind
|
|
||||||
NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
|
|
||||||
Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
|
|
||||||
|
|
||||||
## Kommunikationsstil
|
|
||||||
- Sachlich, aber verstaendlich — kein Juristendeutsch
|
|
||||||
- Deutsch als Hauptsprache
|
|
||||||
- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
|
|
||||||
- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
|
|
||||||
- Praxisbeispiele wo hilfreich
|
|
||||||
- Kurze, praegnante Saetze
|
|
||||||
|
|
||||||
## Antwortformat
|
|
||||||
1. Kurze Zusammenfassung (1-2 Saetze)
|
|
||||||
2. Detaillierte Erklaerung
|
|
||||||
3. Praxishinweise / Handlungsempfehlungen
|
|
||||||
4. Quellenangaben (Artikel, Paragraph, Leitlinie)
|
|
||||||
|
|
||||||
## Einschraenkungen
|
|
||||||
- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
|
|
||||||
- Keine Garantien fuer Rechtssicherheit
|
|
||||||
- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
|
|
||||||
- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
|
|
||||||
- Keine Interpretation von Urteilen (nur Verweis)
|
|
||||||
|
|
||||||
## Eskalation
|
|
||||||
- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
|
|
||||||
- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
|
|
||||||
- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen`
|
|
||||||
|
|
||||||
interface RAGResult {
|
|
||||||
content: string
|
|
||||||
source_name: string
|
|
||||||
source_code: string
|
|
||||||
attribution_text: string
|
|
||||||
score: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query the DSFA RAG corpus for relevant documents
|
|
||||||
*/
|
|
||||||
async function queryRAG(query: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/search?query=${encodeURIComponent(query)}&top_k=5`
|
|
||||||
const res = await fetch(url, {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
console.warn('RAG search failed:', res.status)
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json()
|
|
||||||
|
|
||||||
if (data.results && data.results.length > 0) {
|
|
||||||
return data.results
|
|
||||||
.map(
|
|
||||||
(r: RAGResult, i: number) =>
|
|
||||||
`[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}`
|
|
||||||
)
|
|
||||||
.join('\n\n---\n\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('RAG query error (continuing without context):', error)
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { message, history = [], currentStep = 'default' } = body
|
|
||||||
|
|
||||||
if (!message || typeof message !== 'string') {
|
|
||||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Query RAG for relevant context
|
|
||||||
const ragContext = await queryRAG(message)
|
|
||||||
|
|
||||||
// 2. Build system prompt with RAG context
|
|
||||||
let systemContent = SYSTEM_PROMPT
|
|
||||||
|
|
||||||
if (ragContext) {
|
|
||||||
systemContent += `\n\n## Relevanter Kontext aus dem RAG-System\n\nNutze die folgenden Quellen fuer deine Antwort. Verweise in deiner Antwort auf die jeweilige Quelle:\n\n${ragContext}`
|
|
||||||
}
|
|
||||||
|
|
||||||
systemContent += `\n\n## Aktueller SDK-Schritt\nDer Nutzer befindet sich im SDK-Schritt: ${currentStep}`
|
|
||||||
|
|
||||||
// 3. Build messages array (limit history to last 10 messages)
|
|
||||||
const messages = [
|
|
||||||
{ role: 'system', content: systemContent },
|
|
||||||
...history.slice(-10).map((h: { role: string; content: string }) => ({
|
|
||||||
role: h.role === 'user' ? 'user' : 'assistant',
|
|
||||||
content: h.content,
|
|
||||||
})),
|
|
||||||
{ role: 'user', content: message },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 4. Call Ollama with streaming
|
|
||||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: LLM_MODEL,
|
|
||||||
messages,
|
|
||||||
stream: true,
|
|
||||||
options: {
|
|
||||||
temperature: 0.3,
|
|
||||||
num_predict: 8192,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
signal: AbortSignal.timeout(120000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!ollamaResponse.ok) {
|
|
||||||
const errorText = await ollamaResponse.text()
|
|
||||||
console.error('Ollama error:', ollamaResponse.status, errorText)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status}). Ist Ollama mit dem Modell ${LLM_MODEL} gestartet?` },
|
|
||||||
{ status: 502 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Stream response back as plain text
|
|
||||||
const encoder = new TextEncoder()
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
const reader = ollamaResponse.body!.getReader()
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
|
|
||||||
const chunk = decoder.decode(value, { stream: true })
|
|
||||||
const lines = chunk.split('\n').filter((l) => l.trim())
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(line)
|
|
||||||
if (json.message?.content) {
|
|
||||||
controller.enqueue(encoder.encode(json.message.content))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Partial JSON line, skip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Stream read error:', error)
|
|
||||||
} finally {
|
|
||||||
controller.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return new NextResponse(stream, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Compliance advisor chat error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum LLM fehlgeschlagen. Bitte pruefen Sie ob Ollama laeuft.' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
/**
|
|
||||||
* Drafting Engine Chat API
|
|
||||||
*
|
|
||||||
* Verbindet das DraftingEngineWidget mit dem LLM Backend.
|
|
||||||
* Unterstuetzt alle 4 Modi: explain, ask, draft, validate.
|
|
||||||
* Nutzt State-Projection fuer token-effiziente Kontextgabe.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
|
|
||||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
|
||||||
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
|
||||||
|
|
||||||
// SOUL System Prompt (from agent-core/soul/drafting-agent.soul.md)
|
|
||||||
const DRAFTING_SYSTEM_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf
|
|
||||||
|
|
||||||
## Identitaet
|
|
||||||
Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK,
|
|
||||||
DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und
|
|
||||||
Konsistenz zwischen Dokumenten sicherzustellen.
|
|
||||||
|
|
||||||
## Strikte Constraints
|
|
||||||
- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen
|
|
||||||
- Das bestimmte Level ist bindend fuer die Dokumenttiefe
|
|
||||||
- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung
|
|
||||||
- Kommuniziere auf Deutsch, sachlich und verstaendlich
|
|
||||||
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung
|
|
||||||
|
|
||||||
## Kompetenzbereich
|
|
||||||
DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM V3.0, BSI-Grundschutz, ISO 27001/27701, EDPB Guidelines, WP248`
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Query the RAG corpus for relevant documents
|
|
||||||
*/
|
|
||||||
async function queryRAG(query: string): Promise<string> {
|
|
||||||
try {
|
|
||||||
const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/search?query=${encodeURIComponent(query)}&top_k=3`
|
|
||||||
const res = await fetch(url, {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) return ''
|
|
||||||
|
|
||||||
const data = await res.json()
|
|
||||||
if (data.results?.length > 0) {
|
|
||||||
return data.results
|
|
||||||
.map(
|
|
||||||
(r: { source_name?: string; source_code?: string; content?: string }, i: number) =>
|
|
||||||
`[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}`
|
|
||||||
)
|
|
||||||
.join('\n\n---\n\n')
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
} catch {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const {
|
|
||||||
message,
|
|
||||||
history = [],
|
|
||||||
sdkStateProjection,
|
|
||||||
mode = 'explain',
|
|
||||||
documentType,
|
|
||||||
} = body
|
|
||||||
|
|
||||||
if (!message || typeof message !== 'string') {
|
|
||||||
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Query RAG for legal context
|
|
||||||
const ragContext = await queryRAG(message)
|
|
||||||
|
|
||||||
// 2. Build system prompt with mode-specific instructions + state projection
|
|
||||||
let systemContent = DRAFTING_SYSTEM_PROMPT
|
|
||||||
|
|
||||||
// Mode-specific instructions
|
|
||||||
const modeInstructions: Record<string, string> = {
|
|
||||||
explain: '\n\n## Aktueller Modus: EXPLAIN\nBeantworte Fragen verstaendlich mit Quellenangaben.',
|
|
||||||
ask: '\n\n## Aktueller Modus: ASK\nAnalysiere Luecken und stelle gezielte Fragen. Eine Frage pro Antwort.',
|
|
||||||
draft: `\n\n## Aktueller Modus: DRAFT\nEntwirf strukturierte Dokument-Sections. Dokumenttyp: ${documentType || 'nicht spezifiziert'}.\nAntworte mit JSON wenn ein Draft angefragt wird.`,
|
|
||||||
validate: '\n\n## Aktueller Modus: VALIDATE\nPruefe Cross-Dokument-Konsistenz. Gib Errors, Warnings und Suggestions zurueck.',
|
|
||||||
}
|
|
||||||
systemContent += modeInstructions[mode] || modeInstructions.explain
|
|
||||||
|
|
||||||
// Add state projection context
|
|
||||||
if (sdkStateProjection) {
|
|
||||||
systemContent += `\n\n## SDK-State Projektion (${mode}-Kontext)\n${JSON.stringify(sdkStateProjection, null, 0).slice(0, 3000)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add RAG context
|
|
||||||
if (ragContext) {
|
|
||||||
systemContent += `\n\n## Relevanter Rechtskontext\n${ragContext}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Build messages array
|
|
||||||
const messages = [
|
|
||||||
{ role: 'system', content: systemContent },
|
|
||||||
...history.slice(-10).map((h: { role: string; content: string }) => ({
|
|
||||||
role: h.role === 'user' ? 'user' : 'assistant',
|
|
||||||
content: h.content,
|
|
||||||
})),
|
|
||||||
{ role: 'user', content: message },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 4. Call LLM with streaming
|
|
||||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: LLM_MODEL,
|
|
||||||
messages,
|
|
||||||
stream: true,
|
|
||||||
options: {
|
|
||||||
temperature: mode === 'draft' ? 0.2 : 0.3,
|
|
||||||
num_predict: mode === 'draft' ? 16384 : 8192,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
signal: AbortSignal.timeout(120000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!ollamaResponse.ok) {
|
|
||||||
const errorText = await ollamaResponse.text()
|
|
||||||
console.error('LLM error:', ollamaResponse.status, errorText)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` },
|
|
||||||
{ status: 502 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Stream response back
|
|
||||||
const encoder = new TextEncoder()
|
|
||||||
const stream = new ReadableStream({
|
|
||||||
async start(controller) {
|
|
||||||
const reader = ollamaResponse.body!.getReader()
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
|
|
||||||
const chunk = decoder.decode(value, { stream: true })
|
|
||||||
const lines = chunk.split('\n').filter((l) => l.trim())
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(line)
|
|
||||||
if (json.message?.content) {
|
|
||||||
controller.enqueue(encoder.encode(json.message.content))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Partial JSON, skip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Stream error:', error)
|
|
||||||
} finally {
|
|
||||||
controller.close()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return new NextResponse(stream, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain; charset=utf-8',
|
|
||||||
'Cache-Control': 'no-cache',
|
|
||||||
'Connection': 'keep-alive',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Drafting engine chat error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum LLM fehlgeschlagen.' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
/**
|
|
||||||
* Drafting Engine - Draft API
|
|
||||||
*
|
|
||||||
* Erstellt strukturierte Compliance-Dokument-Entwuerfe.
|
|
||||||
* Baut dokument-spezifische Prompts aus SOUL-Template + State-Projection.
|
|
||||||
* Gibt strukturiertes JSON zurueck.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
|
||||||
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
|
||||||
|
|
||||||
// Import prompt builders
|
|
||||||
import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt'
|
|
||||||
import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom'
|
|
||||||
import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa'
|
|
||||||
import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy'
|
|
||||||
import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen'
|
|
||||||
import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types'
|
|
||||||
import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types'
|
|
||||||
import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer'
|
|
||||||
|
|
||||||
const constraintEnforcer = new ConstraintEnforcer()
|
|
||||||
|
|
||||||
const DRAFTING_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe.
|
|
||||||
Du MUSST immer im JSON-Format antworten mit einem "sections" Array.
|
|
||||||
Jede Section hat: id, title, content, schemaField.
|
|
||||||
Halte die Tiefe strikt am vorgegebenen Level.
|
|
||||||
Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung].
|
|
||||||
Sprache: Deutsch.`
|
|
||||||
|
|
||||||
function buildPromptForDocumentType(
|
|
||||||
documentType: ScopeDocumentType,
|
|
||||||
context: DraftContext,
|
|
||||||
instructions?: string
|
|
||||||
): string {
|
|
||||||
switch (documentType) {
|
|
||||||
case 'vvt':
|
|
||||||
return buildVVTDraftPrompt({ context, instructions })
|
|
||||||
case 'tom':
|
|
||||||
return buildTOMDraftPrompt({ context, instructions })
|
|
||||||
case 'dsfa':
|
|
||||||
return buildDSFADraftPrompt({ context, instructions })
|
|
||||||
case 'dsi':
|
|
||||||
return buildPrivacyPolicyDraftPrompt({ context, instructions })
|
|
||||||
case 'lf':
|
|
||||||
return buildLoeschfristenDraftPrompt({ context, instructions })
|
|
||||||
default:
|
|
||||||
return `## Aufgabe: Entwurf fuer ${documentType}
|
|
||||||
|
|
||||||
### Level: ${context.decisions.level}
|
|
||||||
### Tiefe: ${context.constraints.depthRequirements.depth}
|
|
||||||
### Erforderliche Inhalte:
|
|
||||||
${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
|
||||||
|
|
||||||
${instructions ? `### Anweisungen: ${instructions}` : ''}
|
|
||||||
|
|
||||||
Antworte als JSON mit "sections" Array.`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { documentType, draftContext, instructions, existingDraft } = body
|
|
||||||
|
|
||||||
if (!documentType || !draftContext) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'documentType und draftContext sind erforderlich' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. Constraint Check (Hard Gate)
|
|
||||||
const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext)
|
|
||||||
|
|
||||||
if (!constraintCheck.allowed) {
|
|
||||||
return NextResponse.json({
|
|
||||||
draft: null,
|
|
||||||
constraintCheck,
|
|
||||||
tokensUsed: 0,
|
|
||||||
error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '),
|
|
||||||
}, { status: 403 })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Build document-specific prompt
|
|
||||||
const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions)
|
|
||||||
|
|
||||||
// 3. Build messages
|
|
||||||
const messages = [
|
|
||||||
{ role: 'system', content: DRAFTING_SYSTEM_PROMPT },
|
|
||||||
...(existingDraft ? [{
|
|
||||||
role: 'assistant',
|
|
||||||
content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`,
|
|
||||||
}] : []),
|
|
||||||
{ role: 'user', content: draftPrompt },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 4. Call LLM (non-streaming for structured output)
|
|
||||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: LLM_MODEL,
|
|
||||||
messages,
|
|
||||||
stream: false,
|
|
||||||
options: {
|
|
||||||
temperature: 0.15,
|
|
||||||
num_predict: 16384,
|
|
||||||
},
|
|
||||||
format: 'json',
|
|
||||||
}),
|
|
||||||
signal: AbortSignal.timeout(180000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!ollamaResponse.ok) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` },
|
|
||||||
{ status: 502 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await ollamaResponse.json()
|
|
||||||
const content = result.message?.content || ''
|
|
||||||
|
|
||||||
// 5. Parse JSON response
|
|
||||||
let sections: DraftSection[] = []
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(content)
|
|
||||||
sections = (parsed.sections || []).map((s: Record<string, unknown>, i: number) => ({
|
|
||||||
id: String(s.id || `section-${i}`),
|
|
||||||
title: String(s.title || ''),
|
|
||||||
content: String(s.content || ''),
|
|
||||||
schemaField: s.schemaField ? String(s.schemaField) : undefined,
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
// If not JSON, wrap raw content as single section
|
|
||||||
sections = [{
|
|
||||||
id: 'raw',
|
|
||||||
title: 'Entwurf',
|
|
||||||
content: content,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
const draft: DraftRevision = {
|
|
||||||
id: `draft-${Date.now()}`,
|
|
||||||
content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'),
|
|
||||||
sections,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
instruction: instructions,
|
|
||||||
}
|
|
||||||
|
|
||||||
const response: DraftResponse = {
|
|
||||||
draft,
|
|
||||||
constraintCheck,
|
|
||||||
tokensUsed: result.eval_count || 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(response)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Draft generation error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Draft-Generierung fehlgeschlagen.' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
/**
|
|
||||||
* Drafting Engine - Validate API
|
|
||||||
*
|
|
||||||
* Stufe 1: Deterministische Pruefung gegen DOCUMENT_SCOPE_MATRIX
|
|
||||||
* Stufe 2: LLM Cross-Consistency Check
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { DOCUMENT_SCOPE_MATRIX, DOCUMENT_TYPE_LABELS, getDepthLevelNumeric } from '@/lib/sdk/compliance-scope-types'
|
|
||||||
import type { ScopeDocumentType, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
|
|
||||||
import type { ValidationContext, ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types'
|
|
||||||
import { buildCrossCheckPrompt } from '@/lib/sdk/drafting-engine/prompts/validate-cross-check'
|
|
||||||
|
|
||||||
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
|
|
||||||
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stufe 1: Deterministische Pruefung
|
|
||||||
*/
|
|
||||||
function deterministicCheck(
|
|
||||||
documentType: ScopeDocumentType,
|
|
||||||
validationContext: ValidationContext
|
|
||||||
): ValidationFinding[] {
|
|
||||||
const findings: ValidationFinding[] = []
|
|
||||||
const level = validationContext.scopeLevel
|
|
||||||
const levelNumeric = getDepthLevelNumeric(level)
|
|
||||||
const req = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
|
|
||||||
|
|
||||||
// Check 1: Ist das Dokument auf diesem Level erforderlich?
|
|
||||||
if (req && !req.required && levelNumeric < 3) {
|
|
||||||
findings.push({
|
|
||||||
id: `DET-OPT-${documentType}`,
|
|
||||||
severity: 'suggestion',
|
|
||||||
category: 'scope_violation',
|
|
||||||
title: `${DOCUMENT_TYPE_LABELS[documentType] ?? documentType} ist optional`,
|
|
||||||
description: `Auf Level ${level} ist dieses Dokument nicht verpflichtend.`,
|
|
||||||
documentType,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check 2: VVT vorhanden wenn erforderlich?
|
|
||||||
const vvtReq = DOCUMENT_SCOPE_MATRIX.vvt[level]
|
|
||||||
if (vvtReq.required && validationContext.crossReferences.vvtCategories.length === 0) {
|
|
||||||
findings.push({
|
|
||||||
id: 'DET-VVT-MISSING',
|
|
||||||
severity: 'error',
|
|
||||||
category: 'missing_content',
|
|
||||||
title: 'VVT fehlt',
|
|
||||||
description: `Auf Level ${level} ist ein VVT Pflicht, aber keine Eintraege vorhanden.`,
|
|
||||||
documentType: 'vvt',
|
|
||||||
legalReference: 'Art. 30 DSGVO',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check 3: TOM vorhanden wenn VVT existiert?
|
|
||||||
if (validationContext.crossReferences.vvtCategories.length > 0
|
|
||||||
&& validationContext.crossReferences.tomControls.length === 0) {
|
|
||||||
findings.push({
|
|
||||||
id: 'DET-TOM-MISSING-FOR-VVT',
|
|
||||||
severity: 'warning',
|
|
||||||
category: 'cross_reference',
|
|
||||||
title: 'TOM fehlt bei vorhandenem VVT',
|
|
||||||
description: 'VVT-Eintraege existieren, aber keine TOM-Massnahmen sind definiert.',
|
|
||||||
documentType: 'tom',
|
|
||||||
crossReferenceType: 'vvt',
|
|
||||||
legalReference: 'Art. 32 DSGVO',
|
|
||||||
suggestion: 'TOM-Massnahmen erstellen, die die VVT-Taetigkeiten absichern.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check 4: Loeschfristen fuer VVT-Kategorien
|
|
||||||
if (validationContext.crossReferences.vvtCategories.length > 0
|
|
||||||
&& validationContext.crossReferences.retentionCategories.length === 0) {
|
|
||||||
findings.push({
|
|
||||||
id: 'DET-LF-MISSING-FOR-VVT',
|
|
||||||
severity: 'warning',
|
|
||||||
category: 'cross_reference',
|
|
||||||
title: 'Loeschfristen fehlen',
|
|
||||||
description: 'VVT-Eintraege existieren, aber keine Loeschfristen sind definiert.',
|
|
||||||
documentType: 'lf',
|
|
||||||
crossReferenceType: 'vvt',
|
|
||||||
legalReference: 'Art. 17 DSGVO',
|
|
||||||
suggestion: 'Loeschfristen fuer alle VVT-Datenkategorien definieren.',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return findings
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { documentType, draftContent, validationContext } = body
|
|
||||||
|
|
||||||
if (!documentType || !validationContext) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'documentType und validationContext sind erforderlich' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Stufe 1: Deterministische Pruefung
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
const deterministicFindings = deterministicCheck(documentType, validationContext)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Stufe 2: LLM Cross-Consistency Check
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
let llmFindings: ValidationFinding[] = []
|
|
||||||
|
|
||||||
try {
|
|
||||||
const crossCheckPrompt = buildCrossCheckPrompt({
|
|
||||||
context: validationContext,
|
|
||||||
})
|
|
||||||
|
|
||||||
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: LLM_MODEL,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: 'Du bist ein DSGVO-Compliance-Validator. Antworte NUR im JSON-Format.',
|
|
||||||
},
|
|
||||||
{ role: 'user', content: crossCheckPrompt },
|
|
||||||
],
|
|
||||||
stream: false,
|
|
||||||
options: { temperature: 0.1, num_predict: 8192 },
|
|
||||||
format: 'json',
|
|
||||||
}),
|
|
||||||
signal: AbortSignal.timeout(120000),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (ollamaResponse.ok) {
|
|
||||||
const result = await ollamaResponse.json()
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(result.message?.content || '{}')
|
|
||||||
llmFindings = [
|
|
||||||
...(parsed.errors || []),
|
|
||||||
...(parsed.warnings || []),
|
|
||||||
...(parsed.suggestions || []),
|
|
||||||
].map((f: Record<string, unknown>, i: number) => ({
|
|
||||||
id: String(f.id || `LLM-${i}`),
|
|
||||||
severity: (String(f.severity || 'suggestion')) as 'error' | 'warning' | 'suggestion',
|
|
||||||
category: (String(f.category || 'inconsistency')) as ValidationFinding['category'],
|
|
||||||
title: String(f.title || ''),
|
|
||||||
description: String(f.description || ''),
|
|
||||||
documentType: (String(f.documentType || documentType)) as ScopeDocumentType,
|
|
||||||
crossReferenceType: f.crossReferenceType ? String(f.crossReferenceType) as ScopeDocumentType : undefined,
|
|
||||||
legalReference: f.legalReference ? String(f.legalReference) : undefined,
|
|
||||||
suggestion: f.suggestion ? String(f.suggestion) : undefined,
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
// LLM response not parseable, skip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// LLM unavailable, continue with deterministic results only
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Combine results
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
const allFindings = [...deterministicFindings, ...llmFindings]
|
|
||||||
const errors = allFindings.filter(f => f.severity === 'error')
|
|
||||||
const warnings = allFindings.filter(f => f.severity === 'warning')
|
|
||||||
const suggestions = allFindings.filter(f => f.severity === 'suggestion')
|
|
||||||
|
|
||||||
const result: ValidationResult = {
|
|
||||||
passed: errors.length === 0,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
scopeLevel: validationContext.scopeLevel,
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
suggestions,
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(result)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Validation error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Validierung fehlgeschlagen.' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
/**
|
|
||||||
* Academy API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/academy/* requests to ai-compliance-sdk backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/academy`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
const authHeader = request.headers.get('authorization')
|
|
||||||
if (authHeader) {
|
|
||||||
headers['Authorization'] = authHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenantHeader = request.headers.get('x-tenant-id')
|
|
||||||
if (tenantHeader) {
|
|
||||||
headers['X-Tenant-Id'] = tenantHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
||||||
const contentType = request.headers.get('content-type')
|
|
||||||
if (contentType?.includes('application/json')) {
|
|
||||||
try {
|
|
||||||
const text = await request.text()
|
|
||||||
if (text && text.trim()) {
|
|
||||||
fetchOptions.body = text
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Empty or invalid body - continue without
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
// Handle non-JSON responses (e.g., PDF certificates)
|
|
||||||
const responseContentType = response.headers.get('content-type')
|
|
||||||
if (responseContentType?.includes('application/pdf') ||
|
|
||||||
responseContentType?.includes('application/octet-stream')) {
|
|
||||||
const blob = await response.blob()
|
|
||||||
return new NextResponse(blob, {
|
|
||||||
status: response.status,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': responseContentType,
|
|
||||||
'Content-Disposition': response.headers.get('content-disposition') || '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Academy API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PATCH(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PATCH')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SDK Checkpoints API
|
|
||||||
*
|
|
||||||
* GET /api/sdk/v1/checkpoints - Get all checkpoint statuses
|
|
||||||
* POST /api/sdk/v1/checkpoints - Validate a checkpoint
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Checkpoint definitions
|
|
||||||
const CHECKPOINTS = {
|
|
||||||
'CP-PROF': {
|
|
||||||
id: 'CP-PROF',
|
|
||||||
step: 'company-profile',
|
|
||||||
name: 'Unternehmensprofil Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'NONE',
|
|
||||||
},
|
|
||||||
'CP-UC': {
|
|
||||||
id: 'CP-UC',
|
|
||||||
step: 'use-case-assessment',
|
|
||||||
name: 'Anwendungsfall Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'NONE',
|
|
||||||
},
|
|
||||||
'CP-SCAN': {
|
|
||||||
id: 'CP-SCAN',
|
|
||||||
step: 'screening',
|
|
||||||
name: 'Screening Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'NONE',
|
|
||||||
},
|
|
||||||
'CP-MOD': {
|
|
||||||
id: 'CP-MOD',
|
|
||||||
step: 'modules',
|
|
||||||
name: 'Modules Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'NONE',
|
|
||||||
},
|
|
||||||
'CP-REQ': {
|
|
||||||
id: 'CP-REQ',
|
|
||||||
step: 'requirements',
|
|
||||||
name: 'Requirements Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'NONE',
|
|
||||||
},
|
|
||||||
'CP-CTRL': {
|
|
||||||
id: 'CP-CTRL',
|
|
||||||
step: 'controls',
|
|
||||||
name: 'Controls Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'DSB',
|
|
||||||
},
|
|
||||||
'CP-EVI': {
|
|
||||||
id: 'CP-EVI',
|
|
||||||
step: 'evidence',
|
|
||||||
name: 'Evidence Checkpoint',
|
|
||||||
type: 'RECOMMENDED',
|
|
||||||
blocksProgress: false,
|
|
||||||
requiresReview: 'NONE',
|
|
||||||
},
|
|
||||||
'CP-CHK': {
|
|
||||||
id: 'CP-CHK',
|
|
||||||
step: 'audit-checklist',
|
|
||||||
name: 'Checklist Checkpoint',
|
|
||||||
type: 'RECOMMENDED',
|
|
||||||
blocksProgress: false,
|
|
||||||
requiresReview: 'NONE',
|
|
||||||
},
|
|
||||||
'CP-RISK': {
|
|
||||||
id: 'CP-RISK',
|
|
||||||
step: 'risks',
|
|
||||||
name: 'Risk Matrix Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'DSB',
|
|
||||||
},
|
|
||||||
'CP-AI': {
|
|
||||||
id: 'CP-AI',
|
|
||||||
step: 'ai-act',
|
|
||||||
name: 'AI Act Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'LEGAL',
|
|
||||||
},
|
|
||||||
'CP-DSFA': {
|
|
||||||
id: 'CP-DSFA',
|
|
||||||
step: 'dsfa',
|
|
||||||
name: 'DSFA Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'DSB',
|
|
||||||
},
|
|
||||||
'CP-TOM': {
|
|
||||||
id: 'CP-TOM',
|
|
||||||
step: 'tom',
|
|
||||||
name: 'TOMs Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'NONE',
|
|
||||||
},
|
|
||||||
'CP-VVT': {
|
|
||||||
id: 'CP-VVT',
|
|
||||||
step: 'vvt',
|
|
||||||
name: 'VVT Checkpoint',
|
|
||||||
type: 'REQUIRED',
|
|
||||||
blocksProgress: true,
|
|
||||||
requiresReview: 'DSB',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
checkpoints: CHECKPOINTS,
|
|
||||||
count: Object.keys(CHECKPOINTS).length,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get checkpoints:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to get checkpoints' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { checkpointId, state, context } = body
|
|
||||||
|
|
||||||
if (!checkpointId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'checkpointId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkpoint = CHECKPOINTS[checkpointId as keyof typeof CHECKPOINTS]
|
|
||||||
|
|
||||||
if (!checkpoint) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Checkpoint not found', checkpointId },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform validation based on checkpoint
|
|
||||||
const errors: Array<{ ruleId: string; field: string; message: string; severity: string }> = []
|
|
||||||
const warnings: Array<{ ruleId: string; field: string; message: string; severity: string }> = []
|
|
||||||
|
|
||||||
// Basic validation rules
|
|
||||||
switch (checkpointId) {
|
|
||||||
case 'CP-UC':
|
|
||||||
if (!state?.useCases || state.useCases.length === 0) {
|
|
||||||
errors.push({
|
|
||||||
ruleId: 'uc-min-count',
|
|
||||||
field: 'useCases',
|
|
||||||
message: 'Mindestens ein Use Case muss erstellt werden',
|
|
||||||
severity: 'ERROR',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'CP-SCAN':
|
|
||||||
if (!state?.screening || state.screening.status !== 'COMPLETED') {
|
|
||||||
errors.push({
|
|
||||||
ruleId: 'scan-complete',
|
|
||||||
field: 'screening',
|
|
||||||
message: 'Security Scan muss abgeschlossen sein',
|
|
||||||
severity: 'ERROR',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'CP-MOD':
|
|
||||||
if (!state?.modules || state.modules.length === 0) {
|
|
||||||
errors.push({
|
|
||||||
ruleId: 'mod-min-count',
|
|
||||||
field: 'modules',
|
|
||||||
message: 'Mindestens ein Modul muss zugewiesen werden',
|
|
||||||
severity: 'ERROR',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'CP-RISK':
|
|
||||||
if (state?.risks) {
|
|
||||||
const criticalRisks = state.risks.filter(
|
|
||||||
(r: { severity: string; mitigation: unknown[] }) =>
|
|
||||||
(r.severity === 'CRITICAL' || r.severity === 'HIGH') && r.mitigation.length === 0
|
|
||||||
)
|
|
||||||
if (criticalRisks.length > 0) {
|
|
||||||
errors.push({
|
|
||||||
ruleId: 'critical-risks-mitigated',
|
|
||||||
field: 'risks',
|
|
||||||
message: `${criticalRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`,
|
|
||||||
severity: 'ERROR',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
const passed = errors.length === 0
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
checkpointId,
|
|
||||||
passed,
|
|
||||||
validatedAt: new Date().toISOString(),
|
|
||||||
validatedBy: context?.userId || 'SYSTEM',
|
|
||||||
errors,
|
|
||||||
warnings,
|
|
||||||
checkpoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
...result,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to validate checkpoint:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to validate checkpoint' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
/**
|
|
||||||
* Document Crawler API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/crawler/* requests to document-crawler service (port 8098)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const CRAWLER_BACKEND_URL = process.env.CRAWLER_API_URL || 'http://document-crawler:8098'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${CRAWLER_BACKEND_URL}/api/v1/crawler`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward all relevant headers
|
|
||||||
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
|
|
||||||
for (const name of headerNames) {
|
|
||||||
const value = request.headers.get(name)
|
|
||||||
if (value) {
|
|
||||||
headers[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward body for non-GET requests
|
|
||||||
if (method !== 'GET' && method !== 'DELETE') {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
fetchOptions.body = JSON.stringify(body)
|
|
||||||
} catch {
|
|
||||||
// No body or non-JSON body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle 204 No Content
|
|
||||||
if (response.status === 204) {
|
|
||||||
return new NextResponse(null, { status: 204 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Document Crawler API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum Document Crawler Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo Data Clear API Endpoint
|
|
||||||
*
|
|
||||||
* Clears demo data from the storage (same mechanism as real customer data).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
// Shared store reference (same as seed endpoint)
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var demoStateStore: Map<string, { state: unknown; version: number; updatedAt: Date }> | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!global.demoStateStore) {
|
|
||||||
global.demoStateStore = new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateStore = global.demoStateStore
|
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { tenantId = 'demo-tenant' } = body
|
|
||||||
|
|
||||||
const existed = stateStore.has(tenantId)
|
|
||||||
stateStore.delete(tenantId)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: existed
|
|
||||||
? `Demo data cleared for tenant ${tenantId}`
|
|
||||||
: `No data found for tenant ${tenantId}`,
|
|
||||||
tenantId,
|
|
||||||
existed,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to clear demo data:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
// Also support POST for clearing (for clients that don't support DELETE)
|
|
||||||
return DELETE(request)
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo Data Seed API Endpoint
|
|
||||||
*
|
|
||||||
* This endpoint seeds demo data via the same storage mechanism as real customer data.
|
|
||||||
* Demo data is NOT hardcoded - it goes through the normal API/database path.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { generateDemoState } from '@/lib/sdk/demo-data'
|
|
||||||
|
|
||||||
// In-memory store (same as state endpoint - will be replaced with PostgreSQL)
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var demoStateStore: Map<string, { state: unknown; version: number; updatedAt: Date }> | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!global.demoStateStore) {
|
|
||||||
global.demoStateStore = new Map()
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateStore = global.demoStateStore
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { tenantId = 'demo-tenant', userId = 'demo-user' } = body
|
|
||||||
|
|
||||||
// Generate demo state using the seed data templates
|
|
||||||
const demoState = generateDemoState(tenantId, userId)
|
|
||||||
|
|
||||||
// Store via the same mechanism as real data
|
|
||||||
const storedState = {
|
|
||||||
state: demoState,
|
|
||||||
version: 1,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}
|
|
||||||
|
|
||||||
stateStore.set(tenantId, storedState)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
message: `Demo data seeded for tenant ${tenantId}`,
|
|
||||||
tenantId,
|
|
||||||
version: 1,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to seed demo data:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: error instanceof Error ? error.message : 'Unknown error',
|
|
||||||
},
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const tenantId = searchParams.get('tenantId') || 'demo-tenant'
|
|
||||||
|
|
||||||
const stored = stateStore.get(tenantId)
|
|
||||||
|
|
||||||
if (!stored) {
|
|
||||||
return NextResponse.json({
|
|
||||||
hasData: false,
|
|
||||||
tenantId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
hasData: true,
|
|
||||||
tenantId,
|
|
||||||
version: stored.version,
|
|
||||||
updatedAt: stored.updatedAt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
// Types
|
|
||||||
interface ExtractedSection {
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
type?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExtractedContent {
|
|
||||||
title?: string
|
|
||||||
version?: string
|
|
||||||
lastModified?: string
|
|
||||||
sections?: ExtractedSection[]
|
|
||||||
metadata?: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UploadResponse {
|
|
||||||
success: boolean
|
|
||||||
documentId: string
|
|
||||||
filename: string
|
|
||||||
documentType: string
|
|
||||||
extractedVersion?: string
|
|
||||||
extractedContent?: ExtractedContent
|
|
||||||
suggestedNextVersion?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Detect version from filename
|
|
||||||
function detectVersionFromFilename(filename: string): string | undefined {
|
|
||||||
const patterns = [
|
|
||||||
/[vV](\d+(?:\.\d+)*)/,
|
|
||||||
/version[_-]?(\d+(?:\.\d+)*)/i,
|
|
||||||
/[_-]v?(\d+\.\d+(?:\.\d+)?)[_-]/,
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
|
||||||
const match = filename.match(pattern)
|
|
||||||
if (match) {
|
|
||||||
return match[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Suggest next version
|
|
||||||
function suggestNextVersion(currentVersion?: string): string {
|
|
||||||
if (!currentVersion) return '1.0'
|
|
||||||
|
|
||||||
const parts = currentVersion.split('.').map(Number)
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
parts[parts.length - 1] += 1
|
|
||||||
} else {
|
|
||||||
parts.push(1)
|
|
||||||
}
|
|
||||||
return parts.join('.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: Extract content from PDF/DOCX (simplified - would need proper libraries in production)
|
|
||||||
async function extractDocumentContent(
|
|
||||||
_file: File,
|
|
||||||
documentType: string
|
|
||||||
): Promise<ExtractedContent> {
|
|
||||||
// In production, this would use libraries like:
|
|
||||||
// - pdf-parse for PDFs
|
|
||||||
// - mammoth for DOCX
|
|
||||||
// For now, return mock extracted content based on document type
|
|
||||||
|
|
||||||
const mockContentByType: Record<string, ExtractedContent> = {
|
|
||||||
tom: {
|
|
||||||
title: 'Technische und Organisatorische Maßnahmen',
|
|
||||||
sections: [
|
|
||||||
{ title: 'Vertraulichkeit', content: 'Zugangskontrollen, Zugriffsbeschränkungen...', type: 'category' },
|
|
||||||
{ title: 'Integrität', content: 'Eingabekontrollen, Änderungsprotokolle...', type: 'category' },
|
|
||||||
{ title: 'Verfügbarkeit', content: 'Backup-Strategien, Disaster Recovery...', type: 'category' },
|
|
||||||
{ title: 'Belastbarkeit', content: 'Redundanz, Lasttests...', type: 'category' },
|
|
||||||
],
|
|
||||||
metadata: {
|
|
||||||
lastReview: new Date().toISOString(),
|
|
||||||
responsible: 'Datenschutzbeauftragter',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dsfa: {
|
|
||||||
title: 'Datenschutz-Folgenabschätzung',
|
|
||||||
sections: [
|
|
||||||
{ title: 'Beschreibung der Verarbeitung', content: 'Systematische Beschreibung...', type: 'section' },
|
|
||||||
{ title: 'Erforderlichkeit und Verhältnismäßigkeit', content: 'Bewertung...', type: 'section' },
|
|
||||||
{ title: 'Risiken für Betroffene', content: 'Risikoanalyse...', type: 'section' },
|
|
||||||
{ title: 'Abhilfemaßnahmen', content: 'Geplante Maßnahmen...', type: 'section' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
vvt: {
|
|
||||||
title: 'Verzeichnis von Verarbeitungstätigkeiten',
|
|
||||||
sections: [
|
|
||||||
{ title: 'Verantwortlicher', content: 'Name und Kontaktdaten...', type: 'field' },
|
|
||||||
{ title: 'Verarbeitungszwecke', content: 'Liste der Zwecke...', type: 'list' },
|
|
||||||
{ title: 'Datenkategorien', content: 'Personenbezogene Daten...', type: 'list' },
|
|
||||||
{ title: 'Empfängerkategorien', content: 'Interne und externe Empfänger...', type: 'list' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
loeschfristen: {
|
|
||||||
title: 'Löschkonzept und Aufbewahrungsfristen',
|
|
||||||
sections: [
|
|
||||||
{ title: 'Personalakten', content: '10 Jahre nach Ausscheiden', type: 'retention' },
|
|
||||||
{ title: 'Kundendaten', content: '3 Jahre nach letzter Aktivität', type: 'retention' },
|
|
||||||
{ title: 'Buchhaltungsbelege', content: '10 Jahre (HGB)', type: 'retention' },
|
|
||||||
{ title: 'Bewerbungsunterlagen', content: '6 Monate nach Absage', type: 'retention' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
consent: {
|
|
||||||
title: 'Einwilligungserklärungen',
|
|
||||||
sections: [
|
|
||||||
{ title: 'Newsletter-Einwilligung', content: 'Vorlage für Newsletter...', type: 'template' },
|
|
||||||
{ title: 'Marketing-Einwilligung', content: 'Vorlage für Marketing...', type: 'template' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
policy: {
|
|
||||||
title: 'Datenschutzrichtlinie',
|
|
||||||
sections: [
|
|
||||||
{ title: 'Geltungsbereich', content: 'Diese Richtlinie gilt für...', type: 'section' },
|
|
||||||
{ title: 'Verantwortlichkeiten', content: 'Rollen und Pflichten...', type: 'section' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return mockContentByType[documentType] || {
|
|
||||||
title: 'Unbekanntes Dokument',
|
|
||||||
sections: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const formData = await request.formData()
|
|
||||||
const file = formData.get('file') as File | null
|
|
||||||
const documentType = formData.get('documentType') as string || 'custom'
|
|
||||||
const sessionId = formData.get('sessionId') as string || 'default'
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Keine Datei hochgeladen' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file type
|
|
||||||
const allowedTypes = [
|
|
||||||
'application/pdf',
|
|
||||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
||||||
'application/msword',
|
|
||||||
]
|
|
||||||
|
|
||||||
if (!allowedTypes.includes(file.type)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Ungültiger Dateityp. Erlaubt: PDF, DOCX, DOC' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate document ID
|
|
||||||
const documentId = `doc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
||||||
|
|
||||||
// Extract version from filename
|
|
||||||
const extractedVersion = detectVersionFromFilename(file.name)
|
|
||||||
|
|
||||||
// Extract content (in production, this would parse the actual file)
|
|
||||||
const extractedContent = await extractDocumentContent(file, documentType)
|
|
||||||
|
|
||||||
// Add version to extracted content if found
|
|
||||||
if (extractedVersion) {
|
|
||||||
extractedContent.version = extractedVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store file (in production, save to MinIO/S3)
|
|
||||||
// For now, we just process and return metadata
|
|
||||||
console.log(`[SDK Documents] Uploaded: ${file.name} (${file.size} bytes) for session ${sessionId}`)
|
|
||||||
|
|
||||||
const response: UploadResponse = {
|
|
||||||
success: true,
|
|
||||||
documentId,
|
|
||||||
filename: file.name,
|
|
||||||
documentType,
|
|
||||||
extractedVersion,
|
|
||||||
extractedContent,
|
|
||||||
suggestedNextVersion: suggestNextVersion(extractedVersion),
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(response)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[SDK Documents] Upload error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Upload fehlgeschlagen' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const sessionId = searchParams.get('sessionId')
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Session ID erforderlich' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// In production, fetch uploaded documents from storage
|
|
||||||
// For now, return empty list
|
|
||||||
return NextResponse.json({
|
|
||||||
uploads: [],
|
|
||||||
sessionId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/**
|
|
||||||
* DSB Portal API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/dsb/* requests to ai-compliance-sdk backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/dsb`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
|
|
||||||
for (const name of headerNames) {
|
|
||||||
const value = request.headers.get(name)
|
|
||||||
if (value) {
|
|
||||||
headers[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
||||||
try {
|
|
||||||
const body = await request.text()
|
|
||||||
if (body) {
|
|
||||||
fetchOptions.body = body
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// No body to forward
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('DSB API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
/**
|
|
||||||
* DSGVO API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/dsgvo/* requests to ai-compliance-sdk backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[],
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments.join('/')
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const url = `${SDK_BACKEND_URL}/sdk/v1/dsgvo/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward auth headers if present
|
|
||||||
const authHeader = request.headers.get('authorization')
|
|
||||||
if (authHeader) {
|
|
||||||
headers['Authorization'] = authHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add body for POST/PUT/PATCH methods
|
|
||||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
||||||
const contentType = request.headers.get('content-type')
|
|
||||||
if (contentType?.includes('application/json')) {
|
|
||||||
try {
|
|
||||||
const text = await request.text()
|
|
||||||
if (text && text.trim()) {
|
|
||||||
fetchOptions.body = text
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Empty or invalid body - continue without
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
// Handle non-JSON responses (e.g., PDF export)
|
|
||||||
const responseContentType = response.headers.get('content-type')
|
|
||||||
if (responseContentType?.includes('application/pdf') ||
|
|
||||||
responseContentType?.includes('application/octet-stream')) {
|
|
||||||
const blob = await response.blob()
|
|
||||||
return new NextResponse(blob, {
|
|
||||||
status: response.status,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': responseContentType,
|
|
||||||
'Content-Disposition': response.headers.get('content-disposition') || '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('DSGVO API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PATCH(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PATCH')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
/**
|
|
||||||
* API Route: Datenpunktkatalog
|
|
||||||
*
|
|
||||||
* GET - Katalog abrufen (inkl. kundenspezifischer Datenpunkte)
|
|
||||||
* POST - Katalog speichern/aktualisieren
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import {
|
|
||||||
DataPointCatalog,
|
|
||||||
CompanyInfo,
|
|
||||||
CookieBannerConfig,
|
|
||||||
DataPoint,
|
|
||||||
} from '@/lib/sdk/einwilligungen/types'
|
|
||||||
import { createDefaultCatalog, PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
|
|
||||||
|
|
||||||
// In-Memory Storage (in Produktion: Datenbank)
|
|
||||||
const catalogStorage = new Map<string, {
|
|
||||||
catalog: DataPointCatalog
|
|
||||||
companyInfo: CompanyInfo | null
|
|
||||||
cookieBannerConfig: CookieBannerConfig | null
|
|
||||||
}>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/einwilligungen/catalog
|
|
||||||
*
|
|
||||||
* Laedt den Datenpunktkatalog fuer einen Tenant
|
|
||||||
*/
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hole gespeicherte Daten oder erstelle Default
|
|
||||||
let stored = catalogStorage.get(tenantId)
|
|
||||||
|
|
||||||
if (!stored) {
|
|
||||||
// Erstelle Default-Katalog
|
|
||||||
const defaultCatalog = createDefaultCatalog(tenantId)
|
|
||||||
stored = {
|
|
||||||
catalog: defaultCatalog,
|
|
||||||
companyInfo: null,
|
|
||||||
cookieBannerConfig: null,
|
|
||||||
}
|
|
||||||
catalogStorage.set(tenantId, stored)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
catalog: stored.catalog,
|
|
||||||
companyInfo: stored.companyInfo,
|
|
||||||
cookieBannerConfig: stored.cookieBannerConfig,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading catalog:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to load catalog' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sdk/v1/einwilligungen/catalog
|
|
||||||
*
|
|
||||||
* Speichert den Datenpunktkatalog fuer einen Tenant
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { catalog, companyInfo, cookieBannerConfig } = body
|
|
||||||
|
|
||||||
if (!catalog) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Catalog data required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validiere den Katalog
|
|
||||||
if (!catalog.tenantId || catalog.tenantId !== tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID mismatch' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aktualisiere den Katalog
|
|
||||||
const updatedCatalog: DataPointCatalog = {
|
|
||||||
...catalog,
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Speichere
|
|
||||||
catalogStorage.set(tenantId, {
|
|
||||||
catalog: updatedCatalog,
|
|
||||||
companyInfo: companyInfo || null,
|
|
||||||
cookieBannerConfig: cookieBannerConfig || null,
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
catalog: updatedCatalog,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving catalog:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to save catalog' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sdk/v1/einwilligungen/catalog/customize
|
|
||||||
*
|
|
||||||
* Fuegt einen kundenspezifischen Datenpunkt hinzu
|
|
||||||
*/
|
|
||||||
export async function PUT(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { action, dataPoint, dataPointId } = body
|
|
||||||
|
|
||||||
let stored = catalogStorage.get(tenantId)
|
|
||||||
|
|
||||||
if (!stored) {
|
|
||||||
const defaultCatalog = createDefaultCatalog(tenantId)
|
|
||||||
stored = {
|
|
||||||
catalog: defaultCatalog,
|
|
||||||
companyInfo: null,
|
|
||||||
cookieBannerConfig: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'add': {
|
|
||||||
if (!dataPoint) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Data point required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generiere eindeutige ID
|
|
||||||
const newDataPoint: DataPoint = {
|
|
||||||
...dataPoint,
|
|
||||||
id: `custom-${tenantId}-${Date.now()}`,
|
|
||||||
isCustom: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
stored.catalog.customDataPoints.push(newDataPoint)
|
|
||||||
stored.catalog.updatedAt = new Date()
|
|
||||||
catalogStorage.set(tenantId, stored)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
dataPoint: newDataPoint,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'update': {
|
|
||||||
if (!dataPointId || !dataPoint) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Data point ID and data required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pruefe ob es ein kundenspezifischer Datenpunkt ist
|
|
||||||
const customIndex = stored.catalog.customDataPoints.findIndex(
|
|
||||||
(dp) => dp.id === dataPointId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (customIndex !== -1) {
|
|
||||||
stored.catalog.customDataPoints[customIndex] = {
|
|
||||||
...stored.catalog.customDataPoints[customIndex],
|
|
||||||
...dataPoint,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Vordefinierter Datenpunkt - nur isActive aendern
|
|
||||||
const predefinedIndex = stored.catalog.dataPoints.findIndex(
|
|
||||||
(dp) => dp.id === dataPointId
|
|
||||||
)
|
|
||||||
if (predefinedIndex !== -1 && 'isActive' in dataPoint) {
|
|
||||||
stored.catalog.dataPoints[predefinedIndex] = {
|
|
||||||
...stored.catalog.dataPoints[predefinedIndex],
|
|
||||||
isActive: dataPoint.isActive,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stored.catalog.updatedAt = new Date()
|
|
||||||
catalogStorage.set(tenantId, stored)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'delete': {
|
|
||||||
if (!dataPointId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Data point ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
stored.catalog.customDataPoints = stored.catalog.customDataPoints.filter(
|
|
||||||
(dp) => dp.id !== dataPointId
|
|
||||||
)
|
|
||||||
stored.catalog.updatedAt = new Date()
|
|
||||||
catalogStorage.set(tenantId, stored)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid action' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error customizing catalog:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to customize catalog' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,369 +0,0 @@
|
|||||||
/**
|
|
||||||
* API Route: Consent Management
|
|
||||||
*
|
|
||||||
* POST - Consent erfassen
|
|
||||||
* GET - Consent-Status abrufen
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import {
|
|
||||||
ConsentEntry,
|
|
||||||
ConsentStatistics,
|
|
||||||
DataPointCategory,
|
|
||||||
LegalBasis,
|
|
||||||
} from '@/lib/sdk/einwilligungen/types'
|
|
||||||
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
|
|
||||||
|
|
||||||
// In-Memory Storage fuer Consents
|
|
||||||
const consentStorage = new Map<string, ConsentEntry[]>() // tenantId -> consents
|
|
||||||
|
|
||||||
// Hilfsfunktion: Generiere eindeutige ID
|
|
||||||
function generateId(): string {
|
|
||||||
return `consent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sdk/v1/einwilligungen/consent
|
|
||||||
*
|
|
||||||
* Erfasst eine neue Einwilligung
|
|
||||||
*
|
|
||||||
* Body:
|
|
||||||
* - userId: string - Benutzer-ID
|
|
||||||
* - dataPointId: string - ID des Datenpunkts
|
|
||||||
* - granted: boolean - Einwilligung erteilt?
|
|
||||||
* - consentVersion?: string - Version der Einwilligung
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { userId, dataPointId, granted, consentVersion = '1.0.0' } = body
|
|
||||||
|
|
||||||
if (!userId || !dataPointId || typeof granted !== 'boolean') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'userId, dataPointId, and granted required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hole IP und User-Agent
|
|
||||||
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null
|
|
||||||
const userAgent = request.headers.get('user-agent') || null
|
|
||||||
|
|
||||||
// Erstelle Consent-Eintrag
|
|
||||||
const consent: ConsentEntry = {
|
|
||||||
id: generateId(),
|
|
||||||
userId,
|
|
||||||
dataPointId,
|
|
||||||
granted,
|
|
||||||
grantedAt: new Date(),
|
|
||||||
revokedAt: undefined,
|
|
||||||
ipAddress: ipAddress || undefined,
|
|
||||||
userAgent: userAgent || undefined,
|
|
||||||
consentVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hole bestehende Consents
|
|
||||||
const tenantConsents = consentStorage.get(tenantId) || []
|
|
||||||
|
|
||||||
// Pruefe auf bestehende Einwilligung fuer diesen Datenpunkt
|
|
||||||
const existingIndex = tenantConsents.findIndex(
|
|
||||||
(c) => c.userId === userId && c.dataPointId === dataPointId && !c.revokedAt
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
if (!granted) {
|
|
||||||
// Widerruf: Setze revokedAt
|
|
||||||
tenantConsents[existingIndex].revokedAt = new Date()
|
|
||||||
}
|
|
||||||
// Bei granted=true: Keine Aenderung noetig, Consent existiert bereits
|
|
||||||
} else if (granted) {
|
|
||||||
// Neuer Consent
|
|
||||||
tenantConsents.push(consent)
|
|
||||||
}
|
|
||||||
|
|
||||||
consentStorage.set(tenantId, tenantConsents)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
consent: {
|
|
||||||
id: consent.id,
|
|
||||||
dataPointId: consent.dataPointId,
|
|
||||||
granted: consent.granted,
|
|
||||||
grantedAt: consent.grantedAt,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error recording consent:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to record consent' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/einwilligungen/consent
|
|
||||||
*
|
|
||||||
* Ruft Consent-Status und Statistiken ab
|
|
||||||
*
|
|
||||||
* Query Parameters:
|
|
||||||
* - userId?: string - Fuer spezifischen Benutzer
|
|
||||||
* - stats?: boolean - Statistiken inkludieren
|
|
||||||
*/
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const userId = searchParams.get('userId')
|
|
||||||
const includeStats = searchParams.get('stats') === 'true'
|
|
||||||
|
|
||||||
const tenantConsents = consentStorage.get(tenantId) || []
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
// Spezifischer Benutzer
|
|
||||||
const userConsents = tenantConsents.filter((c) => c.userId === userId)
|
|
||||||
|
|
||||||
// Gruppiere nach Datenpunkt
|
|
||||||
const consentMap: Record<string, { granted: boolean; grantedAt: Date; revokedAt?: Date }> = {}
|
|
||||||
for (const consent of userConsents) {
|
|
||||||
consentMap[consent.dataPointId] = {
|
|
||||||
granted: consent.granted && !consent.revokedAt,
|
|
||||||
grantedAt: consent.grantedAt,
|
|
||||||
revokedAt: consent.revokedAt,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
userId,
|
|
||||||
consents: consentMap,
|
|
||||||
totalConsents: Object.keys(consentMap).length,
|
|
||||||
activeConsents: Object.values(consentMap).filter((c) => c.granted).length,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Statistiken fuer alle Consents
|
|
||||||
if (includeStats) {
|
|
||||||
const stats = calculateStatistics(tenantConsents)
|
|
||||||
return NextResponse.json({
|
|
||||||
statistics: stats,
|
|
||||||
recentConsents: tenantConsents
|
|
||||||
.sort((a, b) => new Date(b.grantedAt).getTime() - new Date(a.grantedAt).getTime())
|
|
||||||
.slice(0, 10)
|
|
||||||
.map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
userId: c.userId.substring(0, 8) + '...', // Anonymisiert
|
|
||||||
dataPointId: c.dataPointId,
|
|
||||||
granted: c.granted,
|
|
||||||
grantedAt: c.grantedAt,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard: Alle Consents (anonymisiert)
|
|
||||||
return NextResponse.json({
|
|
||||||
totalConsents: tenantConsents.length,
|
|
||||||
activeConsents: tenantConsents.filter((c) => c.granted && !c.revokedAt).length,
|
|
||||||
revokedConsents: tenantConsents.filter((c) => c.revokedAt).length,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching consents:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch consents' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /api/sdk/v1/einwilligungen/consent
|
|
||||||
*
|
|
||||||
* Batch-Update von Consents (z.B. Cookie-Banner)
|
|
||||||
*/
|
|
||||||
export async function PUT(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { userId, consents, consentVersion = '1.0.0' } = body
|
|
||||||
|
|
||||||
if (!userId || !consents || typeof consents !== 'object') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'userId and consents object required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null
|
|
||||||
const userAgent = request.headers.get('user-agent') || null
|
|
||||||
|
|
||||||
const tenantConsents = consentStorage.get(tenantId) || []
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
// Verarbeite jeden Consent
|
|
||||||
for (const [dataPointId, granted] of Object.entries(consents)) {
|
|
||||||
if (typeof granted !== 'boolean') continue
|
|
||||||
|
|
||||||
const existingIndex = tenantConsents.findIndex(
|
|
||||||
(c) => c.userId === userId && c.dataPointId === dataPointId && !c.revokedAt
|
|
||||||
)
|
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
const existing = tenantConsents[existingIndex]
|
|
||||||
if (existing.granted !== granted) {
|
|
||||||
if (!granted) {
|
|
||||||
// Widerruf
|
|
||||||
tenantConsents[existingIndex].revokedAt = now
|
|
||||||
} else {
|
|
||||||
// Neuer Consent nach Widerruf
|
|
||||||
tenantConsents.push({
|
|
||||||
id: generateId(),
|
|
||||||
userId,
|
|
||||||
dataPointId,
|
|
||||||
granted: true,
|
|
||||||
grantedAt: now,
|
|
||||||
ipAddress: ipAddress || undefined,
|
|
||||||
userAgent: userAgent || undefined,
|
|
||||||
consentVersion,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (granted) {
|
|
||||||
// Neuer Consent
|
|
||||||
tenantConsents.push({
|
|
||||||
id: generateId(),
|
|
||||||
userId,
|
|
||||||
dataPointId,
|
|
||||||
granted: true,
|
|
||||||
grantedAt: now,
|
|
||||||
ipAddress: ipAddress || undefined,
|
|
||||||
userAgent: userAgent || undefined,
|
|
||||||
consentVersion,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
consentStorage.set(tenantId, tenantConsents)
|
|
||||||
|
|
||||||
// Zaehle aktive Consents fuer diesen User
|
|
||||||
const activeConsents = tenantConsents.filter(
|
|
||||||
(c) => c.userId === userId && c.granted && !c.revokedAt
|
|
||||||
).length
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
userId,
|
|
||||||
activeConsents,
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating consents:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to update consents' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Berechnet Consent-Statistiken
|
|
||||||
*/
|
|
||||||
function calculateStatistics(consents: ConsentEntry[]): ConsentStatistics {
|
|
||||||
const activeConsents = consents.filter((c) => c.granted && !c.revokedAt)
|
|
||||||
const revokedConsents = consents.filter((c) => c.revokedAt)
|
|
||||||
|
|
||||||
// Gruppiere nach Kategorie (18 Kategorien A-R)
|
|
||||||
const byCategory: Record<DataPointCategory, { total: number; active: number; revoked: number }> = {
|
|
||||||
MASTER_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
CONTACT_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
AUTHENTICATION: { total: 0, active: 0, revoked: 0 },
|
|
||||||
CONSENT: { total: 0, active: 0, revoked: 0 },
|
|
||||||
COMMUNICATION: { total: 0, active: 0, revoked: 0 },
|
|
||||||
PAYMENT: { total: 0, active: 0, revoked: 0 },
|
|
||||||
USAGE_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
LOCATION: { total: 0, active: 0, revoked: 0 },
|
|
||||||
DEVICE_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
MARKETING: { total: 0, active: 0, revoked: 0 },
|
|
||||||
ANALYTICS: { total: 0, active: 0, revoked: 0 },
|
|
||||||
SOCIAL_MEDIA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
HEALTH_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
EMPLOYEE_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
CONTRACT_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
LOG_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
AI_DATA: { total: 0, active: 0, revoked: 0 },
|
|
||||||
SECURITY: { total: 0, active: 0, revoked: 0 },
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const consent of consents) {
|
|
||||||
const dataPoint = PREDEFINED_DATA_POINTS.find((dp) => dp.id === consent.dataPointId)
|
|
||||||
if (dataPoint) {
|
|
||||||
byCategory[dataPoint.category].total++
|
|
||||||
if (consent.granted && !consent.revokedAt) {
|
|
||||||
byCategory[dataPoint.category].active++
|
|
||||||
}
|
|
||||||
if (consent.revokedAt) {
|
|
||||||
byCategory[dataPoint.category].revoked++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gruppiere nach Rechtsgrundlage (7 Rechtsgrundlagen)
|
|
||||||
const byLegalBasis: Record<LegalBasis, { total: number; active: number }> = {
|
|
||||||
CONTRACT: { total: 0, active: 0 },
|
|
||||||
CONSENT: { total: 0, active: 0 },
|
|
||||||
EXPLICIT_CONSENT: { total: 0, active: 0 },
|
|
||||||
LEGITIMATE_INTEREST: { total: 0, active: 0 },
|
|
||||||
LEGAL_OBLIGATION: { total: 0, active: 0 },
|
|
||||||
VITAL_INTERESTS: { total: 0, active: 0 },
|
|
||||||
PUBLIC_INTEREST: { total: 0, active: 0 },
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const consent of consents) {
|
|
||||||
const dataPoint = PREDEFINED_DATA_POINTS.find((dp) => dp.id === consent.dataPointId)
|
|
||||||
if (dataPoint) {
|
|
||||||
byLegalBasis[dataPoint.legalBasis].total++
|
|
||||||
if (consent.granted && !consent.revokedAt) {
|
|
||||||
byLegalBasis[dataPoint.legalBasis].active++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Berechne Conversion Rate (Unique Users mit mindestens einem Consent)
|
|
||||||
const uniqueUsers = new Set(consents.map((c) => c.userId))
|
|
||||||
const usersWithActiveConsent = new Set(activeConsents.map((c) => c.userId))
|
|
||||||
const conversionRate = uniqueUsers.size > 0
|
|
||||||
? (usersWithActiveConsent.size / uniqueUsers.size) * 100
|
|
||||||
: 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalConsents: consents.length,
|
|
||||||
activeConsents: activeConsents.length,
|
|
||||||
revokedConsents: revokedConsents.length,
|
|
||||||
byCategory,
|
|
||||||
byLegalBasis,
|
|
||||||
conversionRate: Math.round(conversionRate * 10) / 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
/**
|
|
||||||
* API Route: Cookie Banner Configuration
|
|
||||||
*
|
|
||||||
* GET - Cookie Banner Konfiguration abrufen
|
|
||||||
* POST - Cookie Banner Konfiguration speichern
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import {
|
|
||||||
CookieBannerConfig,
|
|
||||||
CookieBannerStyling,
|
|
||||||
CookieBannerTexts,
|
|
||||||
DataPoint,
|
|
||||||
} from '@/lib/sdk/einwilligungen/types'
|
|
||||||
import {
|
|
||||||
generateCookieBannerConfig,
|
|
||||||
DEFAULT_COOKIE_BANNER_STYLING,
|
|
||||||
DEFAULT_COOKIE_BANNER_TEXTS,
|
|
||||||
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
|
|
||||||
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
|
|
||||||
|
|
||||||
// In-Memory Storage fuer Cookie Banner Configs
|
|
||||||
const configStorage = new Map<string, CookieBannerConfig>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/einwilligungen/cookie-banner/config
|
|
||||||
*
|
|
||||||
* Laedt die Cookie Banner Konfiguration fuer einen Tenant
|
|
||||||
*/
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = configStorage.get(tenantId)
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
// Generiere Default-Konfiguration
|
|
||||||
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
|
|
||||||
configStorage.set(tenantId, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(config)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading cookie banner config:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to load cookie banner config' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sdk/v1/einwilligungen/cookie-banner/config
|
|
||||||
*
|
|
||||||
* Speichert oder aktualisiert die Cookie Banner Konfiguration
|
|
||||||
*
|
|
||||||
* Body:
|
|
||||||
* - dataPointIds?: string[] - IDs der Datenpunkte (fuer Neuberechnung)
|
|
||||||
* - styling?: Partial<CookieBannerStyling> - Styling-Optionen
|
|
||||||
* - texts?: Partial<CookieBannerTexts> - Text-Optionen
|
|
||||||
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const {
|
|
||||||
dataPointIds,
|
|
||||||
styling,
|
|
||||||
texts,
|
|
||||||
customDataPoints = [],
|
|
||||||
} = body
|
|
||||||
|
|
||||||
// Hole bestehende Konfiguration oder erstelle neue
|
|
||||||
let config = configStorage.get(tenantId)
|
|
||||||
|
|
||||||
if (dataPointIds && Array.isArray(dataPointIds)) {
|
|
||||||
// Neu berechnen basierend auf Datenpunkten
|
|
||||||
const allDataPoints: DataPoint[] = [
|
|
||||||
...PREDEFINED_DATA_POINTS,
|
|
||||||
...customDataPoints,
|
|
||||||
]
|
|
||||||
|
|
||||||
const selectedDataPoints = dataPointIds
|
|
||||||
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
|
|
||||||
.filter((dp): dp is DataPoint => dp !== undefined)
|
|
||||||
|
|
||||||
config = generateCookieBannerConfig(
|
|
||||||
tenantId,
|
|
||||||
selectedDataPoints,
|
|
||||||
texts,
|
|
||||||
styling
|
|
||||||
)
|
|
||||||
} else if (config) {
|
|
||||||
// Nur Styling/Texts aktualisieren
|
|
||||||
if (styling) {
|
|
||||||
config.styling = {
|
|
||||||
...config.styling,
|
|
||||||
...styling,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (texts) {
|
|
||||||
config.texts = {
|
|
||||||
...config.texts,
|
|
||||||
...texts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.updatedAt = new Date()
|
|
||||||
} else {
|
|
||||||
// Erstelle Default
|
|
||||||
config = generateCookieBannerConfig(
|
|
||||||
tenantId,
|
|
||||||
PREDEFINED_DATA_POINTS,
|
|
||||||
texts,
|
|
||||||
styling
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
configStorage.set(tenantId, config)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
config,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving cookie banner config:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to save cookie banner config' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PUT /api/sdk/v1/einwilligungen/cookie-banner/config
|
|
||||||
*
|
|
||||||
* Aktualisiert einzelne Kategorien
|
|
||||||
*/
|
|
||||||
export async function PUT(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const { categoryId, enabled } = body
|
|
||||||
|
|
||||||
if (!categoryId || typeof enabled !== 'boolean') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'categoryId and enabled required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let config = configStorage.get(tenantId)
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finde und aktualisiere die Kategorie
|
|
||||||
const categoryIndex = config.categories.findIndex((c) => c.id === categoryId)
|
|
||||||
|
|
||||||
if (categoryIndex === -1) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Category not found' },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Essenzielle Cookies koennen nicht deaktiviert werden
|
|
||||||
if (config.categories[categoryIndex].isRequired && !enabled) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Essential cookies cannot be disabled' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
config.categories[categoryIndex].defaultEnabled = enabled
|
|
||||||
config.updatedAt = new Date()
|
|
||||||
|
|
||||||
configStorage.set(tenantId, config)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
category: config.categories[categoryIndex],
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating cookie category:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to update cookie category' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
/**
|
|
||||||
* API Route: Cookie Banner Embed Code
|
|
||||||
*
|
|
||||||
* GET - Generiert den Embed-Code fuer den Cookie Banner
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { CookieBannerConfig, CookieBannerEmbedCode } from '@/lib/sdk/einwilligungen/types'
|
|
||||||
import {
|
|
||||||
generateCookieBannerConfig,
|
|
||||||
generateEmbedCode,
|
|
||||||
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
|
|
||||||
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
|
|
||||||
|
|
||||||
// In-Memory Storage (in Produktion mit configStorage aus config/route.ts teilen)
|
|
||||||
const configStorage = new Map<string, CookieBannerConfig>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/einwilligungen/cookie-banner/embed-code
|
|
||||||
*
|
|
||||||
* Generiert den Embed-Code fuer den Cookie Banner
|
|
||||||
*
|
|
||||||
* Query Parameters:
|
|
||||||
* - privacyPolicyUrl: string - URL zur Datenschutzerklaerung (default: /datenschutz)
|
|
||||||
* - format: 'combined' | 'separate' - Ausgabeformat (default: combined)
|
|
||||||
*/
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const privacyPolicyUrl = searchParams.get('privacyPolicyUrl') || '/datenschutz'
|
|
||||||
const format = searchParams.get('format') || 'combined'
|
|
||||||
|
|
||||||
// Hole oder erstelle Konfiguration
|
|
||||||
let config = configStorage.get(tenantId)
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
|
|
||||||
configStorage.set(tenantId, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generiere Embed-Code
|
|
||||||
const embedCode = generateEmbedCode(config, privacyPolicyUrl)
|
|
||||||
|
|
||||||
if (format === 'separate') {
|
|
||||||
// Separate Dateien zurueckgeben
|
|
||||||
return NextResponse.json({
|
|
||||||
html: embedCode.html,
|
|
||||||
css: embedCode.css,
|
|
||||||
js: embedCode.js,
|
|
||||||
scriptTag: embedCode.scriptTag,
|
|
||||||
instructions: {
|
|
||||||
de: `
|
|
||||||
Fuegen Sie den folgenden Code in Ihre Website ein:
|
|
||||||
|
|
||||||
1. CSS in den <head>-Bereich:
|
|
||||||
<style>${embedCode.css}</style>
|
|
||||||
|
|
||||||
2. HTML vor dem schliessenden </body>-Tag:
|
|
||||||
${embedCode.html}
|
|
||||||
|
|
||||||
3. JavaScript vor dem schliessenden </body>-Tag:
|
|
||||||
<script>${embedCode.js}</script>
|
|
||||||
|
|
||||||
Alternativ koennen Sie die Dateien separat einbinden:
|
|
||||||
- /cookie-banner.css
|
|
||||||
- /cookie-banner.js
|
|
||||||
`,
|
|
||||||
en: `
|
|
||||||
Add the following code to your website:
|
|
||||||
|
|
||||||
1. CSS in the <head> section:
|
|
||||||
<style>${embedCode.css}</style>
|
|
||||||
|
|
||||||
2. HTML before the closing </body> tag:
|
|
||||||
${embedCode.html}
|
|
||||||
|
|
||||||
3. JavaScript before the closing </body> tag:
|
|
||||||
<script>${embedCode.js}</script>
|
|
||||||
|
|
||||||
Alternatively, you can include the files separately:
|
|
||||||
- /cookie-banner.css
|
|
||||||
- /cookie-banner.js
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combined: Alles in einem HTML-Block
|
|
||||||
const combinedCode = `
|
|
||||||
<!-- Cookie Banner - Start -->
|
|
||||||
<style>
|
|
||||||
${embedCode.css}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
${embedCode.html}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
${embedCode.js}
|
|
||||||
</script>
|
|
||||||
<!-- Cookie Banner - End -->
|
|
||||||
`.trim()
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
embedCode: combinedCode,
|
|
||||||
scriptTag: embedCode.scriptTag,
|
|
||||||
config: {
|
|
||||||
tenantId: config.tenantId,
|
|
||||||
categories: config.categories.map((c) => ({
|
|
||||||
id: c.id,
|
|
||||||
name: c.name,
|
|
||||||
isRequired: c.isRequired,
|
|
||||||
defaultEnabled: c.defaultEnabled,
|
|
||||||
})),
|
|
||||||
styling: config.styling,
|
|
||||||
},
|
|
||||||
instructions: {
|
|
||||||
de: `Fuegen Sie den folgenden Code vor dem schliessenden </body>-Tag Ihrer Website ein.`,
|
|
||||||
en: `Add the following code before the closing </body> tag of your website.`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating embed code:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to generate embed code' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sdk/v1/einwilligungen/cookie-banner/embed-code
|
|
||||||
*
|
|
||||||
* Generiert Embed-Code mit benutzerdefinierten Optionen
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const {
|
|
||||||
privacyPolicyUrl = '/datenschutz',
|
|
||||||
styling,
|
|
||||||
texts,
|
|
||||||
language = 'de',
|
|
||||||
} = body
|
|
||||||
|
|
||||||
// Hole oder erstelle Konfiguration
|
|
||||||
let config = configStorage.get(tenantId)
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS, texts, styling)
|
|
||||||
} else {
|
|
||||||
// Wende temporaere Anpassungen an
|
|
||||||
if (styling) {
|
|
||||||
config = {
|
|
||||||
...config,
|
|
||||||
styling: { ...config.styling, ...styling },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (texts) {
|
|
||||||
config = {
|
|
||||||
...config,
|
|
||||||
texts: { ...config.texts, ...texts },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const embedCode = generateEmbedCode(config, privacyPolicyUrl)
|
|
||||||
|
|
||||||
// Generiere Preview HTML
|
|
||||||
const previewHtml = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="${language}">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Cookie Banner Preview</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background: #f1f5f9;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
.preview-content {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 40px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
h1 { color: #1e293b; }
|
|
||||||
p { color: #64748b; line-height: 1.6; }
|
|
||||||
${embedCode.css}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="preview-content">
|
|
||||||
<h1>Cookie Banner Preview</h1>
|
|
||||||
<p>Dies ist eine Vorschau des Cookie Banners. In der produktiven Umgebung wird der Banner auf Ihrer Website angezeigt.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${embedCode.html}
|
|
||||||
|
|
||||||
<script>
|
|
||||||
${embedCode.js}
|
|
||||||
// Force show banner for preview
|
|
||||||
setTimeout(() => {
|
|
||||||
document.getElementById('cookieBanner')?.classList.add('active');
|
|
||||||
document.getElementById('cookieBannerOverlay')?.classList.add('active');
|
|
||||||
}, 100);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`.trim()
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
embedCode: {
|
|
||||||
html: embedCode.html,
|
|
||||||
css: embedCode.css,
|
|
||||||
js: embedCode.js,
|
|
||||||
scriptTag: embedCode.scriptTag,
|
|
||||||
},
|
|
||||||
previewHtml,
|
|
||||||
config: {
|
|
||||||
tenantId: config.tenantId,
|
|
||||||
categories: config.categories.length,
|
|
||||||
styling: config.styling,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating custom embed code:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to generate embed code' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
/**
|
|
||||||
* API Route: Privacy Policy Generator
|
|
||||||
*
|
|
||||||
* POST - Generiert eine Datenschutzerklaerung aus dem Datenpunktkatalog
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import {
|
|
||||||
CompanyInfo,
|
|
||||||
DataPoint,
|
|
||||||
SupportedLanguage,
|
|
||||||
ExportFormat,
|
|
||||||
GeneratedPrivacyPolicy,
|
|
||||||
} from '@/lib/sdk/einwilligungen/types'
|
|
||||||
import {
|
|
||||||
generatePrivacyPolicy,
|
|
||||||
generatePrivacyPolicySections,
|
|
||||||
} from '@/lib/sdk/einwilligungen/generator/privacy-policy'
|
|
||||||
import {
|
|
||||||
PREDEFINED_DATA_POINTS,
|
|
||||||
getDataPointById,
|
|
||||||
} from '@/lib/sdk/einwilligungen/catalog/loader'
|
|
||||||
|
|
||||||
// In-Memory Storage fuer generierte Policies
|
|
||||||
const policyStorage = new Map<string, GeneratedPrivacyPolicy>()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sdk/v1/einwilligungen/privacy-policy/generate
|
|
||||||
*
|
|
||||||
* Generiert eine Datenschutzerklaerung
|
|
||||||
*
|
|
||||||
* Body:
|
|
||||||
* - dataPointIds: string[] - IDs der zu inkludierenden Datenpunkte
|
|
||||||
* - companyInfo: CompanyInfo - Firmeninformationen
|
|
||||||
* - language: 'de' | 'en' - Sprache
|
|
||||||
* - format: 'HTML' | 'MARKDOWN' | 'PDF' | 'DOCX' - Ausgabeformat
|
|
||||||
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
|
|
||||||
*/
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const tenantId = request.headers.get('X-Tenant-ID')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Tenant ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await request.json()
|
|
||||||
const {
|
|
||||||
dataPointIds,
|
|
||||||
companyInfo,
|
|
||||||
language = 'de',
|
|
||||||
format = 'HTML',
|
|
||||||
customDataPoints = [],
|
|
||||||
} = body
|
|
||||||
|
|
||||||
// Validierung
|
|
||||||
if (!companyInfo || !companyInfo.name || !companyInfo.address || !companyInfo.email) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Company info (name, address, email) required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dataPointIds || !Array.isArray(dataPointIds) || dataPointIds.length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'At least one data point ID required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validiere Sprache
|
|
||||||
const validLanguages: SupportedLanguage[] = ['de', 'en']
|
|
||||||
if (!validLanguages.includes(language)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid language. Must be "de" or "en"' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validiere Format
|
|
||||||
const validFormats: ExportFormat[] = ['HTML', 'MARKDOWN', 'PDF', 'DOCX']
|
|
||||||
if (!validFormats.includes(format)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid format. Must be HTML, MARKDOWN, PDF, or DOCX' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sammle alle Datenpunkte
|
|
||||||
const allDataPoints: DataPoint[] = [
|
|
||||||
...PREDEFINED_DATA_POINTS,
|
|
||||||
...customDataPoints,
|
|
||||||
]
|
|
||||||
|
|
||||||
// Filtere nach ausgewaehlten IDs
|
|
||||||
const selectedDataPoints = dataPointIds
|
|
||||||
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
|
|
||||||
.filter((dp): dp is DataPoint => dp !== undefined)
|
|
||||||
|
|
||||||
if (selectedDataPoints.length === 0) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'No valid data points found for the provided IDs' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generiere die Privacy Policy
|
|
||||||
const policy = generatePrivacyPolicy(
|
|
||||||
tenantId,
|
|
||||||
selectedDataPoints,
|
|
||||||
companyInfo as CompanyInfo,
|
|
||||||
language as SupportedLanguage,
|
|
||||||
format as ExportFormat
|
|
||||||
)
|
|
||||||
|
|
||||||
// Speichere fuer spaeteres Abrufen
|
|
||||||
policyStorage.set(policy.id, policy)
|
|
||||||
|
|
||||||
// Fuer PDF/DOCX: Nur Metadaten zurueckgeben, Download separat
|
|
||||||
if (format === 'PDF' || format === 'DOCX') {
|
|
||||||
return NextResponse.json({
|
|
||||||
id: policy.id,
|
|
||||||
tenantId: policy.tenantId,
|
|
||||||
language: policy.language,
|
|
||||||
format: policy.format,
|
|
||||||
generatedAt: policy.generatedAt,
|
|
||||||
version: policy.version,
|
|
||||||
sections: policy.sections.map((s) => ({
|
|
||||||
id: s.id,
|
|
||||||
title: s.title,
|
|
||||||
order: s.order,
|
|
||||||
})),
|
|
||||||
downloadUrl: `/api/sdk/v1/einwilligungen/privacy-policy/${policy.id}/download`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fuer HTML/Markdown: Vollstaendige Policy zurueckgeben
|
|
||||||
return NextResponse.json(policy)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating privacy policy:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to generate privacy policy' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/einwilligungen/privacy-policy/generate
|
|
||||||
*
|
|
||||||
* Liefert eine Vorschau der Abschnitte ohne vollstaendige Generierung
|
|
||||||
*/
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const language = (searchParams.get('language') as SupportedLanguage) || 'de'
|
|
||||||
|
|
||||||
// Liefere die Standard-Abschnittsstruktur
|
|
||||||
const sections = [
|
|
||||||
{ id: 'controller', order: 1, title: { de: '1. Verantwortlicher', en: '1. Data Controller' } },
|
|
||||||
{ id: 'data-collection', order: 2, title: { de: '2. Erhobene personenbezogene Daten', en: '2. Personal Data We Collect' } },
|
|
||||||
{ id: 'purposes', order: 3, title: { de: '3. Zwecke der Datenverarbeitung', en: '3. Purposes of Data Processing' } },
|
|
||||||
{ id: 'legal-basis', order: 4, title: { de: '4. Rechtsgrundlagen der Verarbeitung', en: '4. Legal Basis for Processing' } },
|
|
||||||
{ id: 'recipients', order: 5, title: { de: '5. Empfaenger und Datenweitergabe', en: '5. Recipients and Data Sharing' } },
|
|
||||||
{ id: 'retention', order: 6, title: { de: '6. Speicherdauer', en: '6. Data Retention' } },
|
|
||||||
{ id: 'rights', order: 7, title: { de: '7. Ihre Rechte als betroffene Person', en: '7. Your Rights as a Data Subject' } },
|
|
||||||
{ id: 'cookies', order: 8, title: { de: '8. Cookies und aehnliche Technologien', en: '8. Cookies and Similar Technologies' } },
|
|
||||||
{ id: 'changes', order: 9, title: { de: '9. Aenderungen dieser Datenschutzerklaerung', en: '9. Changes to this Privacy Policy' } },
|
|
||||||
]
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
sections,
|
|
||||||
availableLanguages: ['de', 'en'],
|
|
||||||
availableFormats: ['HTML', 'MARKDOWN', 'PDF', 'DOCX'],
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching sections:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to fetch sections' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SDK Export API
|
|
||||||
*
|
|
||||||
* GET /api/sdk/v1/export?format=json|pdf|zip - Export SDK data
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const format = searchParams.get('format') || 'json'
|
|
||||||
const tenantId = searchParams.get('tenantId') || 'default'
|
|
||||||
|
|
||||||
switch (format) {
|
|
||||||
case 'json':
|
|
||||||
return exportJSON(tenantId)
|
|
||||||
|
|
||||||
case 'pdf':
|
|
||||||
return exportPDF(tenantId)
|
|
||||||
|
|
||||||
case 'zip':
|
|
||||||
return exportZIP(tenantId)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Unknown export format: ${format}` },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to export:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to export' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportJSON(tenantId: string) {
|
|
||||||
// In production, this would fetch the actual state from the database
|
|
||||||
const exportData = {
|
|
||||||
version: '1.0.0',
|
|
||||||
exportedAt: new Date().toISOString(),
|
|
||||||
tenantId,
|
|
||||||
data: {
|
|
||||||
useCases: [],
|
|
||||||
screening: null,
|
|
||||||
modules: [],
|
|
||||||
requirements: [],
|
|
||||||
controls: [],
|
|
||||||
evidence: [],
|
|
||||||
risks: [],
|
|
||||||
dsfa: null,
|
|
||||||
toms: [],
|
|
||||||
vvt: [],
|
|
||||||
documents: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NextResponse(JSON.stringify(exportData, null, 2), {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Content-Disposition': `attachment; filename="compliance-export-${tenantId}-${new Date().toISOString().split('T')[0]}.json"`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportPDF(tenantId: string) {
|
|
||||||
// In production, this would generate a proper PDF using a library like pdfkit or puppeteer
|
|
||||||
// For now, return a placeholder response
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: 'PDF export not yet implemented',
|
|
||||||
message: 'PDF generation requires server-side rendering. Use JSON export for now.',
|
|
||||||
}, { status: 501 })
|
|
||||||
}
|
|
||||||
|
|
||||||
function exportZIP(tenantId: string) {
|
|
||||||
// In production, this would create a ZIP file with multiple documents
|
|
||||||
// For now, return a placeholder response
|
|
||||||
return NextResponse.json({
|
|
||||||
success: false,
|
|
||||||
error: 'ZIP export not yet implemented',
|
|
||||||
message: 'ZIP generation requires additional server-side processing. Use JSON export for now.',
|
|
||||||
}, { status: 501 })
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SDK Flow API
|
|
||||||
*
|
|
||||||
* GET /api/sdk/v1/flow - Get current flow state and suggestions
|
|
||||||
* POST /api/sdk/v1/flow/next - Navigate to next step
|
|
||||||
* POST /api/sdk/v1/flow/previous - Navigate to previous step
|
|
||||||
*/
|
|
||||||
|
|
||||||
const SDK_STEPS = [
|
|
||||||
// Phase 1
|
|
||||||
{ id: 'company-profile', phase: 1, order: 1, name: 'Unternehmensprofil', url: '/sdk/company-profile' },
|
|
||||||
{ id: 'use-case-assessment', phase: 1, order: 2, name: 'Anwendungsfall-Erfassung', url: '/sdk/advisory-board' },
|
|
||||||
{ id: 'screening', phase: 1, order: 3, name: 'System Screening', url: '/sdk/screening' },
|
|
||||||
{ id: 'modules', phase: 1, order: 4, name: 'Compliance Modules', url: '/sdk/modules' },
|
|
||||||
{ id: 'requirements', phase: 1, order: 5, name: 'Requirements', url: '/sdk/requirements' },
|
|
||||||
{ id: 'controls', phase: 1, order: 6, name: 'Controls', url: '/sdk/controls' },
|
|
||||||
{ id: 'evidence', phase: 1, order: 7, name: 'Evidence', url: '/sdk/evidence' },
|
|
||||||
{ id: 'audit-checklist', phase: 1, order: 8, name: 'Audit Checklist', url: '/sdk/audit-checklist' },
|
|
||||||
{ id: 'risks', phase: 1, order: 9, name: 'Risk Matrix', url: '/sdk/risks' },
|
|
||||||
// Phase 2
|
|
||||||
{ id: 'ai-act', phase: 2, order: 1, name: 'AI Act Klassifizierung', url: '/sdk/ai-act' },
|
|
||||||
{ id: 'obligations', phase: 2, order: 2, name: 'Pflichtenübersicht', url: '/sdk/obligations' },
|
|
||||||
{ id: 'dsfa', phase: 2, order: 3, name: 'DSFA', url: '/sdk/dsfa' },
|
|
||||||
{ id: 'tom', phase: 2, order: 4, name: 'TOMs', url: '/sdk/tom' },
|
|
||||||
{ id: 'loeschfristen', phase: 2, order: 5, name: 'Löschfristen', url: '/sdk/loeschfristen' },
|
|
||||||
{ id: 'vvt', phase: 2, order: 6, name: 'Verarbeitungsverzeichnis', url: '/sdk/vvt' },
|
|
||||||
{ id: 'consent', phase: 2, order: 7, name: 'Rechtliche Vorlagen', url: '/sdk/consent' },
|
|
||||||
{ id: 'cookie-banner', phase: 2, order: 8, name: 'Cookie Banner', url: '/sdk/cookie-banner' },
|
|
||||||
{ id: 'einwilligungen', phase: 2, order: 9, name: 'Einwilligungen', url: '/sdk/einwilligungen' },
|
|
||||||
{ id: 'dsr', phase: 2, order: 10, name: 'DSR Portal', url: '/sdk/dsr' },
|
|
||||||
{ id: 'escalations', phase: 2, order: 11, name: 'Escalations', url: '/sdk/escalations' },
|
|
||||||
]
|
|
||||||
|
|
||||||
function getStepIndex(stepId: string): number {
|
|
||||||
return SDK_STEPS.findIndex(s => s.id === stepId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNextStep(currentStepId: string) {
|
|
||||||
const currentIndex = getStepIndex(currentStepId)
|
|
||||||
if (currentIndex === -1 || currentIndex >= SDK_STEPS.length - 1) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return SDK_STEPS[currentIndex + 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPreviousStep(currentStepId: string) {
|
|
||||||
const currentIndex = getStepIndex(currentStepId)
|
|
||||||
if (currentIndex <= 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return SDK_STEPS[currentIndex - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const currentStepId = searchParams.get('currentStep') || 'company-profile'
|
|
||||||
|
|
||||||
const currentStep = SDK_STEPS.find(s => s.id === currentStepId)
|
|
||||||
const nextStep = getNextStep(currentStepId)
|
|
||||||
const previousStep = getPreviousStep(currentStepId)
|
|
||||||
|
|
||||||
// Generate suggestions based on context
|
|
||||||
const suggestions = [
|
|
||||||
{
|
|
||||||
type: 'NAVIGATION',
|
|
||||||
label: nextStep ? `Weiter zu ${nextStep.name}` : 'Flow abgeschlossen',
|
|
||||||
action: nextStep ? `navigate:${nextStep.url}` : null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'ACTION',
|
|
||||||
label: 'Checkpoint validieren',
|
|
||||||
action: 'validate:current',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'HELP',
|
|
||||||
label: 'Hilfe anzeigen',
|
|
||||||
action: 'help:show',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
currentStep,
|
|
||||||
nextStep,
|
|
||||||
previousStep,
|
|
||||||
totalSteps: SDK_STEPS.length,
|
|
||||||
currentIndex: getStepIndex(currentStepId) + 1,
|
|
||||||
suggestions,
|
|
||||||
steps: SDK_STEPS,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to get flow:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to get flow' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { action, currentStepId } = body
|
|
||||||
|
|
||||||
if (!action || !currentStepId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'action and currentStepId are required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let targetStep = null
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'next':
|
|
||||||
targetStep = getNextStep(currentStepId)
|
|
||||||
break
|
|
||||||
case 'previous':
|
|
||||||
targetStep = getPreviousStep(currentStepId)
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Invalid action' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetStep) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'No target step available' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
targetStep,
|
|
||||||
redirectUrl: targetStep.url,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to navigate flow:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to navigate flow' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SDK Document Generation API
|
|
||||||
*
|
|
||||||
* POST /api/sdk/v1/generate - Generate compliance documents
|
|
||||||
*
|
|
||||||
* Supported document types:
|
|
||||||
* - dsfa: Data Protection Impact Assessment
|
|
||||||
* - tom: Technical and Organizational Measures
|
|
||||||
* - vvt: Processing Register (Art. 30 GDPR)
|
|
||||||
* - cookie-banner: Cookie consent banner code
|
|
||||||
* - audit-report: Audit report
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { documentType, context, options } = body
|
|
||||||
|
|
||||||
if (!documentType) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'documentType is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate document based on type
|
|
||||||
let document: unknown = null
|
|
||||||
let generationTime = Date.now()
|
|
||||||
|
|
||||||
switch (documentType) {
|
|
||||||
case 'dsfa':
|
|
||||||
document = generateDSFA(context, options)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'tom':
|
|
||||||
document = generateTOMs(context, options)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'vvt':
|
|
||||||
document = generateVVT(context, options)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'cookie-banner':
|
|
||||||
document = generateCookieBanner(context, options)
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'audit-report':
|
|
||||||
document = generateAuditReport(context, options)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Unknown document type: ${documentType}` },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
generationTime = Date.now() - generationTime
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
documentType,
|
|
||||||
document,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
generationTimeMs: generationTime,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate document:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to generate document' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// DOCUMENT GENERATORS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function generateDSFA(context: unknown, options: unknown) {
|
|
||||||
return {
|
|
||||||
id: `dsfa-${Date.now()}`,
|
|
||||||
status: 'DRAFT',
|
|
||||||
version: 1,
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
id: 'section-1',
|
|
||||||
title: '1. Systematische Beschreibung der Verarbeitungsvorgänge',
|
|
||||||
content: 'Die geplante Verarbeitung umfasst...',
|
|
||||||
status: 'DRAFT',
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'section-2',
|
|
||||||
title: '2. Bewertung der Notwendigkeit und Verhältnismäßigkeit',
|
|
||||||
content: 'Die Verarbeitung ist notwendig für...',
|
|
||||||
status: 'DRAFT',
|
|
||||||
order: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'section-3',
|
|
||||||
title: '3. Bewertung der Risiken für die Rechte und Freiheiten',
|
|
||||||
content: 'Identifizierte Risiken:\n- Risiko 1\n- Risiko 2',
|
|
||||||
status: 'DRAFT',
|
|
||||||
order: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'section-4',
|
|
||||||
title: '4. Abhilfemaßnahmen',
|
|
||||||
content: 'Folgende Maßnahmen werden ergriffen...',
|
|
||||||
status: 'DRAFT',
|
|
||||||
order: 4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
approvals: [],
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateTOMs(context: unknown, options: unknown) {
|
|
||||||
return {
|
|
||||||
toms: [
|
|
||||||
{
|
|
||||||
id: 'tom-1',
|
|
||||||
category: 'Zutrittskontrolle',
|
|
||||||
name: 'Physische Zugangskontrollen',
|
|
||||||
description: 'Maßnahmen zur Verhinderung unbefugten Zutritts zu Datenverarbeitungsanlagen',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'NOT_IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tom-2',
|
|
||||||
category: 'Zugangskontrolle',
|
|
||||||
name: 'Authentifizierung',
|
|
||||||
description: 'Multi-Faktor-Authentifizierung für alle Systeme',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'NOT_IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tom-3',
|
|
||||||
category: 'Zugriffskontrolle',
|
|
||||||
name: 'Rollenbasierte Zugriffskontrolle',
|
|
||||||
description: 'RBAC-System für granulare Berechtigungsvergabe',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'NOT_IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tom-4',
|
|
||||||
category: 'Weitergabekontrolle',
|
|
||||||
name: 'Verschlüsselung',
|
|
||||||
description: 'Ende-zu-Ende-Verschlüsselung für Datenübertragung',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'NOT_IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tom-5',
|
|
||||||
category: 'Eingabekontrolle',
|
|
||||||
name: 'Audit Logging',
|
|
||||||
description: 'Protokollierung aller Dateneingaben und -änderungen',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'NOT_IMPLEMENTED',
|
|
||||||
priority: 'MEDIUM',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateVVT(context: unknown, options: unknown) {
|
|
||||||
return {
|
|
||||||
processingActivities: [
|
|
||||||
{
|
|
||||||
id: 'pa-1',
|
|
||||||
name: 'Kundenmanagement',
|
|
||||||
purpose: 'Verwaltung von Kundenbeziehungen und Aufträgen',
|
|
||||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Vertrag)',
|
|
||||||
dataCategories: ['Name', 'Kontaktdaten', 'Bestellhistorie'],
|
|
||||||
dataSubjects: ['Kunden'],
|
|
||||||
recipients: ['Interne Mitarbeiter', 'Zahlungsdienstleister'],
|
|
||||||
thirdCountryTransfers: false,
|
|
||||||
retentionPeriod: '10 Jahre (handelsrechtliche Aufbewahrungspflicht)',
|
|
||||||
technicalMeasures: ['Verschlüsselung', 'Zugriffskontrolle'],
|
|
||||||
organizationalMeasures: ['Schulungen', 'Vertraulichkeitsverpflichtung'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
version: '1.0',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateCookieBanner(context: unknown, options: unknown) {
|
|
||||||
return {
|
|
||||||
id: `cookie-${Date.now()}`,
|
|
||||||
style: 'BANNER',
|
|
||||||
position: 'BOTTOM',
|
|
||||||
theme: 'LIGHT',
|
|
||||||
texts: {
|
|
||||||
title: 'Cookie-Einstellungen',
|
|
||||||
description: 'Wir verwenden Cookies, um Ihnen die beste Nutzererfahrung zu bieten.',
|
|
||||||
acceptAll: 'Alle akzeptieren',
|
|
||||||
rejectAll: 'Alle ablehnen',
|
|
||||||
settings: 'Einstellungen',
|
|
||||||
save: 'Speichern',
|
|
||||||
},
|
|
||||||
categories: [
|
|
||||||
{
|
|
||||||
id: 'necessary',
|
|
||||||
name: 'Notwendig',
|
|
||||||
description: 'Diese Cookies sind für die Grundfunktionen erforderlich.',
|
|
||||||
required: true,
|
|
||||||
cookies: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'analytics',
|
|
||||||
name: 'Analyse',
|
|
||||||
description: 'Diese Cookies helfen uns, die Nutzung zu verstehen.',
|
|
||||||
required: false,
|
|
||||||
cookies: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'marketing',
|
|
||||||
name: 'Marketing',
|
|
||||||
description: 'Diese Cookies werden für Werbezwecke verwendet.',
|
|
||||||
required: false,
|
|
||||||
cookies: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
generatedCode: {
|
|
||||||
html: `<!-- Cookie Banner HTML -->
|
|
||||||
<div id="cookie-banner" class="cookie-banner">
|
|
||||||
<div class="cookie-content">
|
|
||||||
<h3>Cookie-Einstellungen</h3>
|
|
||||||
<p>Wir verwenden Cookies, um Ihnen die beste Nutzererfahrung zu bieten.</p>
|
|
||||||
<div class="cookie-actions">
|
|
||||||
<button onclick="acceptAll()">Alle akzeptieren</button>
|
|
||||||
<button onclick="rejectAll()">Alle ablehnen</button>
|
|
||||||
<button onclick="showSettings()">Einstellungen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`,
|
|
||||||
css: `.cookie-banner {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background: white;
|
|
||||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
|
|
||||||
padding: 20px;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
.cookie-content { max-width: 1200px; margin: 0 auto; }
|
|
||||||
.cookie-actions { margin-top: 15px; display: flex; gap: 10px; }
|
|
||||||
.cookie-actions button { padding: 10px 20px; border-radius: 5px; cursor: pointer; }`,
|
|
||||||
js: `function acceptAll() {
|
|
||||||
setCookie('consent', 'all', 365);
|
|
||||||
document.getElementById('cookie-banner').style.display = 'none';
|
|
||||||
}
|
|
||||||
function rejectAll() {
|
|
||||||
setCookie('consent', 'necessary', 365);
|
|
||||||
document.getElementById('cookie-banner').style.display = 'none';
|
|
||||||
}
|
|
||||||
function setCookie(name, value, days) {
|
|
||||||
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
||||||
document.cookie = name + '=' + value + '; expires=' + expires + '; path=/; SameSite=Lax';
|
|
||||||
}`,
|
|
||||||
},
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateAuditReport(context: unknown, options: unknown) {
|
|
||||||
return {
|
|
||||||
id: `audit-${Date.now()}`,
|
|
||||||
title: 'Compliance Audit Report',
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
summary: {
|
|
||||||
totalChecks: 50,
|
|
||||||
passed: 35,
|
|
||||||
failed: 10,
|
|
||||||
warnings: 5,
|
|
||||||
complianceScore: 70,
|
|
||||||
},
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
title: 'Executive Summary',
|
|
||||||
content: 'Dieser Bericht fasst den aktuellen Compliance-Status zusammen...',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Methodik',
|
|
||||||
content: 'Die Prüfung wurde gemäß ISO 27001 und DSGVO durchgeführt...',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Ergebnisse',
|
|
||||||
content: 'Hauptabweichungen: 3\nNebenabweichungen: 7\nEmpfehlungen: 5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Empfehlungen',
|
|
||||||
content: '1. Implementierung von MFA\n2. Verbesserung der Dokumentation\n3. Regelmäßige Schulungen',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
/**
|
|
||||||
* Incidents/Breach Management API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/incidents/* requests to ai-compliance-sdk backend
|
|
||||||
* Supports PDF generation for authority notification forms
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/incidents`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
const authHeader = request.headers.get('authorization')
|
|
||||||
if (authHeader) {
|
|
||||||
headers['Authorization'] = authHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenantHeader = request.headers.get('x-tenant-id')
|
|
||||||
if (tenantHeader) {
|
|
||||||
headers['X-Tenant-Id'] = tenantHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
||||||
const contentType = request.headers.get('content-type')
|
|
||||||
if (contentType?.includes('application/json')) {
|
|
||||||
try {
|
|
||||||
const text = await request.text()
|
|
||||||
if (text && text.trim()) {
|
|
||||||
fetchOptions.body = text
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Empty or invalid body
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
// Handle non-JSON responses (PDF authority forms, exports)
|
|
||||||
const responseContentType = response.headers.get('content-type')
|
|
||||||
if (responseContentType?.includes('application/pdf') ||
|
|
||||||
responseContentType?.includes('application/octet-stream')) {
|
|
||||||
const blob = await response.blob()
|
|
||||||
return new NextResponse(blob, {
|
|
||||||
status: response.status,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': responseContentType,
|
|
||||||
'Content-Disposition': response.headers.get('content-disposition') || '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Incidents API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PATCH(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PATCH')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
/**
|
|
||||||
* Industry Templates API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/industry/* requests to ai-compliance-sdk backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/industry`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
|
|
||||||
for (const name of headerNames) {
|
|
||||||
const value = request.headers.get(name)
|
|
||||||
if (value) {
|
|
||||||
headers[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Industry API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
/**
|
|
||||||
* Multi-Tenant API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/multi-tenant/* requests to ai-compliance-sdk backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/multi-tenant`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward all relevant headers
|
|
||||||
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
|
|
||||||
for (const name of headerNames) {
|
|
||||||
const value = request.headers.get(name)
|
|
||||||
if (value) {
|
|
||||||
headers[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward body for POST/PUT/PATCH/DELETE
|
|
||||||
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
||||||
try {
|
|
||||||
const body = await request.text()
|
|
||||||
if (body) {
|
|
||||||
fetchOptions.body = body
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// No body to forward
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Multi-Tenant API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
/**
|
|
||||||
* Reporting API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/reporting/* requests to ai-compliance-sdk backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/reporting`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward all relevant headers
|
|
||||||
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
|
|
||||||
for (const name of headerNames) {
|
|
||||||
const value = request.headers.get(name)
|
|
||||||
if (value) {
|
|
||||||
headers[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Reporting API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
/**
|
|
||||||
* SSO API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/sso/* requests to ai-compliance-sdk backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/sso`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward all relevant headers
|
|
||||||
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
|
|
||||||
for (const name of headerNames) {
|
|
||||||
const value = request.headers.get(name)
|
|
||||||
if (value) {
|
|
||||||
headers[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward body for POST/PUT/PATCH/DELETE
|
|
||||||
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
||||||
try {
|
|
||||||
const body = await request.text()
|
|
||||||
if (body) {
|
|
||||||
fetchOptions.body = body
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// No body to forward
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('SSO API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SDK State Management API
|
|
||||||
*
|
|
||||||
* GET /api/sdk/v1/state?tenantId=xxx - Load state for a tenant
|
|
||||||
* POST /api/sdk/v1/state - Save state for a tenant
|
|
||||||
* DELETE /api/sdk/v1/state?tenantId=xxx - Clear state for a tenant
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Versioning for optimistic locking
|
|
||||||
* - Last-Modified headers
|
|
||||||
* - ETag support for caching
|
|
||||||
* - Prepared for PostgreSQL migration
|
|
||||||
*/
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TYPES
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface StoredState {
|
|
||||||
state: unknown
|
|
||||||
version: number
|
|
||||||
userId?: string
|
|
||||||
createdAt: string
|
|
||||||
updatedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// STORAGE LAYER (Abstract - Easy to swap to PostgreSQL)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In-memory storage for development
|
|
||||||
* TODO: Replace with PostgreSQL implementation
|
|
||||||
*
|
|
||||||
* PostgreSQL Schema:
|
|
||||||
* CREATE TABLE sdk_states (
|
|
||||||
* id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
* tenant_id VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
* user_id VARCHAR(255),
|
|
||||||
* state JSONB NOT NULL,
|
|
||||||
* version INTEGER DEFAULT 1,
|
|
||||||
* created_at TIMESTAMP DEFAULT NOW(),
|
|
||||||
* updated_at TIMESTAMP DEFAULT NOW()
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* CREATE INDEX idx_sdk_states_tenant ON sdk_states(tenant_id);
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface StateStore {
|
|
||||||
get(tenantId: string): Promise<StoredState | null>
|
|
||||||
save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState>
|
|
||||||
delete(tenantId: string): Promise<boolean>
|
|
||||||
}
|
|
||||||
|
|
||||||
class InMemoryStateStore implements StateStore {
|
|
||||||
private store: Map<string, StoredState> = new Map()
|
|
||||||
|
|
||||||
async get(tenantId: string): Promise<StoredState | null> {
|
|
||||||
return this.store.get(tenantId) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(
|
|
||||||
tenantId: string,
|
|
||||||
state: unknown,
|
|
||||||
userId?: string,
|
|
||||||
expectedVersion?: number
|
|
||||||
): Promise<StoredState> {
|
|
||||||
const existing = this.store.get(tenantId)
|
|
||||||
|
|
||||||
// Optimistic locking check
|
|
||||||
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
|
|
||||||
const error = new Error('Version conflict') as Error & { status: number }
|
|
||||||
error.status = 409
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
const newVersion = existing ? existing.version + 1 : 1
|
|
||||||
|
|
||||||
const stored: StoredState = {
|
|
||||||
state: {
|
|
||||||
...(state as object),
|
|
||||||
lastModified: now,
|
|
||||||
},
|
|
||||||
version: newVersion,
|
|
||||||
userId,
|
|
||||||
createdAt: existing?.createdAt || now,
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.store.set(tenantId, stored)
|
|
||||||
return stored
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(tenantId: string): Promise<boolean> {
|
|
||||||
return this.store.delete(tenantId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Future PostgreSQL implementation would look like:
|
|
||||||
// class PostgreSQLStateStore implements StateStore {
|
|
||||||
// private db: Pool
|
|
||||||
//
|
|
||||||
// constructor(connectionString: string) {
|
|
||||||
// this.db = new Pool({ connectionString })
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// async get(tenantId: string): Promise<StoredState | null> {
|
|
||||||
// const result = await this.db.query(
|
|
||||||
// 'SELECT state, version, user_id, created_at, updated_at FROM sdk_states WHERE tenant_id = $1',
|
|
||||||
// [tenantId]
|
|
||||||
// )
|
|
||||||
// if (result.rows.length === 0) return null
|
|
||||||
// const row = result.rows[0]
|
|
||||||
// return {
|
|
||||||
// state: row.state,
|
|
||||||
// version: row.version,
|
|
||||||
// userId: row.user_id,
|
|
||||||
// createdAt: row.created_at,
|
|
||||||
// updatedAt: row.updated_at,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// async save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState> {
|
|
||||||
// // Use UPSERT with version check
|
|
||||||
// const result = await this.db.query(`
|
|
||||||
// INSERT INTO sdk_states (tenant_id, user_id, state, version)
|
|
||||||
// VALUES ($1, $2, $3, 1)
|
|
||||||
// ON CONFLICT (tenant_id) DO UPDATE SET
|
|
||||||
// state = $3,
|
|
||||||
// user_id = COALESCE($2, sdk_states.user_id),
|
|
||||||
// version = sdk_states.version + 1,
|
|
||||||
// updated_at = NOW()
|
|
||||||
// WHERE ($4::int IS NULL OR sdk_states.version = $4)
|
|
||||||
// RETURNING version, created_at, updated_at
|
|
||||||
// `, [tenantId, userId, JSON.stringify(state), expectedVersion])
|
|
||||||
//
|
|
||||||
// if (result.rows.length === 0) {
|
|
||||||
// throw new Error('Version conflict')
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return {
|
|
||||||
// state,
|
|
||||||
// version: result.rows[0].version,
|
|
||||||
// userId,
|
|
||||||
// createdAt: result.rows[0].created_at,
|
|
||||||
// updatedAt: result.rows[0].updated_at,
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// async delete(tenantId: string): Promise<boolean> {
|
|
||||||
// const result = await this.db.query(
|
|
||||||
// 'DELETE FROM sdk_states WHERE tenant_id = $1',
|
|
||||||
// [tenantId]
|
|
||||||
// )
|
|
||||||
// return result.rowCount > 0
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Use in-memory store for now
|
|
||||||
const stateStore: StateStore = new InMemoryStateStore()
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HELPER FUNCTIONS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function generateETag(version: number, updatedAt: string): string {
|
|
||||||
return `"${version}-${Buffer.from(updatedAt).toString('base64').slice(0, 8)}"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HANDLERS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const tenantId = searchParams.get('tenantId')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const stored = await stateStore.get(tenantId)
|
|
||||||
|
|
||||||
if (!stored) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'State not found', tenantId },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const etag = generateETag(stored.version, stored.updatedAt)
|
|
||||||
|
|
||||||
// Check If-None-Match header for caching
|
|
||||||
const ifNoneMatch = request.headers.get('If-None-Match')
|
|
||||||
if (ifNoneMatch === etag) {
|
|
||||||
return new NextResponse(null, { status: 304 })
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
tenantId,
|
|
||||||
state: stored.state,
|
|
||||||
version: stored.version,
|
|
||||||
lastModified: stored.updatedAt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'ETag': etag,
|
|
||||||
'Last-Modified': stored.updatedAt,
|
|
||||||
'Cache-Control': 'private, no-cache',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load SDK state:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to load state' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { tenantId, state, version } = body
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'state is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check If-Match header for optimistic locking
|
|
||||||
const ifMatch = request.headers.get('If-Match')
|
|
||||||
const expectedVersion = ifMatch ? parseInt(ifMatch, 10) : version
|
|
||||||
|
|
||||||
const stored = await stateStore.save(tenantId, state, body.userId, expectedVersion)
|
|
||||||
|
|
||||||
const etag = generateETag(stored.version, stored.updatedAt)
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
tenantId,
|
|
||||||
state: stored.state,
|
|
||||||
version: stored.version,
|
|
||||||
lastModified: stored.updatedAt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'ETag': etag,
|
|
||||||
'Last-Modified': stored.updatedAt,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
const err = error as Error & { status?: number }
|
|
||||||
|
|
||||||
// Handle version conflict
|
|
||||||
if (err.status === 409 || err.message === 'Version conflict') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'Version conflict. State was modified by another request.',
|
|
||||||
code: 'VERSION_CONFLICT',
|
|
||||||
},
|
|
||||||
{ status: 409 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Failed to save SDK state:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to save state' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const tenantId = searchParams.get('tenantId')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleted = await stateStore.delete(tenantId)
|
|
||||||
|
|
||||||
if (!deleted) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'State not found', tenantId },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
tenantId,
|
|
||||||
deletedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete SDK state:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to delete state' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HEALTH CHECK
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json({ status: 'ok' }, {
|
|
||||||
headers: {
|
|
||||||
'Allow': 'GET, POST, DELETE, OPTIONS',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { TOMRulesEngine } from '@/lib/sdk/tom-generator/rules-engine'
|
|
||||||
import { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TOM Generator Controls Evaluation API
|
|
||||||
*
|
|
||||||
* POST /api/sdk/v1/tom-generator/controls/evaluate - Evaluate controls for given state
|
|
||||||
*
|
|
||||||
* Request body:
|
|
||||||
* {
|
|
||||||
* state: TOMGeneratorState
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Response:
|
|
||||||
* {
|
|
||||||
* evaluations: RulesEngineResult[]
|
|
||||||
* derivedTOMs: DerivedTOM[]
|
|
||||||
* summary: {
|
|
||||||
* total: number
|
|
||||||
* required: number
|
|
||||||
* recommended: number
|
|
||||||
* optional: number
|
|
||||||
* notApplicable: number
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { state } = body
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'state is required in request body' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse dates in state
|
|
||||||
const parsedState: TOMGeneratorState = {
|
|
||||||
...state,
|
|
||||||
createdAt: new Date(state.createdAt),
|
|
||||||
updatedAt: new Date(state.updatedAt),
|
|
||||||
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
|
|
||||||
...step,
|
|
||||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
|
||||||
})) || [],
|
|
||||||
documents: [],
|
|
||||||
derivedTOMs: [],
|
|
||||||
gapAnalysis: null,
|
|
||||||
exports: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize rules engine and evaluate
|
|
||||||
const engine = new TOMRulesEngine()
|
|
||||||
const evaluations = engine.evaluateControls(parsedState)
|
|
||||||
const derivedTOMs = engine.deriveAllTOMs(parsedState)
|
|
||||||
|
|
||||||
// Calculate summary
|
|
||||||
const summary = {
|
|
||||||
total: evaluations.length,
|
|
||||||
required: evaluations.filter((e) => e.applicability === 'REQUIRED').length,
|
|
||||||
recommended: evaluations.filter((e) => e.applicability === 'RECOMMENDED').length,
|
|
||||||
optional: evaluations.filter((e) => e.applicability === 'OPTIONAL').length,
|
|
||||||
notApplicable: evaluations.filter((e) => e.applicability === 'NOT_APPLICABLE').length,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group by category
|
|
||||||
const byCategory: Record<string, typeof evaluations> = {}
|
|
||||||
evaluations.forEach((e) => {
|
|
||||||
const category = e.controlId.split('-')[1] // Extract category from ID like TOM-AC-01
|
|
||||||
if (!byCategory[category]) {
|
|
||||||
byCategory[category] = []
|
|
||||||
}
|
|
||||||
byCategory[category].push(e)
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
evaluations,
|
|
||||||
derivedTOMs,
|
|
||||||
summary,
|
|
||||||
byCategory,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to evaluate controls:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to evaluate controls' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ status: 'ok' },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Allow: 'POST, OPTIONS',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import {
|
|
||||||
getAllControls,
|
|
||||||
getControlById,
|
|
||||||
getControlsByCategory,
|
|
||||||
searchControls,
|
|
||||||
getCategories,
|
|
||||||
} from '@/lib/sdk/tom-generator/controls/loader'
|
|
||||||
import { ControlCategory } from '@/lib/sdk/tom-generator/types'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TOM Generator Controls API
|
|
||||||
*
|
|
||||||
* GET /api/sdk/v1/tom-generator/controls - List all controls
|
|
||||||
* GET /api/sdk/v1/tom-generator/controls?id=xxx - Get single control
|
|
||||||
* GET /api/sdk/v1/tom-generator/controls?category=xxx - Filter by category
|
|
||||||
* GET /api/sdk/v1/tom-generator/controls?search=xxx - Search controls
|
|
||||||
* GET /api/sdk/v1/tom-generator/controls?categories=true - Get categories list
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const id = searchParams.get('id')
|
|
||||||
const category = searchParams.get('category')
|
|
||||||
const search = searchParams.get('search')
|
|
||||||
const categoriesOnly = searchParams.get('categories')
|
|
||||||
const language = (searchParams.get('language') || 'de') as 'de' | 'en'
|
|
||||||
|
|
||||||
// Get categories list
|
|
||||||
if (categoriesOnly === 'true') {
|
|
||||||
const categories = getCategories()
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: categories,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get single control by ID
|
|
||||||
if (id) {
|
|
||||||
const control = getControlById(id)
|
|
||||||
if (!control) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `Control not found: ${id}` },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
...control,
|
|
||||||
// Return localized name and description
|
|
||||||
localizedName: control.name[language],
|
|
||||||
localizedDescription: control.description[language],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by category
|
|
||||||
if (category) {
|
|
||||||
const controls = getControlsByCategory(category as ControlCategory)
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: controls.map((c) => ({
|
|
||||||
...c,
|
|
||||||
localizedName: c.name[language],
|
|
||||||
localizedDescription: c.description[language],
|
|
||||||
})),
|
|
||||||
meta: {
|
|
||||||
category,
|
|
||||||
count: controls.length,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search controls
|
|
||||||
if (search) {
|
|
||||||
const controls = searchControls(search, language)
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: controls.map((c) => ({
|
|
||||||
...c,
|
|
||||||
localizedName: c.name[language],
|
|
||||||
localizedDescription: c.description[language],
|
|
||||||
})),
|
|
||||||
meta: {
|
|
||||||
query: search,
|
|
||||||
count: controls.length,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all controls
|
|
||||||
const controls = getAllControls()
|
|
||||||
const categories = getCategories()
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: controls.map((c) => ({
|
|
||||||
...c,
|
|
||||||
localizedName: c.name[language],
|
|
||||||
localizedDescription: c.description[language],
|
|
||||||
})),
|
|
||||||
meta: {
|
|
||||||
totalControls: controls.length,
|
|
||||||
categories: categories.length,
|
|
||||||
language,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch controls:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch controls' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ status: 'ok' },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Allow: 'GET, OPTIONS',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { TOMDocumentAnalyzer } from '@/lib/sdk/tom-generator/ai/document-analyzer'
|
|
||||||
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TOM Generator Evidence Analysis API
|
|
||||||
*
|
|
||||||
* POST /api/sdk/v1/tom-generator/evidence/[id]/analyze - Analyze evidence document with AI
|
|
||||||
*
|
|
||||||
* Request body:
|
|
||||||
* {
|
|
||||||
* tenantId: string
|
|
||||||
* documentText?: string (if already extracted)
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { id } = await params
|
|
||||||
const body = await request.json()
|
|
||||||
const { tenantId, documentText } = body
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the document
|
|
||||||
const document = await evidenceStore.getById(tenantId, id)
|
|
||||||
if (!document) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `Document not found: ${id}` },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if already analyzed
|
|
||||||
if (document.aiAnalysis && document.status === 'ANALYZED') {
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: document.aiAnalysis,
|
|
||||||
meta: {
|
|
||||||
alreadyAnalyzed: true,
|
|
||||||
analyzedAt: document.aiAnalysis.analyzedAt,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get document text (in production, this would be extracted from the file)
|
|
||||||
const text = documentText || `[Document content from ${document.originalName}]`
|
|
||||||
|
|
||||||
// Initialize analyzer
|
|
||||||
const analyzer = new TOMDocumentAnalyzer()
|
|
||||||
|
|
||||||
// Analyze the document
|
|
||||||
const analysisResult = await analyzer.analyzeDocument(
|
|
||||||
document,
|
|
||||||
text,
|
|
||||||
'de'
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if analysis was successful
|
|
||||||
if (!analysisResult.success || !analysisResult.analysis) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: analysisResult.error || 'Analysis failed' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const analysis = analysisResult.analysis
|
|
||||||
|
|
||||||
// Update the document with analysis results
|
|
||||||
const updatedDocument = await evidenceStore.update(tenantId, id, {
|
|
||||||
aiAnalysis: analysis,
|
|
||||||
status: 'ANALYZED',
|
|
||||||
linkedControlIds: [
|
|
||||||
...new Set([
|
|
||||||
...document.linkedControlIds,
|
|
||||||
...analysis.applicableControls,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
analysis,
|
|
||||||
document: updatedDocument,
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
documentId: id,
|
|
||||||
analyzedAt: analysis.analyzedAt,
|
|
||||||
confidence: analysis.confidence,
|
|
||||||
applicableControlsCount: analysis.applicableControls.length,
|
|
||||||
gapsCount: analysis.gaps.length,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to analyze evidence:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to analyze evidence' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ status: 'ok' },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Allow: 'POST, OPTIONS',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { DocumentType } from '@/lib/sdk/tom-generator/types'
|
|
||||||
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TOM Generator Evidence API
|
|
||||||
*
|
|
||||||
* GET /api/sdk/v1/tom-generator/evidence?tenantId=xxx - List all evidence documents
|
|
||||||
* DELETE /api/sdk/v1/tom-generator/evidence?tenantId=xxx&id=xxx - Delete evidence
|
|
||||||
*/
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HANDLERS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const tenantId = searchParams.get('tenantId')
|
|
||||||
const documentType = searchParams.get('type') as DocumentType | null
|
|
||||||
const status = searchParams.get('status')
|
|
||||||
const id = searchParams.get('id')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get single document
|
|
||||||
if (id) {
|
|
||||||
const document = await evidenceStore.getById(tenantId, id)
|
|
||||||
if (!document) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `Document not found: ${id}` },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: document,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by type
|
|
||||||
if (documentType) {
|
|
||||||
const documents = await evidenceStore.getByType(tenantId, documentType)
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: documents,
|
|
||||||
meta: {
|
|
||||||
count: documents.length,
|
|
||||||
filter: { type: documentType },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
if (status) {
|
|
||||||
const documents = await evidenceStore.getByStatus(tenantId, status)
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: documents,
|
|
||||||
meta: {
|
|
||||||
count: documents.length,
|
|
||||||
filter: { status },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all documents
|
|
||||||
const documents = await evidenceStore.getAll(tenantId)
|
|
||||||
|
|
||||||
// Group by type for summary
|
|
||||||
const byType: Record<string, number> = {}
|
|
||||||
const byStatus: Record<string, number> = {}
|
|
||||||
documents.forEach((doc) => {
|
|
||||||
byType[doc.documentType] = (byType[doc.documentType] || 0) + 1
|
|
||||||
byStatus[doc.status] = (byStatus[doc.status] || 0) + 1
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: documents,
|
|
||||||
meta: {
|
|
||||||
count: documents.length,
|
|
||||||
byType,
|
|
||||||
byStatus,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch evidence:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch evidence' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const tenantId = searchParams.get('tenantId')
|
|
||||||
const id = searchParams.get('id')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'id is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleted = await evidenceStore.delete(tenantId, id)
|
|
||||||
|
|
||||||
if (!deleted) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `Document not found: ${id}` },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
id,
|
|
||||||
deletedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete evidence:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to delete evidence' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ status: 'ok' },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Allow: 'GET, DELETE, OPTIONS',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { EvidenceDocument, DocumentType } from '@/lib/sdk/tom-generator/types'
|
|
||||||
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TOM Generator Evidence Upload API
|
|
||||||
*
|
|
||||||
* POST /api/sdk/v1/tom-generator/evidence/upload - Upload evidence document
|
|
||||||
*
|
|
||||||
* Request: multipart/form-data
|
|
||||||
* - file: File
|
|
||||||
* - tenantId: string
|
|
||||||
* - documentType: DocumentType
|
|
||||||
* - validFrom?: string (ISO date)
|
|
||||||
* - validUntil?: string (ISO date)
|
|
||||||
* - linkedControlIds?: string (comma-separated)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Document type detection based on filename patterns
|
|
||||||
function detectDocumentType(filename: string, mimeType: string): DocumentType {
|
|
||||||
const lower = filename.toLowerCase()
|
|
||||||
|
|
||||||
if (lower.includes('avv') || lower.includes('auftragsverarbeitung')) {
|
|
||||||
return 'AVV'
|
|
||||||
}
|
|
||||||
if (lower.includes('dpa') || lower.includes('data processing')) {
|
|
||||||
return 'DPA'
|
|
||||||
}
|
|
||||||
if (lower.includes('sla') || lower.includes('service level')) {
|
|
||||||
return 'SLA'
|
|
||||||
}
|
|
||||||
if (lower.includes('nda') || lower.includes('vertraulichkeit') || lower.includes('geheimhaltung')) {
|
|
||||||
return 'NDA'
|
|
||||||
}
|
|
||||||
if (lower.includes('policy') || lower.includes('richtlinie')) {
|
|
||||||
return 'POLICY'
|
|
||||||
}
|
|
||||||
if (lower.includes('cert') || lower.includes('zertifikat') || lower.includes('iso')) {
|
|
||||||
return 'CERTIFICATE'
|
|
||||||
}
|
|
||||||
if (lower.includes('audit') || lower.includes('prüf') || lower.includes('bericht')) {
|
|
||||||
return 'AUDIT_REPORT'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'OTHER'
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const formData = await request.formData()
|
|
||||||
const file = formData.get('file') as File | null
|
|
||||||
const tenantId = formData.get('tenantId') as string | null
|
|
||||||
const documentType = formData.get('documentType') as DocumentType | null
|
|
||||||
const validFrom = formData.get('validFrom') as string | null
|
|
||||||
const validUntil = formData.get('validUntil') as string | null
|
|
||||||
const linkedControlIdsStr = formData.get('linkedControlIds') as string | null
|
|
||||||
const uploadedBy = formData.get('uploadedBy') as string | null
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'file is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file data
|
|
||||||
const arrayBuffer = await file.arrayBuffer()
|
|
||||||
const buffer = Buffer.from(arrayBuffer)
|
|
||||||
|
|
||||||
// Generate hash for deduplication
|
|
||||||
const hash = crypto.createHash('sha256').update(buffer).digest('hex')
|
|
||||||
|
|
||||||
// Generate unique filename
|
|
||||||
const id = crypto.randomUUID()
|
|
||||||
const ext = file.name.split('.').pop() || 'bin'
|
|
||||||
const filename = `${id}.${ext}`
|
|
||||||
|
|
||||||
// Detect document type if not provided
|
|
||||||
const detectedType = detectDocumentType(file.name, file.type)
|
|
||||||
const finalDocumentType = documentType || detectedType
|
|
||||||
|
|
||||||
// Parse linked control IDs
|
|
||||||
const linkedControlIds = linkedControlIdsStr
|
|
||||||
? linkedControlIdsStr.split(',').map((s) => s.trim()).filter(Boolean)
|
|
||||||
: []
|
|
||||||
|
|
||||||
// Create evidence document
|
|
||||||
const document: EvidenceDocument = {
|
|
||||||
id,
|
|
||||||
filename,
|
|
||||||
originalName: file.name,
|
|
||||||
mimeType: file.type,
|
|
||||||
size: file.size,
|
|
||||||
uploadedAt: new Date(),
|
|
||||||
uploadedBy: uploadedBy || 'unknown',
|
|
||||||
documentType: finalDocumentType,
|
|
||||||
detectedType,
|
|
||||||
hash,
|
|
||||||
validFrom: validFrom ? new Date(validFrom) : null,
|
|
||||||
validUntil: validUntil ? new Date(validUntil) : null,
|
|
||||||
linkedControlIds,
|
|
||||||
aiAnalysis: null,
|
|
||||||
status: 'PENDING',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the document metadata
|
|
||||||
// Note: In production, the actual file would be stored in MinIO/S3
|
|
||||||
await evidenceStore.add(tenantId, document)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
id: document.id,
|
|
||||||
filename: document.filename,
|
|
||||||
originalName: document.originalName,
|
|
||||||
mimeType: document.mimeType,
|
|
||||||
size: document.size,
|
|
||||||
documentType: document.documentType,
|
|
||||||
detectedType: document.detectedType,
|
|
||||||
status: document.status,
|
|
||||||
uploadedAt: document.uploadedAt.toISOString(),
|
|
||||||
},
|
|
||||||
meta: {
|
|
||||||
hash,
|
|
||||||
needsAnalysis: true,
|
|
||||||
analyzeUrl: `/api/sdk/v1/tom-generator/evidence/${id}/analyze`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to upload evidence:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to upload evidence' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ status: 'ok' },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Allow: 'POST, OPTIONS',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
|
|
||||||
import { generateDOCXContent, generateDOCXFilename } from '@/lib/sdk/tom-generator/export/docx'
|
|
||||||
import { generatePDFContent, generatePDFFilename } from '@/lib/sdk/tom-generator/export/pdf'
|
|
||||||
import { generateZIPFiles, generateZIPFilename } from '@/lib/sdk/tom-generator/export/zip'
|
|
||||||
import crypto from 'crypto'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TOM Generator Export API
|
|
||||||
*
|
|
||||||
* POST /api/sdk/v1/tom-generator/export - Generate export
|
|
||||||
*
|
|
||||||
* Request body:
|
|
||||||
* {
|
|
||||||
* tenantId: string
|
|
||||||
* format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
|
|
||||||
* language: 'de' | 'en'
|
|
||||||
* state: TOMGeneratorState
|
|
||||||
* options?: {
|
|
||||||
* includeEvidence?: boolean
|
|
||||||
* includeGapAnalysis?: boolean
|
|
||||||
* companyLogo?: string (base64)
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
// In-memory export store for tracking exports
|
|
||||||
interface StoredExport {
|
|
||||||
id: string
|
|
||||||
tenantId: string
|
|
||||||
format: string
|
|
||||||
filename: string
|
|
||||||
content: string // Base64 encoded content
|
|
||||||
generatedAt: Date
|
|
||||||
size: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportStore: Map<string, StoredExport> = new Map()
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { tenantId, format, language = 'de', state, options = {} } = body
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!format) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'format is required (DOCX, PDF, JSON, ZIP)' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'state is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse dates in state
|
|
||||||
const parsedState: TOMGeneratorState = {
|
|
||||||
...state,
|
|
||||||
createdAt: new Date(state.createdAt),
|
|
||||||
updatedAt: new Date(state.updatedAt),
|
|
||||||
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
|
|
||||||
...step,
|
|
||||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
|
||||||
})) || [],
|
|
||||||
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
|
|
||||||
...doc,
|
|
||||||
uploadedAt: new Date(doc.uploadedAt),
|
|
||||||
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
|
||||||
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
|
||||||
aiAnalysis: doc.aiAnalysis ? {
|
|
||||||
...doc.aiAnalysis,
|
|
||||||
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
|
||||||
} : null,
|
|
||||||
})) || [],
|
|
||||||
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
|
|
||||||
...tom,
|
|
||||||
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
|
|
||||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
|
||||||
})) || [],
|
|
||||||
gapAnalysis: state.gapAnalysis ? {
|
|
||||||
...state.gapAnalysis,
|
|
||||||
generatedAt: new Date(state.gapAnalysis.generatedAt),
|
|
||||||
} : null,
|
|
||||||
exports: state.exports?.map((exp: { generatedAt: string }) => ({
|
|
||||||
...exp,
|
|
||||||
generatedAt: new Date(exp.generatedAt),
|
|
||||||
})) || [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportId = crypto.randomUUID()
|
|
||||||
let content: string
|
|
||||||
let filename: string
|
|
||||||
let mimeType: string
|
|
||||||
|
|
||||||
switch (format.toUpperCase()) {
|
|
||||||
case 'DOCX': {
|
|
||||||
// Generate DOCX structure (actual binary conversion would require docx library)
|
|
||||||
const docxContent = generateDOCXContent(parsedState, { language: language as 'de' | 'en', ...options })
|
|
||||||
content = Buffer.from(JSON.stringify(docxContent, null, 2)).toString('base64')
|
|
||||||
filename = generateDOCXFilename(parsedState, language as 'de' | 'en')
|
|
||||||
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'PDF': {
|
|
||||||
// Generate PDF structure (actual binary conversion would require pdf library)
|
|
||||||
const pdfContent = generatePDFContent(parsedState, { language: language as 'de' | 'en', ...options })
|
|
||||||
content = Buffer.from(JSON.stringify(pdfContent, null, 2)).toString('base64')
|
|
||||||
filename = generatePDFFilename(parsedState, language as 'de' | 'en')
|
|
||||||
mimeType = 'application/pdf'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'JSON':
|
|
||||||
content = Buffer.from(JSON.stringify(parsedState, null, 2)).toString('base64')
|
|
||||||
filename = `tom-export-${tenantId}-${new Date().toISOString().split('T')[0]}.json`
|
|
||||||
mimeType = 'application/json'
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'ZIP': {
|
|
||||||
const files = generateZIPFiles(parsedState, { language: language as 'de' | 'en', ...options })
|
|
||||||
// For now, return the files metadata (actual ZIP generation would require a library)
|
|
||||||
content = Buffer.from(JSON.stringify(files, null, 2)).toString('base64')
|
|
||||||
filename = generateZIPFilename(parsedState, language as 'de' | 'en')
|
|
||||||
mimeType = 'application/zip'
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `Unsupported format: ${format}` },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the export
|
|
||||||
const storedExport: StoredExport = {
|
|
||||||
id: exportId,
|
|
||||||
tenantId,
|
|
||||||
format: format.toUpperCase(),
|
|
||||||
filename,
|
|
||||||
content,
|
|
||||||
generatedAt: new Date(),
|
|
||||||
size: Buffer.from(content, 'base64').length,
|
|
||||||
}
|
|
||||||
exportStore.set(exportId, storedExport)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
exportId,
|
|
||||||
filename,
|
|
||||||
format: format.toUpperCase(),
|
|
||||||
mimeType,
|
|
||||||
size: storedExport.size,
|
|
||||||
generatedAt: storedExport.generatedAt.toISOString(),
|
|
||||||
downloadUrl: `/api/sdk/v1/tom-generator/export?exportId=${exportId}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to generate export:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to generate export' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const exportId = searchParams.get('exportId')
|
|
||||||
|
|
||||||
if (!exportId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'exportId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const storedExport = exportStore.get(exportId)
|
|
||||||
if (!storedExport) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: `Export not found: ${exportId}` },
|
|
||||||
{ status: 404 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the file as download
|
|
||||||
const buffer = Buffer.from(storedExport.content, 'base64')
|
|
||||||
|
|
||||||
let mimeType: string
|
|
||||||
switch (storedExport.format) {
|
|
||||||
case 'DOCX':
|
|
||||||
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
||||||
break
|
|
||||||
case 'PDF':
|
|
||||||
mimeType = 'application/pdf'
|
|
||||||
break
|
|
||||||
case 'JSON':
|
|
||||||
mimeType = 'application/json'
|
|
||||||
break
|
|
||||||
case 'ZIP':
|
|
||||||
mimeType = 'application/zip'
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
mimeType = 'application/octet-stream'
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NextResponse(buffer, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': mimeType,
|
|
||||||
'Content-Disposition': `attachment; filename="${storedExport.filename}"`,
|
|
||||||
'Content-Length': buffer.length.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to download export:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to download export' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ status: 'ok' },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Allow: 'GET, POST, OPTIONS',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { TOMRulesEngine } from '@/lib/sdk/tom-generator/rules-engine'
|
|
||||||
import { TOMGeneratorState, GapAnalysisResult } from '@/lib/sdk/tom-generator/types'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TOM Generator Gap Analysis API
|
|
||||||
*
|
|
||||||
* POST /api/sdk/v1/tom-generator/gap-analysis - Perform gap analysis
|
|
||||||
*
|
|
||||||
* Request body:
|
|
||||||
* {
|
|
||||||
* tenantId: string
|
|
||||||
* state: TOMGeneratorState
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Response:
|
|
||||||
* {
|
|
||||||
* gapAnalysis: GapAnalysisResult
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { tenantId, state } = body
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'state is required in request body' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse dates in state
|
|
||||||
const parsedState: TOMGeneratorState = {
|
|
||||||
...state,
|
|
||||||
createdAt: new Date(state.createdAt),
|
|
||||||
updatedAt: new Date(state.updatedAt),
|
|
||||||
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
|
|
||||||
...step,
|
|
||||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
|
||||||
})) || [],
|
|
||||||
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
|
|
||||||
...doc,
|
|
||||||
uploadedAt: new Date(doc.uploadedAt),
|
|
||||||
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
|
||||||
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
|
||||||
aiAnalysis: doc.aiAnalysis ? {
|
|
||||||
...doc.aiAnalysis,
|
|
||||||
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
|
||||||
} : null,
|
|
||||||
})) || [],
|
|
||||||
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
|
|
||||||
...tom,
|
|
||||||
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
|
|
||||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
|
||||||
})) || [],
|
|
||||||
gapAnalysis: state.gapAnalysis ? {
|
|
||||||
...state.gapAnalysis,
|
|
||||||
generatedAt: new Date(state.gapAnalysis.generatedAt),
|
|
||||||
} : null,
|
|
||||||
exports: state.exports?.map((exp: { generatedAt: string }) => ({
|
|
||||||
...exp,
|
|
||||||
generatedAt: new Date(exp.generatedAt),
|
|
||||||
})) || [],
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize rules engine
|
|
||||||
const engine = new TOMRulesEngine()
|
|
||||||
|
|
||||||
// Perform gap analysis using derived TOMs and documents from state
|
|
||||||
const gapAnalysis = engine.performGapAnalysis(
|
|
||||||
parsedState.derivedTOMs,
|
|
||||||
parsedState.documents
|
|
||||||
)
|
|
||||||
|
|
||||||
// Calculate detailed metrics
|
|
||||||
const metrics = calculateGapMetrics(gapAnalysis)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
gapAnalysis,
|
|
||||||
metrics,
|
|
||||||
generatedAt: gapAnalysis.generatedAt.toISOString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to perform gap analysis:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to perform gap analysis' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateGapMetrics(gapAnalysis: GapAnalysisResult) {
|
|
||||||
const totalGaps = gapAnalysis.missingControls.length +
|
|
||||||
gapAnalysis.partialControls.length +
|
|
||||||
gapAnalysis.missingEvidence.length
|
|
||||||
|
|
||||||
const criticalGaps = gapAnalysis.missingControls.filter(
|
|
||||||
(c) => c.priority === 'CRITICAL' || c.priority === 'HIGH'
|
|
||||||
).length
|
|
||||||
|
|
||||||
const mediumGaps = gapAnalysis.missingControls.filter(
|
|
||||||
(c) => c.priority === 'MEDIUM'
|
|
||||||
).length
|
|
||||||
|
|
||||||
const lowGaps = gapAnalysis.missingControls.filter(
|
|
||||||
(c) => c.priority === 'LOW'
|
|
||||||
).length
|
|
||||||
|
|
||||||
// Group missing controls by category
|
|
||||||
const gapsByCategory: Record<string, number> = {}
|
|
||||||
gapAnalysis.missingControls.forEach((control) => {
|
|
||||||
const category = control.controlId.split('-')[1] || 'OTHER'
|
|
||||||
gapsByCategory[category] = (gapsByCategory[category] || 0) + 1
|
|
||||||
})
|
|
||||||
|
|
||||||
// Calculate compliance readiness
|
|
||||||
const maxScore = 100
|
|
||||||
const deductionPerCritical = 10
|
|
||||||
const deductionPerMedium = 5
|
|
||||||
const deductionPerLow = 2
|
|
||||||
const deductionPerPartial = 3
|
|
||||||
const deductionPerMissingEvidence = 1
|
|
||||||
|
|
||||||
const deductions =
|
|
||||||
criticalGaps * deductionPerCritical +
|
|
||||||
mediumGaps * deductionPerMedium +
|
|
||||||
lowGaps * deductionPerLow +
|
|
||||||
gapAnalysis.partialControls.length * deductionPerPartial +
|
|
||||||
gapAnalysis.missingEvidence.length * deductionPerMissingEvidence
|
|
||||||
|
|
||||||
const complianceReadiness = Math.max(0, Math.min(100, maxScore - deductions))
|
|
||||||
|
|
||||||
// Prioritized action items
|
|
||||||
const prioritizedActions = [
|
|
||||||
...gapAnalysis.missingControls
|
|
||||||
.filter((c) => c.priority === 'CRITICAL')
|
|
||||||
.map((c) => ({
|
|
||||||
type: 'MISSING_CONTROL',
|
|
||||||
priority: 'CRITICAL',
|
|
||||||
controlId: c.controlId,
|
|
||||||
reason: c.reason,
|
|
||||||
action: `Implement control ${c.controlId}`,
|
|
||||||
})),
|
|
||||||
...gapAnalysis.missingControls
|
|
||||||
.filter((c) => c.priority === 'HIGH')
|
|
||||||
.map((c) => ({
|
|
||||||
type: 'MISSING_CONTROL',
|
|
||||||
priority: 'HIGH',
|
|
||||||
controlId: c.controlId,
|
|
||||||
reason: c.reason,
|
|
||||||
action: `Implement control ${c.controlId}`,
|
|
||||||
})),
|
|
||||||
...gapAnalysis.partialControls.map((c) => ({
|
|
||||||
type: 'PARTIAL_CONTROL',
|
|
||||||
priority: 'MEDIUM',
|
|
||||||
controlId: c.controlId,
|
|
||||||
missingAspects: c.missingAspects,
|
|
||||||
action: `Complete implementation of ${c.controlId}`,
|
|
||||||
})),
|
|
||||||
...gapAnalysis.missingEvidence.map((e) => ({
|
|
||||||
type: 'MISSING_EVIDENCE',
|
|
||||||
priority: 'LOW',
|
|
||||||
controlId: e.controlId,
|
|
||||||
requiredEvidence: e.requiredEvidence,
|
|
||||||
action: `Upload evidence for ${e.controlId}`,
|
|
||||||
})),
|
|
||||||
]
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalGaps,
|
|
||||||
criticalGaps,
|
|
||||||
mediumGaps,
|
|
||||||
lowGaps,
|
|
||||||
partialControls: gapAnalysis.partialControls.length,
|
|
||||||
missingEvidence: gapAnalysis.missingEvidence.length,
|
|
||||||
gapsByCategory,
|
|
||||||
complianceReadiness,
|
|
||||||
overallScore: gapAnalysis.overallScore,
|
|
||||||
prioritizedActionsCount: prioritizedActions.length,
|
|
||||||
prioritizedActions: prioritizedActions.slice(0, 10), // Top 10 actions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ status: 'ok' },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Allow: 'POST, OPTIONS',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import {
|
|
||||||
TOMGeneratorState,
|
|
||||||
createEmptyTOMGeneratorState,
|
|
||||||
} from '@/lib/sdk/tom-generator/types'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TOM Generator State API
|
|
||||||
*
|
|
||||||
* GET /api/sdk/v1/tom-generator/state?tenantId=xxx - Load TOM generator state
|
|
||||||
* POST /api/sdk/v1/tom-generator/state - Save TOM generator state
|
|
||||||
* DELETE /api/sdk/v1/tom-generator/state?tenantId=xxx - Clear state
|
|
||||||
*/
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// STORAGE (In-Memory for development)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface StoredTOMState {
|
|
||||||
state: TOMGeneratorState
|
|
||||||
version: number
|
|
||||||
createdAt: string
|
|
||||||
updatedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
class InMemoryTOMStateStore {
|
|
||||||
private store: Map<string, StoredTOMState> = new Map()
|
|
||||||
|
|
||||||
async get(tenantId: string): Promise<StoredTOMState | null> {
|
|
||||||
return this.store.get(tenantId) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(tenantId: string, state: TOMGeneratorState, expectedVersion?: number): Promise<StoredTOMState> {
|
|
||||||
const existing = this.store.get(tenantId)
|
|
||||||
|
|
||||||
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
|
|
||||||
const error = new Error('Version conflict') as Error & { status: number }
|
|
||||||
error.status = 409
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date().toISOString()
|
|
||||||
const newVersion = existing ? existing.version + 1 : 1
|
|
||||||
|
|
||||||
const stored: StoredTOMState = {
|
|
||||||
state: {
|
|
||||||
...state,
|
|
||||||
updatedAt: new Date(now),
|
|
||||||
},
|
|
||||||
version: newVersion,
|
|
||||||
createdAt: existing?.createdAt || now,
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.store.set(tenantId, stored)
|
|
||||||
return stored
|
|
||||||
}
|
|
||||||
|
|
||||||
async delete(tenantId: string): Promise<boolean> {
|
|
||||||
return this.store.delete(tenantId)
|
|
||||||
}
|
|
||||||
|
|
||||||
async list(): Promise<{ tenantId: string; updatedAt: string }[]> {
|
|
||||||
const result: { tenantId: string; updatedAt: string }[] = []
|
|
||||||
this.store.forEach((value, key) => {
|
|
||||||
result.push({ tenantId: key, updatedAt: value.updatedAt })
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateStore = new InMemoryTOMStateStore()
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HANDLERS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const tenantId = searchParams.get('tenantId')
|
|
||||||
|
|
||||||
// List all states if no tenantId provided
|
|
||||||
if (!tenantId) {
|
|
||||||
const states = await stateStore.list()
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: states,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const stored = await stateStore.get(tenantId)
|
|
||||||
|
|
||||||
if (!stored) {
|
|
||||||
// Return empty state for new tenants
|
|
||||||
const emptyState = createEmptyTOMGeneratorState(tenantId)
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
tenantId,
|
|
||||||
state: emptyState,
|
|
||||||
version: 0,
|
|
||||||
isNew: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
tenantId,
|
|
||||||
state: stored.state,
|
|
||||||
version: stored.version,
|
|
||||||
lastModified: stored.updatedAt,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load TOM generator state:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to load state' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const { tenantId, state, version } = body
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!state) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'state is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deserialize dates
|
|
||||||
const parsedState: TOMGeneratorState = {
|
|
||||||
...state,
|
|
||||||
createdAt: new Date(state.createdAt),
|
|
||||||
updatedAt: new Date(state.updatedAt),
|
|
||||||
steps: state.steps.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
|
|
||||||
...step,
|
|
||||||
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
|
|
||||||
})),
|
|
||||||
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
|
|
||||||
...doc,
|
|
||||||
uploadedAt: new Date(doc.uploadedAt),
|
|
||||||
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
|
|
||||||
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
|
|
||||||
aiAnalysis: doc.aiAnalysis ? {
|
|
||||||
...doc.aiAnalysis,
|
|
||||||
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
|
|
||||||
} : null,
|
|
||||||
})) || [],
|
|
||||||
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
|
|
||||||
...tom,
|
|
||||||
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
|
|
||||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
|
||||||
})) || [],
|
|
||||||
gapAnalysis: state.gapAnalysis ? {
|
|
||||||
...state.gapAnalysis,
|
|
||||||
generatedAt: new Date(state.gapAnalysis.generatedAt),
|
|
||||||
} : null,
|
|
||||||
exports: state.exports?.map((exp: { generatedAt: string }) => ({
|
|
||||||
...exp,
|
|
||||||
generatedAt: new Date(exp.generatedAt),
|
|
||||||
})) || [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const stored = await stateStore.save(tenantId, parsedState, version)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
tenantId,
|
|
||||||
state: stored.state,
|
|
||||||
version: stored.version,
|
|
||||||
lastModified: stored.updatedAt,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
const err = error as Error & { status?: number }
|
|
||||||
|
|
||||||
if (err.status === 409 || err.message === 'Version conflict') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: false,
|
|
||||||
error: 'Version conflict. State was modified by another request.',
|
|
||||||
code: 'VERSION_CONFLICT',
|
|
||||||
},
|
|
||||||
{ status: 409 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('Failed to save TOM generator state:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to save state' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { searchParams } = new URL(request.url)
|
|
||||||
const tenantId = searchParams.get('tenantId')
|
|
||||||
|
|
||||||
if (!tenantId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'tenantId is required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleted = await stateStore.delete(tenantId)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
tenantId,
|
|
||||||
deleted,
|
|
||||||
deletedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete TOM generator state:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to delete state' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function OPTIONS() {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ status: 'ok' },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Allow: 'GET, POST, DELETE, OPTIONS',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
|
|
||||||
// Forward the request to the SDK backend
|
|
||||||
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/assess`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Forward tenant ID if present
|
|
||||||
...(request.headers.get('X-Tenant-ID') && {
|
|
||||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
console.error('SDK backend error:', errorText)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'SDK backend error', details: errorText },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to call SDK backend:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to connect to SDK backend' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
|
|
||||||
// Forward the request to the SDK backend
|
|
||||||
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/export/direct`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
// Forward tenant ID if present
|
|
||||||
...(request.headers.get('X-Tenant-ID') && {
|
|
||||||
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
console.error('SDK backend error:', errorText)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'SDK backend error', details: errorText },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to call SDK backend:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to connect to SDK backend' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import {
|
|
||||||
Finding,
|
|
||||||
CONTRACT_REVIEW_SYSTEM_PROMPT,
|
|
||||||
} from '@/lib/sdk/vendor-compliance'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sdk/v1/vendor-compliance/contracts/[id]/review
|
|
||||||
*
|
|
||||||
* Starts the LLM-based contract review process
|
|
||||||
*/
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { id: contractId } = await params
|
|
||||||
|
|
||||||
// In production:
|
|
||||||
// 1. Fetch contract from database
|
|
||||||
// 2. Extract text from PDF/DOCX using embedding-service
|
|
||||||
// 3. Send to LLM for analysis
|
|
||||||
// 4. Store findings in database
|
|
||||||
// 5. Update contract with compliance score
|
|
||||||
|
|
||||||
// For demo, return mock analysis results
|
|
||||||
const mockFindings: Finding[] = [
|
|
||||||
{
|
|
||||||
id: uuidv4(),
|
|
||||||
tenantId: 'default',
|
|
||||||
contractId,
|
|
||||||
vendorId: 'mock-vendor',
|
|
||||||
type: 'OK',
|
|
||||||
category: 'AVV_CONTENT',
|
|
||||||
severity: 'LOW',
|
|
||||||
title: {
|
|
||||||
de: 'Weisungsgebundenheit vorhanden',
|
|
||||||
en: 'Instruction binding present',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
de: 'Der Vertrag enthält eine angemessene Regelung zur Weisungsgebundenheit des Auftragsverarbeiters.',
|
|
||||||
en: 'The contract contains an appropriate provision for processor instruction binding.',
|
|
||||||
},
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
documentId: contractId,
|
|
||||||
page: 2,
|
|
||||||
startChar: 150,
|
|
||||||
endChar: 350,
|
|
||||||
quotedText: 'Der Auftragnehmer verarbeitet personenbezogene Daten ausschließlich auf dokumentierte Weisung des Auftraggebers.',
|
|
||||||
quoteHash: 'abc123',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
affectedRequirement: 'Art. 28 Abs. 3 lit. a DSGVO',
|
|
||||||
triggeredControls: ['VND-CON-01'],
|
|
||||||
status: 'OPEN',
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuidv4(),
|
|
||||||
tenantId: 'default',
|
|
||||||
contractId,
|
|
||||||
vendorId: 'mock-vendor',
|
|
||||||
type: 'GAP',
|
|
||||||
category: 'INCIDENT',
|
|
||||||
severity: 'HIGH',
|
|
||||||
title: {
|
|
||||||
de: 'Meldefrist für Datenpannen zu lang',
|
|
||||||
en: 'Data breach notification deadline too long',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
de: 'Die vereinbarte Meldefrist von 72 Stunden ist zu lang, um die eigene Meldepflicht gegenüber der Aufsichtsbehörde fristgerecht erfüllen zu können.',
|
|
||||||
en: 'The agreed notification deadline of 72 hours is too long to meet own notification obligations to the supervisory authority in time.',
|
|
||||||
},
|
|
||||||
recommendation: {
|
|
||||||
de: 'Verhandeln Sie eine kürzere Meldefrist von maximal 24-48 Stunden.',
|
|
||||||
en: 'Negotiate a shorter notification deadline of maximum 24-48 hours.',
|
|
||||||
},
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
documentId: contractId,
|
|
||||||
page: 5,
|
|
||||||
startChar: 820,
|
|
||||||
endChar: 950,
|
|
||||||
quotedText: 'Der Auftragnehmer wird den Auftraggeber innerhalb von 72 Stunden über eine Verletzung des Schutzes personenbezogener Daten informieren.',
|
|
||||||
quoteHash: 'def456',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
affectedRequirement: 'Art. 33 Abs. 2 DSGVO',
|
|
||||||
triggeredControls: ['VND-INC-01'],
|
|
||||||
status: 'OPEN',
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: uuidv4(),
|
|
||||||
tenantId: 'default',
|
|
||||||
contractId,
|
|
||||||
vendorId: 'mock-vendor',
|
|
||||||
type: 'RISK',
|
|
||||||
category: 'TRANSFER',
|
|
||||||
severity: 'MEDIUM',
|
|
||||||
title: {
|
|
||||||
de: 'Drittlandtransfer USA ohne TIA',
|
|
||||||
en: 'Third country transfer to USA without TIA',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
de: 'Der Vertrag erlaubt Datenverarbeitung in den USA. Es liegt jedoch kein Transfer Impact Assessment (TIA) vor.',
|
|
||||||
en: 'The contract allows data processing in the USA. However, no Transfer Impact Assessment (TIA) is available.',
|
|
||||||
},
|
|
||||||
recommendation: {
|
|
||||||
de: 'Führen Sie ein TIA durch und dokumentieren Sie zusätzliche Schutzmaßnahmen.',
|
|
||||||
en: 'Conduct a TIA and document supplementary measures.',
|
|
||||||
},
|
|
||||||
citations: [
|
|
||||||
{
|
|
||||||
documentId: contractId,
|
|
||||||
page: 8,
|
|
||||||
startChar: 1200,
|
|
||||||
endChar: 1350,
|
|
||||||
quotedText: 'Die Verarbeitung kann auch in Rechenzentren in den Vereinigten Staaten von Amerika erfolgen.',
|
|
||||||
quoteHash: 'ghi789',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
affectedRequirement: 'Art. 44-49 DSGVO, Schrems II',
|
|
||||||
triggeredControls: ['VND-TRF-01', 'VND-TRF-03'],
|
|
||||||
status: 'OPEN',
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Calculate compliance score based on findings
|
|
||||||
const okFindings = mockFindings.filter((f) => f.type === 'OK').length
|
|
||||||
const totalChecks = mockFindings.length + 5 // Assume 5 additional checks passed
|
|
||||||
const complianceScore = Math.round((okFindings / totalChecks) * 100 + 60) // Base score + passed checks
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
contractId,
|
|
||||||
findings: mockFindings,
|
|
||||||
complianceScore: Math.min(100, complianceScore),
|
|
||||||
reviewCompletedAt: new Date().toISOString(),
|
|
||||||
topRisks: [
|
|
||||||
{ de: 'Meldefrist für Datenpannen zu lang', en: 'Data breach notification deadline too long' },
|
|
||||||
{ de: 'Fehlende TIA für USA-Transfer', en: 'Missing TIA for USA transfer' },
|
|
||||||
],
|
|
||||||
requiredActions: [
|
|
||||||
{ de: 'Meldefrist auf 24-48h verkürzen', en: 'Reduce notification deadline to 24-48h' },
|
|
||||||
{ de: 'TIA für USA-Transfer durchführen', en: 'Conduct TIA for USA transfer' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error reviewing contract:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to review contract' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/vendor-compliance/contracts/[id]/review
|
|
||||||
*
|
|
||||||
* Get existing review results
|
|
||||||
*/
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { id: contractId } = await params
|
|
||||||
|
|
||||||
// In production, fetch from database
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: {
|
|
||||||
contractId,
|
|
||||||
findings: [],
|
|
||||||
complianceScore: null,
|
|
||||||
reviewStatus: 'PENDING',
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching review:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch review' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { ContractDocument } from '@/lib/sdk/vendor-compliance'
|
|
||||||
|
|
||||||
// In-memory storage for demo purposes
|
|
||||||
const contracts: Map<string, ContractDocument> = new Map()
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const contractList = Array.from(contracts.values())
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: contractList,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching contracts:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch contracts' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
// Handle multipart form data for file upload
|
|
||||||
const formData = await request.formData()
|
|
||||||
const file = formData.get('file') as File | null
|
|
||||||
const vendorId = formData.get('vendorId') as string
|
|
||||||
const metadataStr = formData.get('metadata') as string
|
|
||||||
|
|
||||||
if (!file || !vendorId) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'File and vendorId are required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = metadataStr ? JSON.parse(metadataStr) : {}
|
|
||||||
const id = uuidv4()
|
|
||||||
|
|
||||||
// In production, upload file to storage (MinIO, S3, etc.)
|
|
||||||
const storagePath = `contracts/${id}/${file.name}`
|
|
||||||
|
|
||||||
const contract: ContractDocument = {
|
|
||||||
id,
|
|
||||||
tenantId: 'default',
|
|
||||||
vendorId,
|
|
||||||
fileName: `${id}-${file.name}`,
|
|
||||||
originalName: file.name,
|
|
||||||
mimeType: file.type,
|
|
||||||
fileSize: file.size,
|
|
||||||
storagePath,
|
|
||||||
documentType: metadata.documentType || 'OTHER',
|
|
||||||
version: metadata.version || '1.0',
|
|
||||||
previousVersionId: metadata.previousVersionId,
|
|
||||||
parties: metadata.parties,
|
|
||||||
effectiveDate: metadata.effectiveDate ? new Date(metadata.effectiveDate) : undefined,
|
|
||||||
expirationDate: metadata.expirationDate ? new Date(metadata.expirationDate) : undefined,
|
|
||||||
autoRenewal: metadata.autoRenewal,
|
|
||||||
renewalNoticePeriod: metadata.renewalNoticePeriod,
|
|
||||||
terminationNoticePeriod: metadata.terminationNoticePeriod,
|
|
||||||
reviewStatus: 'PENDING',
|
|
||||||
status: 'DRAFT',
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}
|
|
||||||
|
|
||||||
contracts.set(id, contract)
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: contract,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
{ status: 201 }
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading contract:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to upload contract' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { CONTROLS_LIBRARY } from '@/lib/sdk/vendor-compliance'
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const searchParams = request.nextUrl.searchParams
|
|
||||||
const domain = searchParams.get('domain')
|
|
||||||
|
|
||||||
let controls = [...CONTROLS_LIBRARY]
|
|
||||||
|
|
||||||
// Filter by domain if provided
|
|
||||||
if (domain) {
|
|
||||||
controls = controls.filter((c) => c.domain === domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: controls,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching controls:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch controls' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/vendor-compliance/export/[reportId]/download
|
|
||||||
*
|
|
||||||
* Download a generated report file.
|
|
||||||
* In production, this would redirect to a signed MinIO/S3 URL or stream the file.
|
|
||||||
*/
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ reportId: string }> }
|
|
||||||
) {
|
|
||||||
const { reportId } = await params
|
|
||||||
|
|
||||||
// TODO: Implement actual file download
|
|
||||||
// This would typically:
|
|
||||||
// 1. Verify report exists and user has access
|
|
||||||
// 2. Generate signed URL for MinIO/S3
|
|
||||||
// 3. Redirect to signed URL or stream file
|
|
||||||
|
|
||||||
// For now, return a placeholder PDF
|
|
||||||
const placeholderContent = `
|
|
||||||
%PDF-1.4
|
|
||||||
1 0 obj
|
|
||||||
<< /Type /Catalog /Pages 2 0 R >>
|
|
||||||
endobj
|
|
||||||
2 0 obj
|
|
||||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
|
||||||
endobj
|
|
||||||
3 0 obj
|
|
||||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>
|
|
||||||
endobj
|
|
||||||
4 0 obj
|
|
||||||
<< /Length 200 >>
|
|
||||||
stream
|
|
||||||
BT
|
|
||||||
/F1 24 Tf
|
|
||||||
100 700 Td
|
|
||||||
(Vendor Compliance Report) Tj
|
|
||||||
/F1 12 Tf
|
|
||||||
100 650 Td
|
|
||||||
(Report ID: ${reportId}) Tj
|
|
||||||
100 620 Td
|
|
||||||
(Generated: ${new Date().toISOString()}) Tj
|
|
||||||
100 580 Td
|
|
||||||
(This is a placeholder. Implement actual report generation.) Tj
|
|
||||||
ET
|
|
||||||
endstream
|
|
||||||
endobj
|
|
||||||
5 0 obj
|
|
||||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
|
||||||
endobj
|
|
||||||
xref
|
|
||||||
0 6
|
|
||||||
0000000000 65535 f
|
|
||||||
0000000009 00000 n
|
|
||||||
0000000058 00000 n
|
|
||||||
0000000115 00000 n
|
|
||||||
0000000266 00000 n
|
|
||||||
0000000519 00000 n
|
|
||||||
trailer
|
|
||||||
<< /Size 6 /Root 1 0 R >>
|
|
||||||
startxref
|
|
||||||
598
|
|
||||||
%%EOF
|
|
||||||
`.trim()
|
|
||||||
|
|
||||||
// Return as PDF
|
|
||||||
return new NextResponse(placeholderContent, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/pdf',
|
|
||||||
'Content-Disposition': `attachment; filename="Report_${reportId.slice(0, 8)}.pdf"`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/vendor-compliance/export/[reportId]
|
|
||||||
*
|
|
||||||
* Get report metadata by ID.
|
|
||||||
*/
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ reportId: string }> }
|
|
||||||
) {
|
|
||||||
const { reportId } = await params
|
|
||||||
|
|
||||||
// TODO: Fetch report metadata from database
|
|
||||||
// For now, return mock data
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
id: reportId,
|
|
||||||
status: 'completed',
|
|
||||||
filename: `Report_${reportId.slice(0, 8)}.pdf`,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24h
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /api/sdk/v1/vendor-compliance/export/[reportId]
|
|
||||||
*
|
|
||||||
* Delete a generated report.
|
|
||||||
*/
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ reportId: string }> }
|
|
||||||
) {
|
|
||||||
const { reportId } = await params
|
|
||||||
|
|
||||||
// TODO: Delete report from storage and database
|
|
||||||
console.log('Deleting report:', reportId)
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
deletedId: reportId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /api/sdk/v1/vendor-compliance/export
|
|
||||||
*
|
|
||||||
* Generate and export reports in various formats.
|
|
||||||
* Currently returns mock data - integrate with actual report generation service.
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface ExportConfig {
|
|
||||||
reportType: 'VVT_EXPORT' | 'VENDOR_AUDIT' | 'ROPA' | 'MANAGEMENT_SUMMARY' | 'DPIA_INPUT'
|
|
||||||
format: 'PDF' | 'DOCX' | 'XLSX' | 'JSON'
|
|
||||||
scope: {
|
|
||||||
vendorIds: string[]
|
|
||||||
processingActivityIds: string[]
|
|
||||||
includeFindings: boolean
|
|
||||||
includeControls: boolean
|
|
||||||
includeRiskAssessment: boolean
|
|
||||||
dateRange?: {
|
|
||||||
from: string
|
|
||||||
to: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const REPORT_TYPE_NAMES: Record<ExportConfig['reportType'], string> = {
|
|
||||||
VVT_EXPORT: 'Verarbeitungsverzeichnis',
|
|
||||||
VENDOR_AUDIT: 'Vendor-Audit-Pack',
|
|
||||||
ROPA: 'RoPA',
|
|
||||||
MANAGEMENT_SUMMARY: 'Management-Summary',
|
|
||||||
DPIA_INPUT: 'DSFA-Input',
|
|
||||||
}
|
|
||||||
|
|
||||||
const FORMAT_EXTENSIONS: Record<ExportConfig['format'], string> = {
|
|
||||||
PDF: 'pdf',
|
|
||||||
DOCX: 'docx',
|
|
||||||
XLSX: 'xlsx',
|
|
||||||
JSON: 'json',
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const config = (await request.json()) as ExportConfig
|
|
||||||
|
|
||||||
// Validate request
|
|
||||||
if (!config.reportType || !config.format) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'reportType and format are required' },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate report ID and filename
|
|
||||||
const reportId = uuidv4()
|
|
||||||
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '')
|
|
||||||
const filename = `${REPORT_TYPE_NAMES[config.reportType]}_${timestamp}.${FORMAT_EXTENSIONS[config.format]}`
|
|
||||||
|
|
||||||
// TODO: Implement actual report generation
|
|
||||||
// This would typically:
|
|
||||||
// 1. Fetch data from database based on scope
|
|
||||||
// 2. Generate report using template engine (e.g., docx-templates, pdfkit)
|
|
||||||
// 3. Store in MinIO/S3
|
|
||||||
// 4. Return download URL
|
|
||||||
|
|
||||||
// Mock implementation - simulate processing time
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
// In production, this would be a signed URL to MinIO/S3
|
|
||||||
const downloadUrl = `/api/sdk/v1/vendor-compliance/export/${reportId}/download`
|
|
||||||
|
|
||||||
// Log export for audit trail
|
|
||||||
console.log('Export generated:', {
|
|
||||||
reportId,
|
|
||||||
reportType: config.reportType,
|
|
||||||
format: config.format,
|
|
||||||
scope: config.scope,
|
|
||||||
filename,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
id: reportId,
|
|
||||||
reportType: config.reportType,
|
|
||||||
format: config.format,
|
|
||||||
filename,
|
|
||||||
downloadUrl,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
scope: {
|
|
||||||
vendorCount: config.scope.vendorIds?.length || 0,
|
|
||||||
activityCount: config.scope.processingActivityIds?.length || 0,
|
|
||||||
includesFindings: config.scope.includeFindings,
|
|
||||||
includesControls: config.scope.includeControls,
|
|
||||||
includesRiskAssessment: config.scope.includeRiskAssessment,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Export error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Failed to generate export' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/sdk/v1/vendor-compliance/export
|
|
||||||
*
|
|
||||||
* List recent exports for the current tenant.
|
|
||||||
*/
|
|
||||||
export async function GET() {
|
|
||||||
// TODO: Implement fetching recent exports from database
|
|
||||||
// For now, return empty list
|
|
||||||
return NextResponse.json({
|
|
||||||
exports: [],
|
|
||||||
totalCount: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { Finding } from '@/lib/sdk/vendor-compliance'
|
|
||||||
|
|
||||||
// In-memory storage for demo purposes
|
|
||||||
const findings: Map<string, Finding> = new Map()
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const searchParams = request.nextUrl.searchParams
|
|
||||||
const vendorId = searchParams.get('vendorId')
|
|
||||||
const contractId = searchParams.get('contractId')
|
|
||||||
const status = searchParams.get('status')
|
|
||||||
|
|
||||||
let findingsList = Array.from(findings.values())
|
|
||||||
|
|
||||||
// Filter by vendor
|
|
||||||
if (vendorId) {
|
|
||||||
findingsList = findingsList.filter((f) => f.vendorId === vendorId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by contract
|
|
||||||
if (contractId) {
|
|
||||||
findingsList = findingsList.filter((f) => f.contractId === contractId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
if (status) {
|
|
||||||
findingsList = findingsList.filter((f) => f.status === status)
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: findingsList,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching findings:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch findings' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
// This would reference the same storage as the main route
|
|
||||||
// In production, this would be database calls
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { id } = await params
|
|
||||||
|
|
||||||
// In production, fetch from database
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: null, // Would return the activity
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching processing activity:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch processing activity' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { id } = await params
|
|
||||||
const body = await request.json()
|
|
||||||
|
|
||||||
// In production, update in database
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: { id, ...body, updatedAt: new Date() },
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating processing activity:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to update processing activity' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const { id } = await params
|
|
||||||
|
|
||||||
// In production, delete from database
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting processing activity:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to delete processing activity' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { ProcessingActivity, generateVVTId } from '@/lib/sdk/vendor-compliance'
|
|
||||||
|
|
||||||
// In-memory storage for demo purposes
|
|
||||||
// In production, this would be replaced with database calls
|
|
||||||
const processingActivities: Map<string, ProcessingActivity> = new Map()
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const activities = Array.from(processingActivities.values())
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: activities,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching processing activities:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch processing activities' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
|
|
||||||
// Generate IDs
|
|
||||||
const id = uuidv4()
|
|
||||||
const existingIds = Array.from(processingActivities.values()).map((a) => a.vvtId)
|
|
||||||
const vvtId = body.vvtId || generateVVTId(existingIds)
|
|
||||||
|
|
||||||
const activity: ProcessingActivity = {
|
|
||||||
id,
|
|
||||||
tenantId: 'default', // Would come from auth context
|
|
||||||
vvtId,
|
|
||||||
name: body.name,
|
|
||||||
responsible: body.responsible,
|
|
||||||
dpoContact: body.dpoContact,
|
|
||||||
purposes: body.purposes || [],
|
|
||||||
dataSubjectCategories: body.dataSubjectCategories || [],
|
|
||||||
personalDataCategories: body.personalDataCategories || [],
|
|
||||||
recipientCategories: body.recipientCategories || [],
|
|
||||||
thirdCountryTransfers: body.thirdCountryTransfers || [],
|
|
||||||
retentionPeriod: body.retentionPeriod || { description: { de: '', en: '' } },
|
|
||||||
technicalMeasures: body.technicalMeasures || [],
|
|
||||||
legalBasis: body.legalBasis || [],
|
|
||||||
dataSources: body.dataSources || [],
|
|
||||||
systems: body.systems || [],
|
|
||||||
dataFlows: body.dataFlows || [],
|
|
||||||
protectionLevel: body.protectionLevel || 'MEDIUM',
|
|
||||||
dpiaRequired: body.dpiaRequired || false,
|
|
||||||
dpiaJustification: body.dpiaJustification,
|
|
||||||
subProcessors: body.subProcessors || [],
|
|
||||||
legalRetentionBasis: body.legalRetentionBasis,
|
|
||||||
status: body.status || 'DRAFT',
|
|
||||||
owner: body.owner || '',
|
|
||||||
lastReviewDate: body.lastReviewDate,
|
|
||||||
nextReviewDate: body.nextReviewDate,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}
|
|
||||||
|
|
||||||
processingActivities.set(id, activity)
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: activity,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
{ status: 201 }
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating processing activity:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to create processing activity' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
|
||||||
import { Vendor } from '@/lib/sdk/vendor-compliance'
|
|
||||||
|
|
||||||
// In-memory storage for demo purposes
|
|
||||||
const vendors: Map<string, Vendor> = new Map()
|
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const vendorList = Array.from(vendors.values())
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
success: true,
|
|
||||||
data: vendorList,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching vendors:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to fetch vendors' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const body = await request.json()
|
|
||||||
const id = uuidv4()
|
|
||||||
|
|
||||||
const vendor: Vendor = {
|
|
||||||
id,
|
|
||||||
tenantId: 'default',
|
|
||||||
name: body.name,
|
|
||||||
legalForm: body.legalForm,
|
|
||||||
country: body.country,
|
|
||||||
address: body.address,
|
|
||||||
website: body.website,
|
|
||||||
role: body.role,
|
|
||||||
serviceDescription: body.serviceDescription,
|
|
||||||
serviceCategory: body.serviceCategory,
|
|
||||||
dataAccessLevel: body.dataAccessLevel || 'NONE',
|
|
||||||
processingLocations: body.processingLocations || [],
|
|
||||||
transferMechanisms: body.transferMechanisms || [],
|
|
||||||
certifications: body.certifications || [],
|
|
||||||
primaryContact: body.primaryContact,
|
|
||||||
dpoContact: body.dpoContact,
|
|
||||||
securityContact: body.securityContact,
|
|
||||||
contractTypes: body.contractTypes || [],
|
|
||||||
contracts: body.contracts || [],
|
|
||||||
inherentRiskScore: body.inherentRiskScore || 50,
|
|
||||||
residualRiskScore: body.residualRiskScore || 50,
|
|
||||||
manualRiskAdjustment: body.manualRiskAdjustment,
|
|
||||||
riskJustification: body.riskJustification,
|
|
||||||
reviewFrequency: body.reviewFrequency || 'ANNUAL',
|
|
||||||
lastReviewDate: body.lastReviewDate,
|
|
||||||
nextReviewDate: body.nextReviewDate,
|
|
||||||
status: body.status || 'ACTIVE',
|
|
||||||
processingActivityIds: body.processingActivityIds || [],
|
|
||||||
notes: body.notes,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
}
|
|
||||||
|
|
||||||
vendors.set(id, vendor)
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
data: vendor,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
{ status: 201 }
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating vendor:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ success: false, error: 'Failed to create vendor' },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
/**
|
|
||||||
* Vendor Compliance API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/vendors/* requests to ai-compliance-sdk backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/vendors`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forward all relevant headers
|
|
||||||
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
|
|
||||||
for (const name of headerNames) {
|
|
||||||
const value = request.headers.get(name)
|
|
||||||
if (value) {
|
|
||||||
headers[name] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(30000),
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
||||||
const contentType = request.headers.get('content-type')
|
|
||||||
if (contentType?.includes('application/json')) {
|
|
||||||
try {
|
|
||||||
const text = await request.text()
|
|
||||||
if (text && text.trim()) {
|
|
||||||
fetchOptions.body = text
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Empty or invalid body - continue without
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
// Handle non-JSON responses (e.g., PDF exports)
|
|
||||||
const responseContentType = response.headers.get('content-type')
|
|
||||||
if (responseContentType?.includes('application/pdf') ||
|
|
||||||
responseContentType?.includes('application/octet-stream')) {
|
|
||||||
const blob = await response.blob()
|
|
||||||
return new NextResponse(blob, {
|
|
||||||
status: response.status,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': responseContentType,
|
|
||||||
'Content-Disposition': response.headers.get('content-disposition') || '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Vendor Compliance API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PATCH(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PATCH')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
/**
|
|
||||||
* Whistleblower API Proxy - Catch-all route
|
|
||||||
* Proxies all /api/sdk/v1/whistleblower/* requests to ai-compliance-sdk backend
|
|
||||||
* Supports multipart/form-data for file uploads
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server'
|
|
||||||
|
|
||||||
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
|
|
||||||
|
|
||||||
async function proxyRequest(
|
|
||||||
request: NextRequest,
|
|
||||||
pathSegments: string[] | undefined,
|
|
||||||
method: string
|
|
||||||
) {
|
|
||||||
const pathStr = pathSegments?.join('/') || ''
|
|
||||||
const searchParams = request.nextUrl.searchParams.toString()
|
|
||||||
const basePath = `${SDK_BACKEND_URL}/sdk/v1/whistleblower`
|
|
||||||
const url = pathStr
|
|
||||||
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: HeadersInit = {}
|
|
||||||
const contentType = request.headers.get('content-type')
|
|
||||||
|
|
||||||
// Forward auth headers
|
|
||||||
const authHeader = request.headers.get('authorization')
|
|
||||||
if (authHeader) {
|
|
||||||
headers['Authorization'] = authHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenantHeader = request.headers.get('x-tenant-id')
|
|
||||||
if (tenantHeader) {
|
|
||||||
headers['X-Tenant-Id'] = tenantHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: AbortSignal.timeout(60000), // 60s for file uploads
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
||||||
if (contentType?.includes('multipart/form-data')) {
|
|
||||||
// Forward multipart form data (file uploads)
|
|
||||||
const formData = await request.formData()
|
|
||||||
fetchOptions.body = formData
|
|
||||||
// Don't set Content-Type - let fetch set it with boundary
|
|
||||||
} else if (contentType?.includes('application/json')) {
|
|
||||||
headers['Content-Type'] = 'application/json'
|
|
||||||
try {
|
|
||||||
const text = await request.text()
|
|
||||||
if (text && text.trim()) {
|
|
||||||
fetchOptions.body = text
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Empty or invalid body
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
headers['Content-Type'] = 'application/json'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
headers['Content-Type'] = 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions)
|
|
||||||
|
|
||||||
// Handle non-JSON responses (e.g., PDF exports, file downloads)
|
|
||||||
const responseContentType = response.headers.get('content-type')
|
|
||||||
if (responseContentType?.includes('application/pdf') ||
|
|
||||||
responseContentType?.includes('application/octet-stream') ||
|
|
||||||
responseContentType?.includes('image/')) {
|
|
||||||
const blob = await response.blob()
|
|
||||||
return new NextResponse(blob, {
|
|
||||||
status: response.status,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': responseContentType,
|
|
||||||
'Content-Disposition': response.headers.get('content-disposition') || '',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text()
|
|
||||||
let errorJson
|
|
||||||
try {
|
|
||||||
errorJson = JSON.parse(errorText)
|
|
||||||
} catch {
|
|
||||||
errorJson = { error: errorText }
|
|
||||||
}
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `Backend Error: ${response.status}`, ...errorJson },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return NextResponse.json(data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Whistleblower API proxy error:', error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
|
|
||||||
{ status: 503 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'GET')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'POST')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PUT')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PATCH(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'PATCH')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function DELETE(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ path?: string[] }> }
|
|
||||||
) {
|
|
||||||
const { path } = await params
|
|
||||||
return proxyRequest(request, path, 'DELETE')
|
|
||||||
}
|
|
||||||
@@ -1,324 +0,0 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
||||||
import { exportToPDF, exportToZIP, downloadExport } from '../export'
|
|
||||||
import type { SDKState } from '../types'
|
|
||||||
|
|
||||||
// Mock jsPDF as a class
|
|
||||||
vi.mock('jspdf', () => {
|
|
||||||
return {
|
|
||||||
default: class MockJsPDF {
|
|
||||||
internal = {
|
|
||||||
pageSize: { getWidth: () => 210, getHeight: () => 297 },
|
|
||||||
}
|
|
||||||
setFillColor = vi.fn().mockReturnThis()
|
|
||||||
setDrawColor = vi.fn().mockReturnThis()
|
|
||||||
setTextColor = vi.fn().mockReturnThis()
|
|
||||||
setFontSize = vi.fn().mockReturnThis()
|
|
||||||
setFont = vi.fn().mockReturnThis()
|
|
||||||
setLineWidth = vi.fn().mockReturnThis()
|
|
||||||
text = vi.fn().mockReturnThis()
|
|
||||||
line = vi.fn().mockReturnThis()
|
|
||||||
rect = vi.fn().mockReturnThis()
|
|
||||||
roundedRect = vi.fn().mockReturnThis()
|
|
||||||
circle = vi.fn().mockReturnThis()
|
|
||||||
addPage = vi.fn().mockReturnThis()
|
|
||||||
setPage = vi.fn().mockReturnThis()
|
|
||||||
getNumberOfPages = vi.fn(() => 5)
|
|
||||||
splitTextToSize = vi.fn((text: string) => [text])
|
|
||||||
output = vi.fn(() => new Blob(['mock-pdf'], { type: 'application/pdf' }))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Mock JSZip as a class
|
|
||||||
vi.mock('jszip', () => {
|
|
||||||
return {
|
|
||||||
default: class MockJSZip {
|
|
||||||
private mockFolder = {
|
|
||||||
file: vi.fn().mockReturnThis(),
|
|
||||||
folder: vi.fn(() => this.mockFolder),
|
|
||||||
}
|
|
||||||
folder = vi.fn(() => this.mockFolder)
|
|
||||||
generateAsync = vi.fn(() => Promise.resolve(new Blob(['mock-zip'], { type: 'application/zip' })))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const createMockState = (overrides: Partial<SDKState> = {}): SDKState => ({
|
|
||||||
version: '1.0.0',
|
|
||||||
lastModified: new Date('2024-01-15'),
|
|
||||||
tenantId: 'test-tenant',
|
|
||||||
userId: 'test-user',
|
|
||||||
subscription: 'PROFESSIONAL',
|
|
||||||
currentPhase: 1,
|
|
||||||
currentStep: 'use-case-workshop',
|
|
||||||
completedSteps: ['use-case-workshop', 'screening'],
|
|
||||||
checkpoints: {
|
|
||||||
'CP-UC': {
|
|
||||||
checkpointId: 'CP-UC',
|
|
||||||
passed: true,
|
|
||||||
validatedAt: new Date(),
|
|
||||||
validatedBy: 'SYSTEM',
|
|
||||||
errors: [],
|
|
||||||
warnings: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
useCases: [
|
|
||||||
{
|
|
||||||
id: 'uc-1',
|
|
||||||
name: 'Test Use Case',
|
|
||||||
description: 'A test use case for testing',
|
|
||||||
category: 'Marketing',
|
|
||||||
stepsCompleted: 3,
|
|
||||||
steps: [
|
|
||||||
{ id: 's1', name: 'Step 1', completed: true, data: {} },
|
|
||||||
{ id: 's2', name: 'Step 2', completed: true, data: {} },
|
|
||||||
{ id: 's3', name: 'Step 3', completed: true, data: {} },
|
|
||||||
],
|
|
||||||
assessmentResult: null,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
activeUseCase: 'uc-1',
|
|
||||||
screening: null,
|
|
||||||
modules: [],
|
|
||||||
requirements: [],
|
|
||||||
controls: [
|
|
||||||
{
|
|
||||||
id: 'ctrl-1',
|
|
||||||
name: 'Test Control',
|
|
||||||
description: 'A test control',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Access Control',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: [],
|
|
||||||
owner: 'Test Owner',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
evidence: [],
|
|
||||||
checklist: [],
|
|
||||||
risks: [
|
|
||||||
{
|
|
||||||
id: 'risk-1',
|
|
||||||
title: 'Test Risk',
|
|
||||||
description: 'A test risk',
|
|
||||||
category: 'Security',
|
|
||||||
likelihood: 3,
|
|
||||||
impact: 4,
|
|
||||||
severity: 'HIGH',
|
|
||||||
inherentRiskScore: 12,
|
|
||||||
residualRiskScore: 6,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'mit-1',
|
|
||||||
description: 'Test mitigation',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 80,
|
|
||||||
controlId: 'ctrl-1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'Risk Owner',
|
|
||||||
relatedControls: ['ctrl-1'],
|
|
||||||
relatedRequirements: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
aiActClassification: null,
|
|
||||||
obligations: [],
|
|
||||||
dsfa: null,
|
|
||||||
toms: [],
|
|
||||||
retentionPolicies: [],
|
|
||||||
vvt: [],
|
|
||||||
documents: [],
|
|
||||||
cookieBanner: null,
|
|
||||||
consents: [],
|
|
||||||
dsrConfig: null,
|
|
||||||
escalationWorkflows: [],
|
|
||||||
sbom: null,
|
|
||||||
securityIssues: [],
|
|
||||||
securityBacklog: [],
|
|
||||||
commandBarHistory: [],
|
|
||||||
recentSearches: [],
|
|
||||||
preferences: {
|
|
||||||
language: 'de',
|
|
||||||
theme: 'light',
|
|
||||||
compactMode: false,
|
|
||||||
showHints: true,
|
|
||||||
autoSave: true,
|
|
||||||
autoValidate: true,
|
|
||||||
allowParallelWork: true,
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('exportToPDF', () => {
|
|
||||||
it('should return a Blob', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
const result = await exportToPDF(state)
|
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(Blob)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create a PDF with the correct type', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
const result = await exportToPDF(state)
|
|
||||||
|
|
||||||
expect(result.type).toBe('application/pdf')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty state', async () => {
|
|
||||||
const emptyState = createMockState({
|
|
||||||
useCases: [],
|
|
||||||
risks: [],
|
|
||||||
controls: [],
|
|
||||||
completedSteps: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await exportToPDF(emptyState)
|
|
||||||
expect(result).toBeInstanceOf(Blob)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle state with multiple risks of different severities', async () => {
|
|
||||||
const state = createMockState({
|
|
||||||
risks: [
|
|
||||||
{
|
|
||||||
id: 'risk-1',
|
|
||||||
title: 'Critical Risk',
|
|
||||||
description: 'Critical',
|
|
||||||
category: 'Security',
|
|
||||||
likelihood: 5,
|
|
||||||
impact: 5,
|
|
||||||
severity: 'CRITICAL',
|
|
||||||
inherentRiskScore: 25,
|
|
||||||
residualRiskScore: 15,
|
|
||||||
status: 'IDENTIFIED',
|
|
||||||
mitigation: [],
|
|
||||||
owner: null,
|
|
||||||
relatedControls: [],
|
|
||||||
relatedRequirements: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'risk-2',
|
|
||||||
title: 'Low Risk',
|
|
||||||
description: 'Low',
|
|
||||||
category: 'Operational',
|
|
||||||
likelihood: 1,
|
|
||||||
impact: 1,
|
|
||||||
severity: 'LOW',
|
|
||||||
inherentRiskScore: 1,
|
|
||||||
residualRiskScore: 1,
|
|
||||||
status: 'ACCEPTED',
|
|
||||||
mitigation: [],
|
|
||||||
owner: null,
|
|
||||||
relatedControls: [],
|
|
||||||
relatedRequirements: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await exportToPDF(state)
|
|
||||||
expect(result).toBeInstanceOf(Blob)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('exportToZIP', () => {
|
|
||||||
it('should return a Blob', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
const result = await exportToZIP(state)
|
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(Blob)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should create a ZIP with the correct type', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
const result = await exportToZIP(state)
|
|
||||||
|
|
||||||
expect(result.type).toBe('application/zip')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty state', async () => {
|
|
||||||
const emptyState = createMockState({
|
|
||||||
useCases: [],
|
|
||||||
risks: [],
|
|
||||||
controls: [],
|
|
||||||
completedSteps: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await exportToZIP(emptyState)
|
|
||||||
expect(result).toBeInstanceOf(Blob)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respect includeEvidence option', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
const result = await exportToZIP(state, { includeEvidence: false })
|
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(Blob)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should respect includeDocuments option', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
const result = await exportToZIP(state, { includeDocuments: false })
|
|
||||||
|
|
||||||
expect(result).toBeInstanceOf(Blob)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('downloadExport', () => {
|
|
||||||
let mockCreateElement: ReturnType<typeof vi.spyOn>
|
|
||||||
let mockAppendChild: ReturnType<typeof vi.spyOn>
|
|
||||||
let mockRemoveChild: ReturnType<typeof vi.spyOn>
|
|
||||||
let mockLink: { href: string; download: string; click: ReturnType<typeof vi.fn> }
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mockLink = {
|
|
||||||
href: '',
|
|
||||||
download: '',
|
|
||||||
click: vi.fn(),
|
|
||||||
}
|
|
||||||
|
|
||||||
mockCreateElement = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as unknown as HTMLElement)
|
|
||||||
mockAppendChild = vi.spyOn(document.body, 'appendChild').mockImplementation(() => mockLink as unknown as HTMLElement)
|
|
||||||
mockRemoveChild = vi.spyOn(document.body, 'removeChild').mockImplementation(() => mockLink as unknown as HTMLElement)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should download JSON format', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
await downloadExport(state, 'json')
|
|
||||||
|
|
||||||
expect(mockLink.download).toContain('.json')
|
|
||||||
expect(mockLink.click).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should download PDF format', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
await downloadExport(state, 'pdf')
|
|
||||||
|
|
||||||
expect(mockLink.download).toContain('.pdf')
|
|
||||||
expect(mockLink.click).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should download ZIP format', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
await downloadExport(state, 'zip')
|
|
||||||
|
|
||||||
expect(mockLink.download).toContain('.zip')
|
|
||||||
expect(mockLink.click).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include date in filename', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
await downloadExport(state, 'json')
|
|
||||||
|
|
||||||
// Check that filename contains a date pattern
|
|
||||||
expect(mockLink.download).toMatch(/ai-compliance-sdk-\d{4}-\d{2}-\d{2}\.json/)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should throw error for unknown format', async () => {
|
|
||||||
const state = createMockState()
|
|
||||||
|
|
||||||
await expect(downloadExport(state, 'unknown' as any)).rejects.toThrow('Unknown export format')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
import {
|
|
||||||
SDK_STEPS,
|
|
||||||
getStepById,
|
|
||||||
getStepByUrl,
|
|
||||||
getNextStep,
|
|
||||||
getPreviousStep,
|
|
||||||
getCompletionPercentage,
|
|
||||||
getPhaseCompletionPercentage,
|
|
||||||
type SDKState,
|
|
||||||
} from '../types'
|
|
||||||
|
|
||||||
describe('SDK_STEPS', () => {
|
|
||||||
it('should have steps defined for both phases', () => {
|
|
||||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
|
|
||||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
|
|
||||||
|
|
||||||
expect(phase1Steps.length).toBeGreaterThan(0)
|
|
||||||
expect(phase2Steps.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have unique IDs for all steps', () => {
|
|
||||||
const ids = SDK_STEPS.map(s => s.id)
|
|
||||||
const uniqueIds = new Set(ids)
|
|
||||||
expect(uniqueIds.size).toBe(ids.length)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have unique URLs for all steps', () => {
|
|
||||||
const urls = SDK_STEPS.map(s => s.url)
|
|
||||||
const uniqueUrls = new Set(urls)
|
|
||||||
expect(uniqueUrls.size).toBe(urls.length)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have checkpoint IDs for all steps', () => {
|
|
||||||
SDK_STEPS.forEach(step => {
|
|
||||||
expect(step.checkpointId).toBeDefined()
|
|
||||||
expect(step.checkpointId.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getStepById', () => {
|
|
||||||
it('should return the correct step for a valid ID', () => {
|
|
||||||
const step = getStepById('use-case-workshop')
|
|
||||||
expect(step).toBeDefined()
|
|
||||||
expect(step?.name).toBe('Use Case Workshop')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return undefined for an invalid ID', () => {
|
|
||||||
const step = getStepById('invalid-step-id')
|
|
||||||
expect(step).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should find steps in Phase 2', () => {
|
|
||||||
const step = getStepById('dsfa')
|
|
||||||
expect(step).toBeDefined()
|
|
||||||
expect(step?.phase).toBe(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getStepByUrl', () => {
|
|
||||||
it('should return the correct step for a valid URL', () => {
|
|
||||||
const step = getStepByUrl('/sdk/advisory-board')
|
|
||||||
expect(step).toBeDefined()
|
|
||||||
expect(step?.id).toBe('use-case-workshop')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return undefined for an invalid URL', () => {
|
|
||||||
const step = getStepByUrl('/invalid/url')
|
|
||||||
expect(step).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should find Phase 2 steps by URL', () => {
|
|
||||||
const step = getStepByUrl('/sdk/dsfa')
|
|
||||||
expect(step).toBeDefined()
|
|
||||||
expect(step?.id).toBe('dsfa')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getNextStep', () => {
|
|
||||||
it('should return the next step in sequence', () => {
|
|
||||||
const nextStep = getNextStep('use-case-workshop')
|
|
||||||
expect(nextStep).toBeDefined()
|
|
||||||
expect(nextStep?.id).toBe('screening')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return undefined for the last step', () => {
|
|
||||||
const lastStep = SDK_STEPS[SDK_STEPS.length - 1]
|
|
||||||
const nextStep = getNextStep(lastStep.id)
|
|
||||||
expect(nextStep).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle transition between phases', () => {
|
|
||||||
const lastPhase1Step = SDK_STEPS.filter(s => s.phase === 1).pop()
|
|
||||||
expect(lastPhase1Step).toBeDefined()
|
|
||||||
|
|
||||||
const nextStep = getNextStep(lastPhase1Step!.id)
|
|
||||||
expect(nextStep?.phase).toBe(2)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getPreviousStep', () => {
|
|
||||||
it('should return the previous step in sequence', () => {
|
|
||||||
const prevStep = getPreviousStep('screening')
|
|
||||||
expect(prevStep).toBeDefined()
|
|
||||||
expect(prevStep?.id).toBe('use-case-workshop')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return undefined for the first step', () => {
|
|
||||||
const prevStep = getPreviousStep('use-case-workshop')
|
|
||||||
expect(prevStep).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getCompletionPercentage', () => {
|
|
||||||
const createMockState = (completedSteps: string[]): SDKState => ({
|
|
||||||
version: '1.0.0',
|
|
||||||
lastModified: new Date(),
|
|
||||||
tenantId: 'test',
|
|
||||||
userId: 'test',
|
|
||||||
subscription: 'PROFESSIONAL',
|
|
||||||
currentPhase: 1,
|
|
||||||
currentStep: 'use-case-workshop',
|
|
||||||
completedSteps,
|
|
||||||
checkpoints: {},
|
|
||||||
useCases: [],
|
|
||||||
activeUseCase: null,
|
|
||||||
screening: null,
|
|
||||||
modules: [],
|
|
||||||
requirements: [],
|
|
||||||
controls: [],
|
|
||||||
evidence: [],
|
|
||||||
checklist: [],
|
|
||||||
risks: [],
|
|
||||||
aiActClassification: null,
|
|
||||||
obligations: [],
|
|
||||||
dsfa: null,
|
|
||||||
toms: [],
|
|
||||||
retentionPolicies: [],
|
|
||||||
vvt: [],
|
|
||||||
documents: [],
|
|
||||||
cookieBanner: null,
|
|
||||||
consents: [],
|
|
||||||
dsrConfig: null,
|
|
||||||
escalationWorkflows: [],
|
|
||||||
sbom: null,
|
|
||||||
securityIssues: [],
|
|
||||||
securityBacklog: [],
|
|
||||||
commandBarHistory: [],
|
|
||||||
recentSearches: [],
|
|
||||||
preferences: {
|
|
||||||
language: 'de',
|
|
||||||
theme: 'light',
|
|
||||||
compactMode: false,
|
|
||||||
showHints: true,
|
|
||||||
autoSave: true,
|
|
||||||
autoValidate: true,
|
|
||||||
allowParallelWork: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 0 for no completed steps', () => {
|
|
||||||
const state = createMockState([])
|
|
||||||
const percentage = getCompletionPercentage(state)
|
|
||||||
expect(percentage).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 100 for all completed steps', () => {
|
|
||||||
const allStepIds = SDK_STEPS.map(s => s.id)
|
|
||||||
const state = createMockState(allStepIds)
|
|
||||||
const percentage = getCompletionPercentage(state)
|
|
||||||
expect(percentage).toBe(100)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should calculate correct percentage for partial completion', () => {
|
|
||||||
const halfSteps = SDK_STEPS.slice(0, Math.floor(SDK_STEPS.length / 2)).map(s => s.id)
|
|
||||||
const state = createMockState(halfSteps)
|
|
||||||
const percentage = getCompletionPercentage(state)
|
|
||||||
expect(percentage).toBeGreaterThan(40)
|
|
||||||
expect(percentage).toBeLessThan(60)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('getPhaseCompletionPercentage', () => {
|
|
||||||
const createMockState = (completedSteps: string[]): SDKState => ({
|
|
||||||
version: '1.0.0',
|
|
||||||
lastModified: new Date(),
|
|
||||||
tenantId: 'test',
|
|
||||||
userId: 'test',
|
|
||||||
subscription: 'PROFESSIONAL',
|
|
||||||
currentPhase: 1,
|
|
||||||
currentStep: 'use-case-workshop',
|
|
||||||
completedSteps,
|
|
||||||
checkpoints: {},
|
|
||||||
useCases: [],
|
|
||||||
activeUseCase: null,
|
|
||||||
screening: null,
|
|
||||||
modules: [],
|
|
||||||
requirements: [],
|
|
||||||
controls: [],
|
|
||||||
evidence: [],
|
|
||||||
checklist: [],
|
|
||||||
risks: [],
|
|
||||||
aiActClassification: null,
|
|
||||||
obligations: [],
|
|
||||||
dsfa: null,
|
|
||||||
toms: [],
|
|
||||||
retentionPolicies: [],
|
|
||||||
vvt: [],
|
|
||||||
documents: [],
|
|
||||||
cookieBanner: null,
|
|
||||||
consents: [],
|
|
||||||
dsrConfig: null,
|
|
||||||
escalationWorkflows: [],
|
|
||||||
sbom: null,
|
|
||||||
securityIssues: [],
|
|
||||||
securityBacklog: [],
|
|
||||||
commandBarHistory: [],
|
|
||||||
recentSearches: [],
|
|
||||||
preferences: {
|
|
||||||
language: 'de',
|
|
||||||
theme: 'light',
|
|
||||||
compactMode: false,
|
|
||||||
showHints: true,
|
|
||||||
autoSave: true,
|
|
||||||
autoValidate: true,
|
|
||||||
allowParallelWork: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 0 for Phase 1 with no completed steps', () => {
|
|
||||||
const state = createMockState([])
|
|
||||||
const percentage = getPhaseCompletionPercentage(state, 1)
|
|
||||||
expect(percentage).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return 100 for Phase 1 when all Phase 1 steps are complete', () => {
|
|
||||||
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1).map(s => s.id)
|
|
||||||
const state = createMockState(phase1Steps)
|
|
||||||
const percentage = getPhaseCompletionPercentage(state, 1)
|
|
||||||
expect(percentage).toBe(100)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not count Phase 2 steps in Phase 1 percentage', () => {
|
|
||||||
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2).map(s => s.id)
|
|
||||||
const state = createMockState(phase2Steps)
|
|
||||||
const percentage = getPhaseCompletionPercentage(state, 1)
|
|
||||||
expect(percentage).toBe(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,722 +0,0 @@
|
|||||||
import type { ScopeProfilingAnswer, ComplianceDepthLevel, ScopeDocumentType } from './compliance-scope-types'
|
|
||||||
|
|
||||||
export interface GoldenTest {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
answers: ScopeProfilingAnswer[]
|
|
||||||
expectedLevel: ComplianceDepthLevel | null // null for prefill tests
|
|
||||||
expectedMinDocuments?: ScopeDocumentType[]
|
|
||||||
expectedHardTriggerIds?: string[]
|
|
||||||
expectedDsfaRequired?: boolean
|
|
||||||
tags: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GOLDEN_TESTS: GoldenTest[] = [
|
|
||||||
// GT-01: 2-Person Freelancer, nur B2B, DE-Hosting → L1
|
|
||||||
{
|
|
||||||
id: 'GT-01',
|
|
||||||
name: '2-Person Freelancer B2B',
|
|
||||||
description: 'Kleinstes Setup ohne besondere Risiken',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '2' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'consulting' },
|
|
||||||
{ questionId: 'data_health', value: false },
|
|
||||||
{ questionId: 'data_genetic', value: false },
|
|
||||||
{ questionId: 'data_biometric', value: false },
|
|
||||||
{ questionId: 'data_racial_ethnic', value: false },
|
|
||||||
{ questionId: 'data_political_opinion', value: false },
|
|
||||||
{ questionId: 'data_religious', value: false },
|
|
||||||
{ questionId: 'data_union_membership', value: false },
|
|
||||||
{ questionId: 'data_sexual_orientation', value: false },
|
|
||||||
{ questionId: 'data_criminal', value: false },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
{ questionId: 'process_has_dsfa', value: true },
|
|
||||||
{ questionId: 'process_has_incident_plan', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '<1000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '<100' },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L1',
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
|
|
||||||
expectedHardTriggerIds: [],
|
|
||||||
expectedDsfaRequired: false,
|
|
||||||
tags: ['baseline', 'freelancer', 'b2b'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-02: Solo IT-Berater → L1
|
|
||||||
{
|
|
||||||
id: 'GT-02',
|
|
||||||
name: 'Solo IT-Berater',
|
|
||||||
description: 'Einzelperson, minimale Datenverarbeitung',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '1' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'it_services' },
|
|
||||||
{ questionId: 'data_health', value: false },
|
|
||||||
{ questionId: 'data_genetic', value: false },
|
|
||||||
{ questionId: 'data_biometric', value: false },
|
|
||||||
{ questionId: 'data_volume', value: '<1000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '<50' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L1',
|
|
||||||
expectedHardTriggerIds: [],
|
|
||||||
tags: ['baseline', 'solo', 'minimal'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-03: 5-Person Agentur, Website, kein Tracking → L1
|
|
||||||
{
|
|
||||||
id: 'GT-03',
|
|
||||||
name: '5-Person Agentur ohne Tracking',
|
|
||||||
description: 'Kleine Agentur, einfache Website ohne Analytics',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '5' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'marketing' },
|
|
||||||
{ questionId: 'tech_has_website', value: true },
|
|
||||||
{ questionId: 'tech_has_tracking', value: false },
|
|
||||||
{ questionId: 'data_volume', value: '1000-10000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '100-1000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L1',
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
|
|
||||||
tags: ['baseline', 'agency', 'simple'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-04: 30-Person SaaS B2B, EU-Cloud → L2 (scale trigger)
|
|
||||||
{
|
|
||||||
id: 'GT-04',
|
|
||||||
name: '30-Person SaaS B2B',
|
|
||||||
description: 'Scale-Trigger durch Mitarbeiterzahl',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '30' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'software' },
|
|
||||||
{ questionId: 'tech_has_cloud', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '10000-100000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '1000-10000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
{ questionId: 'process_has_dsfa', value: false },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L2',
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER'],
|
|
||||||
tags: ['scale', 'saas', 'growth'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-05: 50-Person Handel B2C, Webshop → L2 (B2C+Webshop)
|
|
||||||
{
|
|
||||||
id: 'GT-05',
|
|
||||||
name: '50-Person E-Commerce B2C',
|
|
||||||
description: 'B2C mit Webshop erhöht Anforderungen',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '50' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'retail' },
|
|
||||||
{ questionId: 'tech_has_webshop', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '10000-100000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L2',
|
|
||||||
expectedHardTriggerIds: ['HT-H01'],
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER', 'EINWILLIGUNG'],
|
|
||||||
tags: ['b2c', 'webshop', 'retail'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-06: 80-Person Dienstleister, Cloud → L2 (scale)
|
|
||||||
{
|
|
||||||
id: 'GT-06',
|
|
||||||
name: '80-Person Dienstleister',
|
|
||||||
description: 'Größerer Betrieb mit Cloud-Services',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '80' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'professional_services' },
|
|
||||||
{ questionId: 'tech_has_cloud', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '1000-10000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L2',
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'AVV'],
|
|
||||||
tags: ['scale', 'services'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-07: 20-Person Startup mit GA4 Tracking → L2 (tracking)
|
|
||||||
{
|
|
||||||
id: 'GT-07',
|
|
||||||
name: 'Startup mit Google Analytics',
|
|
||||||
description: 'Tracking-Tools erhöhen Compliance-Anforderungen',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '20' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'technology' },
|
|
||||||
{ questionId: 'tech_has_website', value: true },
|
|
||||||
{ questionId: 'tech_has_tracking', value: true },
|
|
||||||
{ questionId: 'tech_tracking_tools', value: 'google_analytics' },
|
|
||||||
{ questionId: 'data_volume', value: '10000-100000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L2',
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER', 'EINWILLIGUNG'],
|
|
||||||
tags: ['tracking', 'analytics', 'startup'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-08: Kita-App (Minderjaehrige) → L3 (HT-B01)
|
|
||||||
{
|
|
||||||
id: 'GT-08',
|
|
||||||
name: 'Kita-App für Eltern',
|
|
||||||
description: 'Datenverarbeitung von Minderjährigen unter 16',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '15' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'education' },
|
|
||||||
{ questionId: 'data_subjects_minors', value: true },
|
|
||||||
{ questionId: 'data_subjects_minors_age', value: '<16' },
|
|
||||||
{ questionId: 'data_volume', value: '1000-10000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-B01'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'EINWILLIGUNG', 'AVV'],
|
|
||||||
tags: ['hard-trigger', 'minors', 'education'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-09: Krankenhaus-Software → L3 (HT-A01)
|
|
||||||
{
|
|
||||||
id: 'GT-09',
|
|
||||||
name: 'Krankenhaus-Verwaltungssoftware',
|
|
||||||
description: 'Gesundheitsdaten Art. 9 DSGVO',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '200' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'healthcare' },
|
|
||||||
{ questionId: 'data_health', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '10-50' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-A01'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
|
|
||||||
tags: ['hard-trigger', 'health', 'art9'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-10: HR-Scoring-Plattform → L3 (HT-C01)
|
|
||||||
{
|
|
||||||
id: 'GT-10',
|
|
||||||
name: 'HR-Scoring für Bewerbungen',
|
|
||||||
description: 'Automatisierte Entscheidungen im HR-Bereich',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '40' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'hr_tech' },
|
|
||||||
{ questionId: 'tech_has_adm', value: true },
|
|
||||||
{ questionId: 'tech_adm_type', value: 'profiling' },
|
|
||||||
{ questionId: 'tech_adm_impact', value: 'employment' },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-C01'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
|
|
||||||
tags: ['hard-trigger', 'adm', 'profiling'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-11: Fintech Kreditscoring → L3 (HT-H05 + C01)
|
|
||||||
{
|
|
||||||
id: 'GT-11',
|
|
||||||
name: 'Fintech Kreditscoring',
|
|
||||||
description: 'Finanzsektor mit automatisierten Entscheidungen',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '120' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'finance' },
|
|
||||||
{ questionId: 'tech_has_adm', value: true },
|
|
||||||
{ questionId: 'tech_adm_type', value: 'scoring' },
|
|
||||||
{ questionId: 'tech_adm_impact', value: 'credit' },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-H05', 'HT-C01'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
|
|
||||||
tags: ['hard-trigger', 'finance', 'scoring'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-12: Bildungsplattform Minderjaehrige → L3 (HT-B01)
|
|
||||||
{
|
|
||||||
id: 'GT-12',
|
|
||||||
name: 'Online-Lernplattform für Schüler',
|
|
||||||
description: 'Bildungssektor mit minderjährigen Nutzern',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '35' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'education' },
|
|
||||||
{ questionId: 'data_subjects_minors', value: true },
|
|
||||||
{ questionId: 'data_subjects_minors_age', value: '<16' },
|
|
||||||
{ questionId: 'tech_has_tracking', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-B01'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'education', 'minors'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-13: Datenbroker → L3 (HT-H02)
|
|
||||||
{
|
|
||||||
id: 'GT-13',
|
|
||||||
name: 'Datenbroker / Adresshandel',
|
|
||||||
description: 'Geschäftsmodell basiert auf Datenhandel',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '25' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'data_broker' },
|
|
||||||
{ questionId: 'data_is_core_business', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '100-1000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-H02'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'data-broker'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-14: Video + ADM → L3 (HT-D05)
|
|
||||||
{
|
|
||||||
id: 'GT-14',
|
|
||||||
name: 'Videoüberwachung mit Gesichtserkennung',
|
|
||||||
description: 'Biometrische Daten mit automatisierter Verarbeitung',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '60' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'security' },
|
|
||||||
{ questionId: 'data_biometric', value: true },
|
|
||||||
{ questionId: 'tech_has_video_surveillance', value: true },
|
|
||||||
{ questionId: 'tech_has_adm', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-D05'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'biometric', 'video'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-15: 500-MA Konzern ohne Zert → L3 (HT-G04)
|
|
||||||
{
|
|
||||||
id: 'GT-15',
|
|
||||||
name: 'Großunternehmen ohne Zertifizierung',
|
|
||||||
description: 'Scale-Trigger durch Unternehmensgröße',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '500' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'manufacturing' },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '>100000' },
|
|
||||||
{ questionId: 'cert_has_iso27001', value: false },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-G04'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'scale', 'enterprise'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-16: ISO 27001 Anbieter → L4 (HT-F01)
|
|
||||||
{
|
|
||||||
id: 'GT-16',
|
|
||||||
name: 'ISO 27001 zertifizierter Cloud-Provider',
|
|
||||||
description: 'Zertifizierung erfordert höchste Compliance',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '150' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'cloud_services' },
|
|
||||||
{ questionId: 'cert_has_iso27001', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
{ questionId: 'process_has_dsfa', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L4',
|
|
||||||
expectedHardTriggerIds: ['HT-F01'],
|
|
||||||
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV', 'CERT_ISO27001'],
|
|
||||||
tags: ['hard-trigger', 'certification', 'iso'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-17: TISAX Automobilzulieferer → L4 (HT-F04)
|
|
||||||
{
|
|
||||||
id: 'GT-17',
|
|
||||||
name: 'TISAX-zertifizierter Automobilzulieferer',
|
|
||||||
description: 'Automotive-Branche mit TISAX-Anforderungen',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '300' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'automotive' },
|
|
||||||
{ questionId: 'cert_has_tisax', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '10-50' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L4',
|
|
||||||
expectedHardTriggerIds: ['HT-F04'],
|
|
||||||
tags: ['hard-trigger', 'certification', 'tisax'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-18: ISO 27701 Cloud-Provider → L4 (HT-F02)
|
|
||||||
{
|
|
||||||
id: 'GT-18',
|
|
||||||
name: 'ISO 27701 Privacy-zertifiziert',
|
|
||||||
description: 'Privacy-spezifische Zertifizierung',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '200' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'cloud_services' },
|
|
||||||
{ questionId: 'cert_has_iso27701', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
{ questionId: 'process_has_dsfa', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L4',
|
|
||||||
expectedHardTriggerIds: ['HT-F02'],
|
|
||||||
tags: ['hard-trigger', 'certification', 'privacy'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-19: Grosskonzern + Art.9 + >1M DS → L4 (HT-G05)
|
|
||||||
{
|
|
||||||
id: 'GT-19',
|
|
||||||
name: 'Konzern mit sensiblen Massendaten',
|
|
||||||
description: 'Kombination aus Scale und Art. 9 Daten',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '2000' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'insurance' },
|
|
||||||
{ questionId: 'data_health', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '>100000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L4',
|
|
||||||
expectedHardTriggerIds: ['HT-G05'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'scale', 'art9'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-20: Nur B2C Webshop → L2 (HT-H01)
|
|
||||||
{
|
|
||||||
id: 'GT-20',
|
|
||||||
name: 'Reiner B2C Webshop',
|
|
||||||
description: 'B2C-Trigger ohne weitere Risiken',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '12' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'retail' },
|
|
||||||
{ questionId: 'tech_has_webshop', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '10000-100000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '1000-10000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L2',
|
|
||||||
expectedHardTriggerIds: ['HT-H01'],
|
|
||||||
tags: ['b2c', 'webshop'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-21: Keine Daten, keine MA → L1
|
|
||||||
{
|
|
||||||
id: 'GT-21',
|
|
||||||
name: 'Minimale Datenverarbeitung',
|
|
||||||
description: 'Absolute Baseline ohne Risiken',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '1' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'consulting' },
|
|
||||||
{ questionId: 'data_volume', value: '<1000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '<50' },
|
|
||||||
{ questionId: 'tech_has_website', value: false },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L1',
|
|
||||||
expectedHardTriggerIds: [],
|
|
||||||
tags: ['baseline', 'minimal'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-22: Alle Art.9 Kategorien → L3 (HT-A09)
|
|
||||||
{
|
|
||||||
id: 'GT-22',
|
|
||||||
name: 'Alle Art. 9 Kategorien',
|
|
||||||
description: 'Multiple sensible Datenkategorien',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '50' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'research' },
|
|
||||||
{ questionId: 'data_health', value: true },
|
|
||||||
{ questionId: 'data_genetic', value: true },
|
|
||||||
{ questionId: 'data_biometric', value: true },
|
|
||||||
{ questionId: 'data_racial_ethnic', value: true },
|
|
||||||
{ questionId: 'data_political_opinion', value: true },
|
|
||||||
{ questionId: 'data_religious', value: true },
|
|
||||||
{ questionId: 'data_union_membership', value: true },
|
|
||||||
{ questionId: 'data_sexual_orientation', value: true },
|
|
||||||
{ questionId: 'data_criminal', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-A09'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'art9', 'multiple-categories'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-23: Drittland + Art.9 → L3 (HT-E04)
|
|
||||||
{
|
|
||||||
id: 'GT-23',
|
|
||||||
name: 'Drittlandtransfer mit Art. 9 Daten',
|
|
||||||
description: 'Kombination aus Drittland und sensiblen Daten',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '45' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'us' },
|
|
||||||
{ questionId: 'org_industry', value: 'healthcare' },
|
|
||||||
{ questionId: 'data_health', value: true },
|
|
||||||
{ questionId: 'tech_has_third_country_transfer', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-E04'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'third-country', 'art9'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-24: Minderjaehrige + Art.9 → L4 (HT-B02)
|
|
||||||
{
|
|
||||||
id: 'GT-24',
|
|
||||||
name: 'Minderjährige mit Gesundheitsdaten',
|
|
||||||
description: 'Kombination aus vulnerabler Gruppe und Art. 9',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '30' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'healthcare' },
|
|
||||||
{ questionId: 'data_subjects_minors', value: true },
|
|
||||||
{ questionId: 'data_subjects_minors_age', value: '<16' },
|
|
||||||
{ questionId: 'data_health', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '10000-100000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L4',
|
|
||||||
expectedHardTriggerIds: ['HT-B02'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'minors', 'health', 'combined-risk'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-25: KI autonome Entscheidungen → L3 (HT-C02)
|
|
||||||
{
|
|
||||||
id: 'GT-25',
|
|
||||||
name: 'KI mit autonomen Entscheidungen',
|
|
||||||
description: 'AI Act relevante autonome Systeme',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '70' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'ai_services' },
|
|
||||||
{ questionId: 'tech_has_adm', value: true },
|
|
||||||
{ questionId: 'tech_adm_type', value: 'autonomous_decision' },
|
|
||||||
{ questionId: 'tech_has_ai', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-C02'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'ai', 'adm'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-26: Multiple Zertifizierungen → L4 (HT-F01-05)
|
|
||||||
{
|
|
||||||
id: 'GT-26',
|
|
||||||
name: 'Multiple Zertifizierungen',
|
|
||||||
description: 'Mehrere Zertifizierungen kombiniert',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '250' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'cloud_services' },
|
|
||||||
{ questionId: 'cert_has_iso27001', value: true },
|
|
||||||
{ questionId: 'cert_has_iso27701', value: true },
|
|
||||||
{ questionId: 'cert_has_soc2', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
{ questionId: 'process_has_dsfa', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L4',
|
|
||||||
expectedHardTriggerIds: ['HT-F01', 'HT-F02', 'HT-F03'],
|
|
||||||
tags: ['hard-trigger', 'certification', 'multiple'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-27: Oeffentlicher Sektor + Gesundheit → L3 (HT-H07 + A01)
|
|
||||||
{
|
|
||||||
id: 'GT-27',
|
|
||||||
name: 'Öffentlicher Sektor mit Gesundheitsdaten',
|
|
||||||
description: 'Behörde mit Art. 9 Datenverarbeitung',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '120' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2g' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'public_sector' },
|
|
||||||
{ questionId: 'org_is_public_sector', value: true },
|
|
||||||
{ questionId: 'data_health', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-H07', 'HT-A01'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'public-sector', 'health'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-28: Bildung + KI + Minderjaehrige → L4 (HT-B03)
|
|
||||||
{
|
|
||||||
id: 'GT-28',
|
|
||||||
name: 'EdTech mit KI für Minderjährige',
|
|
||||||
description: 'Triple-Risiko: Bildung, KI, vulnerable Gruppe',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '55' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'education' },
|
|
||||||
{ questionId: 'data_subjects_minors', value: true },
|
|
||||||
{ questionId: 'data_subjects_minors_age', value: '<16' },
|
|
||||||
{ questionId: 'tech_has_ai', value: true },
|
|
||||||
{ questionId: 'tech_has_adm', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '100000-1000000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L4',
|
|
||||||
expectedHardTriggerIds: ['HT-B03'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'education', 'ai', 'minors', 'triple-risk'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-29: Freelancer mit 1 Art.9 → L3 (hard trigger override despite low score)
|
|
||||||
{
|
|
||||||
id: 'GT-29',
|
|
||||||
name: 'Freelancer mit Gesundheitsdaten',
|
|
||||||
description: 'Hard Trigger überschreibt niedrige Score-Bewertung',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '1' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'de' },
|
|
||||||
{ questionId: 'org_industry', value: 'healthcare' },
|
|
||||||
{ questionId: 'data_health', value: true },
|
|
||||||
{ questionId: 'data_volume', value: '<1000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '<50' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-A01'],
|
|
||||||
expectedDsfaRequired: true,
|
|
||||||
tags: ['hard-trigger', 'override', 'art9', 'freelancer'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-30: Enterprise, alle Prozesse vorhanden → L3 (good process maturity)
|
|
||||||
{
|
|
||||||
id: 'GT-30',
|
|
||||||
name: 'Enterprise mit reifer Prozesslandschaft',
|
|
||||||
description: 'Große Organisation mit allen Compliance-Prozessen',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '450' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
{ questionId: 'org_industry', value: 'manufacturing' },
|
|
||||||
{ questionId: 'data_volume', value: '>1000000' },
|
|
||||||
{ questionId: 'org_customer_count', value: '10000-100000' },
|
|
||||||
{ questionId: 'process_has_vvt', value: true },
|
|
||||||
{ questionId: 'process_has_tom', value: true },
|
|
||||||
{ questionId: 'process_has_dsfa', value: true },
|
|
||||||
{ questionId: 'process_has_incident_plan', value: true },
|
|
||||||
{ questionId: 'process_has_dsb', value: true },
|
|
||||||
{ questionId: 'process_has_training', value: true },
|
|
||||||
],
|
|
||||||
expectedLevel: 'L3',
|
|
||||||
expectedHardTriggerIds: ['HT-G04'],
|
|
||||||
tags: ['enterprise', 'mature', 'all-processes'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-31: SMB, nur 1 Block beantwortet → L1 (graceful degradation)
|
|
||||||
{
|
|
||||||
id: 'GT-31',
|
|
||||||
name: 'Unvollständige Profilerstellung',
|
|
||||||
description: 'Test für graceful degradation bei unvollständigen Antworten',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '8' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2b' },
|
|
||||||
{ questionId: 'org_industry', value: 'consulting' },
|
|
||||||
// Nur Block 1 (Organization) beantwortet, Rest fehlt
|
|
||||||
],
|
|
||||||
expectedLevel: 'L1',
|
|
||||||
expectedHardTriggerIds: [],
|
|
||||||
tags: ['incomplete', 'degradation', 'edge-case'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// GT-32: CompanyProfile Prefill Konsistenz → null (prefill test, no expected level)
|
|
||||||
{
|
|
||||||
id: 'GT-32',
|
|
||||||
name: 'CompanyProfile Prefill Test',
|
|
||||||
description: 'Prüft ob CompanyProfile-Daten korrekt in ScopeProfile übernommen werden',
|
|
||||||
answers: [
|
|
||||||
{ questionId: 'org_employee_count', value: '25' },
|
|
||||||
{ questionId: 'org_business_model', value: 'b2c' },
|
|
||||||
{ questionId: 'org_industry', value: 'retail' },
|
|
||||||
{ questionId: 'tech_hosting_location', value: 'eu' },
|
|
||||||
// Diese Werte sollten mit CompanyProfile-Prefill übereinstimmen
|
|
||||||
],
|
|
||||||
expectedLevel: null,
|
|
||||||
tags: ['prefill', 'integration', 'consistency'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
@@ -1,821 +0,0 @@
|
|||||||
import type {
|
|
||||||
ScopeQuestionBlock,
|
|
||||||
ScopeQuestionBlockId,
|
|
||||||
ScopeProfilingQuestion,
|
|
||||||
ScopeProfilingAnswer,
|
|
||||||
ComplianceScopeState,
|
|
||||||
} from './compliance-scope-types'
|
|
||||||
import type { CompanyProfile } from './types'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block 1: Organisation & Reife
|
|
||||||
*/
|
|
||||||
const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
|
|
||||||
id: 'organisation',
|
|
||||||
title: 'Organisation & Reife',
|
|
||||||
description: 'Grundlegende Informationen zu Ihrer Organisation und Compliance-Zielen',
|
|
||||||
order: 1,
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
id: 'org_employee_count',
|
|
||||||
type: 'number',
|
|
||||||
question: 'Wie viele Mitarbeiter hat Ihre Organisation?',
|
|
||||||
helpText: 'Geben Sie die Gesamtzahl aller Beschäftigten an (inkl. Teilzeit, Minijobs)',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 5, complexity: 8, assurance: 6 },
|
|
||||||
mapsToCompanyProfile: 'employeeCount',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'org_customer_count',
|
|
||||||
type: 'single',
|
|
||||||
question: 'Wie viele Kunden/Nutzer betreuen Sie?',
|
|
||||||
helpText: 'Schätzen Sie die Anzahl aktiver Kunden oder Nutzer',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: '<100', label: 'Weniger als 100' },
|
|
||||||
{ value: '100-1000', label: '100 bis 1.000' },
|
|
||||||
{ value: '1000-10000', label: '1.000 bis 10.000' },
|
|
||||||
{ value: '10000-100000', label: '10.000 bis 100.000' },
|
|
||||||
{ value: '100000+', label: 'Mehr als 100.000' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 6, complexity: 7, assurance: 6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'org_annual_revenue',
|
|
||||||
type: 'single',
|
|
||||||
question: 'Wie hoch ist Ihr jährlicher Umsatz?',
|
|
||||||
helpText: 'Wählen Sie die zutreffende Umsatzklasse',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: '<2Mio', label: 'Unter 2 Mio. EUR' },
|
|
||||||
{ value: '2-10Mio', label: '2 bis 10 Mio. EUR' },
|
|
||||||
{ value: '10-50Mio', label: '10 bis 50 Mio. EUR' },
|
|
||||||
{ value: '>50Mio', label: 'Über 50 Mio. EUR' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 4, complexity: 6, assurance: 7 },
|
|
||||||
mapsToCompanyProfile: 'annualRevenue',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'org_cert_target',
|
|
||||||
type: 'multi',
|
|
||||||
question: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?',
|
|
||||||
helpText: 'Mehrfachauswahl möglich. Zertifizierungen erhöhen den Assurance-Bedarf',
|
|
||||||
required: false,
|
|
||||||
options: [
|
|
||||||
{ value: 'ISO27001', label: 'ISO 27001 (Informationssicherheit)' },
|
|
||||||
{ value: 'ISO27701', label: 'ISO 27701 (Datenschutz-Erweiterung)' },
|
|
||||||
{ value: 'TISAX', label: 'TISAX (Automotive)' },
|
|
||||||
{ value: 'SOC2', label: 'SOC 2 (US-Standard)' },
|
|
||||||
{ value: 'BSI-Grundschutz', label: 'BSI IT-Grundschutz' },
|
|
||||||
{ value: 'Keine', label: 'Keine Zertifizierung geplant' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 3, complexity: 5, assurance: 10 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'org_industry',
|
|
||||||
type: 'single',
|
|
||||||
question: 'In welcher Branche sind Sie tätig?',
|
|
||||||
helpText: 'Ihre Branche beeinflusst Risikobewertung und regulatorische Anforderungen',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'it_software', label: 'IT & Software' },
|
|
||||||
{ value: 'healthcare', label: 'Gesundheitswesen' },
|
|
||||||
{ value: 'education', label: 'Bildung & Forschung' },
|
|
||||||
{ value: 'finance', label: 'Finanzdienstleistungen' },
|
|
||||||
{ value: 'retail', label: 'Einzelhandel & E-Commerce' },
|
|
||||||
{ value: 'manufacturing', label: 'Produktion & Fertigung' },
|
|
||||||
{ value: 'consulting', label: 'Beratung & Dienstleistungen' },
|
|
||||||
{ value: 'public', label: 'Öffentliche Verwaltung' },
|
|
||||||
{ value: 'other', label: 'Sonstige' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
|
|
||||||
mapsToCompanyProfile: 'industry',
|
|
||||||
mapsToVVTQuestion: 'org_industry',
|
|
||||||
mapsToLFQuestion: 'org-branche',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'org_business_model',
|
|
||||||
type: 'single',
|
|
||||||
question: 'Was ist Ihr primäres Geschäftsmodell?',
|
|
||||||
helpText: 'B2C-Modelle haben höhere Datenschutzanforderungen',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'b2b', label: 'B2B (Business-to-Business)' },
|
|
||||||
{ value: 'b2c', label: 'B2C (Business-to-Consumer)' },
|
|
||||||
{ value: 'both', label: 'B2B und B2C gemischt' },
|
|
||||||
{ value: 'b2g', label: 'B2G (Business-to-Government)' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
|
|
||||||
mapsToCompanyProfile: 'businessModel',
|
|
||||||
mapsToVVTQuestion: 'org_b2b_b2c',
|
|
||||||
mapsToLFQuestion: 'org-geschaeftsmodell',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'org_has_dsb',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Haben Sie einen Datenschutzbeauftragten bestellt?',
|
|
||||||
helpText: 'Ein DSB ist bei mehr als 20 Personen mit regelmäßiger Datenverarbeitung Pflicht',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 5, complexity: 3, assurance: 6 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block 2: Daten & Betroffene
|
|
||||||
*/
|
|
||||||
const BLOCK_2_DATA: ScopeQuestionBlock = {
|
|
||||||
id: 'data',
|
|
||||||
title: 'Daten & Betroffene',
|
|
||||||
description: 'Art und Umfang der verarbeiteten personenbezogenen Daten',
|
|
||||||
order: 2,
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
id: 'data_minors',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Verarbeiten Sie Daten von Minderjährigen?',
|
|
||||||
helpText: 'Besondere Schutzpflichten für unter 16-Jährige (bzw. 13-Jährige bei Online-Diensten)',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
|
|
||||||
mapsToVVTQuestion: 'data_minors',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'data_art9',
|
|
||||||
type: 'multi',
|
|
||||||
question: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?',
|
|
||||||
helpText: 'Diese Daten unterliegen erhöhten Schutzanforderungen',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'gesundheit', label: 'Gesundheitsdaten' },
|
|
||||||
{ value: 'biometrie', label: 'Biometrische Daten (z.B. Fingerabdruck, Gesichtserkennung)' },
|
|
||||||
{ value: 'genetik', label: 'Genetische Daten' },
|
|
||||||
{ value: 'politisch', label: 'Politische Meinungen' },
|
|
||||||
{ value: 'religion', label: 'Religiöse/weltanschauliche Überzeugungen' },
|
|
||||||
{ value: 'gewerkschaft', label: 'Gewerkschaftszugehörigkeit' },
|
|
||||||
{ value: 'sexualleben', label: 'Sexualleben/sexuelle Orientierung' },
|
|
||||||
{ value: 'strafrechtlich', label: 'Strafrechtliche Verurteilungen/Straftaten' },
|
|
||||||
{ value: 'ethnisch', label: 'Ethnische Herkunft' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
|
|
||||||
mapsToVVTQuestion: 'data_health',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'data_hr',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Verarbeiten Sie Personaldaten (HR)?',
|
|
||||||
helpText: 'Bewerberdaten, Gehälter, Leistungsbeurteilungen etc.',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
|
|
||||||
mapsToVVTQuestion: 'dept_hr',
|
|
||||||
mapsToLFQuestion: 'data-hr',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'data_communication',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?',
|
|
||||||
helpText: 'Inhalte oder Metadaten von Kommunikationsvorgängen',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'data_financial',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?',
|
|
||||||
helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
|
|
||||||
mapsToVVTQuestion: 'dept_finance',
|
|
||||||
mapsToLFQuestion: 'data-buchhaltung',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'data_volume',
|
|
||||||
type: 'single',
|
|
||||||
question: 'Wie viele Personendatensätze verarbeiten Sie insgesamt?',
|
|
||||||
helpText: 'Schätzen Sie die Gesamtzahl betroffener Personen',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: '<1000', label: 'Unter 1.000' },
|
|
||||||
{ value: '1000-10000', label: '1.000 bis 10.000' },
|
|
||||||
{ value: '10000-100000', label: '10.000 bis 100.000' },
|
|
||||||
{ value: '100000-1000000', label: '100.000 bis 1 Mio.' },
|
|
||||||
{ value: '>1000000', label: 'Über 1 Mio.' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block 3: Verarbeitung & Zweck
|
|
||||||
*/
|
|
||||||
const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
|
|
||||||
id: 'processing',
|
|
||||||
title: 'Verarbeitung & Zweck',
|
|
||||||
description: 'Wie und wofür werden personenbezogene Daten verarbeitet?',
|
|
||||||
order: 3,
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
id: 'proc_tracking',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Setzen Sie Tracking oder Profiling ein?',
|
|
||||||
helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_adm_scoring',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?',
|
|
||||||
helpText: 'Scoring, Bonitätsprüfung, automatische Ablehnung ohne menschliche Beteiligung',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_ai_usage',
|
|
||||||
type: 'multi',
|
|
||||||
question: 'Setzen Sie KI-Systeme ein?',
|
|
||||||
helpText: 'KI-Einsatz kann zusätzliche Anforderungen (EU AI Act) auslösen',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'keine', label: 'Keine KI im Einsatz' },
|
|
||||||
{ value: 'chatbot', label: 'Chatbots/Virtuelle Assistenten' },
|
|
||||||
{ value: 'scoring', label: 'Scoring/Risikobewertung' },
|
|
||||||
{ value: 'profiling', label: 'Profiling/Verhaltensvorhersage' },
|
|
||||||
{ value: 'generativ', label: 'Generative KI (Text, Bild, Code)' },
|
|
||||||
{ value: 'autonom', label: 'Autonome Systeme/Entscheidungen' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 8, complexity: 9, assurance: 7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_data_combination',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Führen Sie Daten aus verschiedenen Quellen zusammen?',
|
|
||||||
helpText: 'Data Matching, Anreicherung aus externen Quellen',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 7, complexity: 7, assurance: 6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_employee_monitoring',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Überwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?',
|
|
||||||
helpText: 'Beschäftigtendatenschutz nach § 26 BDSG',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_video_surveillance',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Setzen Sie Videoüberwachung ein?',
|
|
||||||
helpText: 'Kameras in Büros, Produktionsstätten, Verkaufsräumen etc.',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 8, complexity: 5, assurance: 7 },
|
|
||||||
mapsToVVTQuestion: 'special_video_surveillance',
|
|
||||||
mapsToLFQuestion: 'data-video',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block 4: Technik/Hosting/Transfers
|
|
||||||
*/
|
|
||||||
const BLOCK_4_TECH: ScopeQuestionBlock = {
|
|
||||||
id: 'tech',
|
|
||||||
title: 'Technik, Hosting & Transfers',
|
|
||||||
description: 'Technische Infrastruktur und Datenübermittlung',
|
|
||||||
order: 4,
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
id: 'tech_hosting_location',
|
|
||||||
type: 'single',
|
|
||||||
question: 'Wo werden Ihre Daten primär gehostet?',
|
|
||||||
helpText: 'Standort bestimmt anwendbares Datenschutzrecht',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'de', label: 'Deutschland' },
|
|
||||||
{ value: 'eu', label: 'EU (ohne Deutschland)' },
|
|
||||||
{ value: 'ewr', label: 'EWR (z.B. Norwegen, Island)' },
|
|
||||||
{ value: 'us_adequacy', label: 'USA (mit Angemessenheitsbeschluss/DPF)' },
|
|
||||||
{ value: 'drittland', label: 'Drittland ohne Angemessenheitsbeschluss' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tech_subprocessors',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?',
|
|
||||||
helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. – erfordert AVV nach Art. 28 DSGVO',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 6, complexity: 7, assurance: 7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tech_third_country',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Übermitteln Sie Daten in Drittländer?',
|
|
||||||
helpText: 'Transfer außerhalb EU/EWR erfordert Schutzmaßnahmen (SCC, BCR etc.)',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
|
|
||||||
mapsToVVTQuestion: 'transfer_cloud_us',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tech_encryption_rest',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Sind Daten im Ruhezustand verschlüsselt (at rest)?',
|
|
||||||
helpText: 'Datenbank-, Dateisystem- oder Volume-Verschlüsselung',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: -5, complexity: 3, assurance: 7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tech_encryption_transit',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Sind Daten bei Übertragung verschlüsselt (in transit)?',
|
|
||||||
helpText: 'TLS/SSL für alle Verbindungen',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: -5, complexity: 2, assurance: 7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tech_cloud_providers',
|
|
||||||
type: 'multi',
|
|
||||||
question: 'Welche Cloud-Anbieter nutzen Sie?',
|
|
||||||
helpText: 'Mehrfachauswahl möglich',
|
|
||||||
required: false,
|
|
||||||
options: [
|
|
||||||
{ value: 'aws', label: 'Amazon Web Services (AWS)' },
|
|
||||||
{ value: 'azure', label: 'Microsoft Azure' },
|
|
||||||
{ value: 'gcp', label: 'Google Cloud Platform (GCP)' },
|
|
||||||
{ value: 'hetzner', label: 'Hetzner' },
|
|
||||||
{ value: 'ionos', label: 'IONOS' },
|
|
||||||
{ value: 'ovh', label: 'OVH' },
|
|
||||||
{ value: 'andere', label: 'Andere Anbieter' },
|
|
||||||
{ value: 'keine', label: 'Keine Cloud-Nutzung (On-Premise)' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 5, complexity: 6, assurance: 6 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block 5: Rechte & Prozesse
|
|
||||||
*/
|
|
||||||
const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
|
|
||||||
id: 'processes',
|
|
||||||
title: 'Rechte & Prozesse',
|
|
||||||
description: 'Etablierte Datenschutz- und Sicherheitsprozesse',
|
|
||||||
order: 5,
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
id: 'proc_dsar_process',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Haben Sie einen Prozess für Betroffenenrechte (DSAR)?',
|
|
||||||
helpText: 'Auskunft, Löschung, Berichtigung, Widerspruch etc. – Art. 15-22 DSGVO',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 6, complexity: 5, assurance: 8 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_deletion_concept',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Haben Sie ein Löschkonzept?',
|
|
||||||
helpText: 'Definierte Löschfristen und automatisierte Löschroutinen',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 7, complexity: 6, assurance: 8 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_incident_response',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Haben Sie einen Notfallplan für Datenschutzvorfälle?',
|
|
||||||
helpText: 'Incident Response Plan, 72h-Meldepflicht an Aufsichtsbehörde (Art. 33 DSGVO)',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 8, complexity: 6, assurance: 9 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_regular_audits',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Führen Sie regelmäßige Datenschutz-Audits durch?',
|
|
||||||
helpText: 'Interne oder externe Prüfungen mindestens jährlich',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 5, complexity: 4, assurance: 9 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'proc_training',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?',
|
|
||||||
helpText: 'Awareness-Trainings, Onboarding, jährliche Auffrischung',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 6, complexity: 3, assurance: 7 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block 6: Produktkontext
|
|
||||||
*/
|
|
||||||
const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
|
|
||||||
id: 'product',
|
|
||||||
title: 'Produktkontext',
|
|
||||||
description: 'Spezifische Merkmale Ihrer Produkte und Services',
|
|
||||||
order: 6,
|
|
||||||
questions: [
|
|
||||||
{
|
|
||||||
id: 'prod_type',
|
|
||||||
type: 'multi',
|
|
||||||
question: 'Welche Art von Produkten/Services bieten Sie an?',
|
|
||||||
helpText: 'Mehrfachauswahl möglich',
|
|
||||||
required: true,
|
|
||||||
options: [
|
|
||||||
{ value: 'webapp', label: 'Web-Anwendung' },
|
|
||||||
{ value: 'mobile', label: 'Mobile App (iOS/Android)' },
|
|
||||||
{ value: 'saas', label: 'SaaS-Plattform' },
|
|
||||||
{ value: 'onpremise', label: 'On-Premise Software' },
|
|
||||||
{ value: 'api', label: 'API/Schnittstellen' },
|
|
||||||
{ value: 'iot', label: 'IoT/Hardware' },
|
|
||||||
{ value: 'beratung', label: 'Beratungsleistungen' },
|
|
||||||
{ value: 'handel', label: 'Handel/Vertrieb' },
|
|
||||||
],
|
|
||||||
scoreWeights: { risk: 5, complexity: 6, assurance: 5 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'prod_cookies_consent',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Benötigen Sie Cookie-Consent (Tracking-Cookies)?',
|
|
||||||
helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 5, complexity: 4, assurance: 6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'prod_webshop',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Betreiben Sie einen Online-Shop?',
|
|
||||||
helpText: 'E-Commerce mit Zahlungsabwicklung, Bestellverwaltung',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'prod_api_external',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?',
|
|
||||||
helpText: 'Programmierschnittstellen für Partner, Entwickler etc.',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 7, complexity: 7, assurance: 7 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'prod_data_broker',
|
|
||||||
type: 'boolean',
|
|
||||||
question: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?',
|
|
||||||
helpText: 'Verkauf oder Vermittlung personenbezogener Daten',
|
|
||||||
required: true,
|
|
||||||
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All question blocks in order
|
|
||||||
*/
|
|
||||||
export const SCOPE_QUESTION_BLOCKS: ScopeQuestionBlock[] = [
|
|
||||||
BLOCK_1_ORGANISATION,
|
|
||||||
BLOCK_2_DATA,
|
|
||||||
BLOCK_3_PROCESSING,
|
|
||||||
BLOCK_4_TECH,
|
|
||||||
BLOCK_5_PROCESSES,
|
|
||||||
BLOCK_6_PRODUCT,
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefill scope answers from CompanyProfile
|
|
||||||
*/
|
|
||||||
export function prefillFromCompanyProfile(
|
|
||||||
profile: CompanyProfile
|
|
||||||
): ScopeProfilingAnswer[] {
|
|
||||||
const answers: ScopeProfilingAnswer[] = []
|
|
||||||
|
|
||||||
// employeeCount
|
|
||||||
if (profile.employeeCount != null) {
|
|
||||||
answers.push({
|
|
||||||
questionId: 'org_employee_count',
|
|
||||||
value: profile.employeeCount,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// annualRevenue
|
|
||||||
if (profile.annualRevenue) {
|
|
||||||
answers.push({
|
|
||||||
questionId: 'org_annual_revenue',
|
|
||||||
value: profile.annualRevenue,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// industry
|
|
||||||
if (profile.industry) {
|
|
||||||
answers.push({
|
|
||||||
questionId: 'org_industry',
|
|
||||||
value: profile.industry,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// businessModel
|
|
||||||
if (profile.businessModel) {
|
|
||||||
answers.push({
|
|
||||||
questionId: 'org_business_model',
|
|
||||||
value: profile.businessModel,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// dpoName -> org_has_dsb
|
|
||||||
if (profile.dpoName && profile.dpoName.trim() !== '') {
|
|
||||||
answers.push({
|
|
||||||
questionId: 'org_has_dsb',
|
|
||||||
value: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// usesAI -> proc_ai_usage
|
|
||||||
if (profile.usesAI === true) {
|
|
||||||
// We don't know which specific AI type, so just mark as "generativ" as a default
|
|
||||||
answers.push({
|
|
||||||
questionId: 'proc_ai_usage',
|
|
||||||
value: ['generativ'],
|
|
||||||
})
|
|
||||||
} else if (profile.usesAI === false) {
|
|
||||||
answers.push({
|
|
||||||
questionId: 'proc_ai_usage',
|
|
||||||
value: ['keine'],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// offerings -> prod_type mapping
|
|
||||||
if (profile.offerings && profile.offerings.length > 0) {
|
|
||||||
const prodTypes: string[] = []
|
|
||||||
const offeringsLower = profile.offerings.map((o) => o.toLowerCase())
|
|
||||||
|
|
||||||
if (offeringsLower.some((o) => o.includes('webapp') || o.includes('web'))) {
|
|
||||||
prodTypes.push('webapp')
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
offeringsLower.some((o) => o.includes('mobile') || o.includes('app'))
|
|
||||||
) {
|
|
||||||
prodTypes.push('mobile')
|
|
||||||
}
|
|
||||||
if (offeringsLower.some((o) => o.includes('saas') || o.includes('cloud'))) {
|
|
||||||
prodTypes.push('saas')
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
offeringsLower.some(
|
|
||||||
(o) => o.includes('onpremise') || o.includes('on-premise')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
prodTypes.push('onpremise')
|
|
||||||
}
|
|
||||||
if (offeringsLower.some((o) => o.includes('api'))) {
|
|
||||||
prodTypes.push('api')
|
|
||||||
}
|
|
||||||
if (offeringsLower.some((o) => o.includes('iot') || o.includes('hardware'))) {
|
|
||||||
prodTypes.push('iot')
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
offeringsLower.some(
|
|
||||||
(o) => o.includes('beratung') || o.includes('consulting')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
prodTypes.push('beratung')
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
offeringsLower.some(
|
|
||||||
(o) => o.includes('handel') || o.includes('shop') || o.includes('commerce')
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
prodTypes.push('handel')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prodTypes.length > 0) {
|
|
||||||
answers.push({
|
|
||||||
questionId: 'prod_type',
|
|
||||||
value: prodTypes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return answers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefill scope answers from VVT profiling answers
|
|
||||||
*/
|
|
||||||
export function prefillFromVVTAnswers(
|
|
||||||
vvtAnswers: Record<string, unknown>
|
|
||||||
): ScopeProfilingAnswer[] {
|
|
||||||
const answers: ScopeProfilingAnswer[] = []
|
|
||||||
|
|
||||||
// Build reverse mapping: VVT question -> Scope question
|
|
||||||
const reverseMap: Record<string, string> = {}
|
|
||||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
|
||||||
for (const q of block.questions) {
|
|
||||||
if (q.mapsToVVTQuestion) {
|
|
||||||
reverseMap[q.mapsToVVTQuestion] = q.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map VVT answers to scope answers
|
|
||||||
for (const [vvtQuestionId, vvtValue] of Object.entries(vvtAnswers)) {
|
|
||||||
const scopeQuestionId = reverseMap[vvtQuestionId]
|
|
||||||
if (scopeQuestionId) {
|
|
||||||
answers.push({
|
|
||||||
questionId: scopeQuestionId,
|
|
||||||
value: vvtValue,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return answers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefill scope answers from Loeschfristen profiling answers
|
|
||||||
*/
|
|
||||||
export function prefillFromLoeschfristenAnswers(
|
|
||||||
lfAnswers: Array<{ questionId: string; value: unknown }>
|
|
||||||
): ScopeProfilingAnswer[] {
|
|
||||||
const answers: ScopeProfilingAnswer[] = []
|
|
||||||
|
|
||||||
// Build reverse mapping: LF question -> Scope question
|
|
||||||
const reverseMap: Record<string, string> = {}
|
|
||||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
|
||||||
for (const q of block.questions) {
|
|
||||||
if (q.mapsToLFQuestion) {
|
|
||||||
reverseMap[q.mapsToLFQuestion] = q.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map LF answers to scope answers
|
|
||||||
for (const lfAnswer of lfAnswers) {
|
|
||||||
const scopeQuestionId = reverseMap[lfAnswer.questionId]
|
|
||||||
if (scopeQuestionId) {
|
|
||||||
answers.push({
|
|
||||||
questionId: scopeQuestionId,
|
|
||||||
value: lfAnswer.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return answers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export scope answers in VVT format
|
|
||||||
*/
|
|
||||||
export function exportToVVTAnswers(
|
|
||||||
scopeAnswers: ScopeProfilingAnswer[]
|
|
||||||
): Record<string, unknown> {
|
|
||||||
const vvtAnswers: Record<string, unknown> = {}
|
|
||||||
|
|
||||||
for (const answer of scopeAnswers) {
|
|
||||||
// Find the question
|
|
||||||
let question: ScopeProfilingQuestion | undefined
|
|
||||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
|
||||||
question = block.questions.find((q) => q.id === answer.questionId)
|
|
||||||
if (question) break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (question?.mapsToVVTQuestion) {
|
|
||||||
vvtAnswers[question.mapsToVVTQuestion] = answer.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return vvtAnswers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export scope answers in Loeschfristen format
|
|
||||||
*/
|
|
||||||
export function exportToLoeschfristenAnswers(
|
|
||||||
scopeAnswers: ScopeProfilingAnswer[]
|
|
||||||
): Array<{ questionId: string; value: unknown }> {
|
|
||||||
const lfAnswers: Array<{ questionId: string; value: unknown }> = []
|
|
||||||
|
|
||||||
for (const answer of scopeAnswers) {
|
|
||||||
// Find the question
|
|
||||||
let question: ScopeProfilingQuestion | undefined
|
|
||||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
|
||||||
question = block.questions.find((q) => q.id === answer.questionId)
|
|
||||||
if (question) break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (question?.mapsToLFQuestion) {
|
|
||||||
lfAnswers.push({
|
|
||||||
questionId: question.mapsToLFQuestion,
|
|
||||||
value: answer.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lfAnswers
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export scope answers for TOM generator
|
|
||||||
*/
|
|
||||||
export function exportToTOMProfile(
|
|
||||||
scopeAnswers: ScopeProfilingAnswer[]
|
|
||||||
): Record<string, unknown> {
|
|
||||||
const tomProfile: Record<string, unknown> = {}
|
|
||||||
|
|
||||||
// Get answer values
|
|
||||||
const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId)
|
|
||||||
|
|
||||||
// Map relevant scope answers to TOM profile fields
|
|
||||||
tomProfile.industry = getVal('org_industry')
|
|
||||||
tomProfile.employeeCount = getVal('org_employee_count')
|
|
||||||
tomProfile.hasDataMinors = getVal('data_minors')
|
|
||||||
tomProfile.hasSpecialCategories = Array.isArray(getVal('data_art9'))
|
|
||||||
? (getVal('data_art9') as string[]).length > 0
|
|
||||||
: false
|
|
||||||
tomProfile.hasAutomatedDecisions = getVal('proc_adm_scoring')
|
|
||||||
tomProfile.usesAI = Array.isArray(getVal('proc_ai_usage'))
|
|
||||||
? !(getVal('proc_ai_usage') as string[]).includes('keine')
|
|
||||||
: false
|
|
||||||
tomProfile.hasThirdCountryTransfer = getVal('tech_third_country')
|
|
||||||
tomProfile.hasEncryptionRest = getVal('tech_encryption_rest')
|
|
||||||
tomProfile.hasEncryptionTransit = getVal('tech_encryption_transit')
|
|
||||||
tomProfile.hasIncidentResponse = getVal('proc_incident_response')
|
|
||||||
tomProfile.hasDeletionConcept = getVal('proc_deletion_concept')
|
|
||||||
tomProfile.hasRegularAudits = getVal('proc_regular_audits')
|
|
||||||
tomProfile.hasTraining = getVal('proc_training')
|
|
||||||
|
|
||||||
return tomProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a block is complete (all required questions answered)
|
|
||||||
*/
|
|
||||||
export function isBlockComplete(
|
|
||||||
answers: ScopeProfilingAnswer[],
|
|
||||||
blockId: ScopeQuestionBlockId
|
|
||||||
): boolean {
|
|
||||||
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
|
|
||||||
if (!block) return false
|
|
||||||
|
|
||||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
|
||||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
|
||||||
|
|
||||||
return requiredQuestions.every((q) => answeredQuestionIds.has(q.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get progress for a specific block (0-100)
|
|
||||||
*/
|
|
||||||
export function getBlockProgress(
|
|
||||||
answers: ScopeProfilingAnswer[],
|
|
||||||
blockId: ScopeQuestionBlockId
|
|
||||||
): number {
|
|
||||||
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
|
|
||||||
if (!block) return 0
|
|
||||||
|
|
||||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
|
||||||
if (requiredQuestions.length === 0) return 100
|
|
||||||
|
|
||||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
|
||||||
const answeredCount = requiredQuestions.filter((q) =>
|
|
||||||
answeredQuestionIds.has(q.id)
|
|
||||||
).length
|
|
||||||
|
|
||||||
return Math.round((answeredCount / requiredQuestions.length) * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get total progress across all blocks (0-100)
|
|
||||||
*/
|
|
||||||
export function getTotalProgress(answers: ScopeProfilingAnswer[]): number {
|
|
||||||
let totalRequired = 0
|
|
||||||
let totalAnswered = 0
|
|
||||||
|
|
||||||
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
|
|
||||||
|
|
||||||
for (const block of SCOPE_QUESTION_BLOCKS) {
|
|
||||||
const requiredQuestions = block.questions.filter((q) => q.required)
|
|
||||||
totalRequired += requiredQuestions.length
|
|
||||||
totalAnswered += requiredQuestions.filter((q) =>
|
|
||||||
answeredQuestionIds.has(q.id)
|
|
||||||
).length
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalRequired === 0) return 100
|
|
||||||
return Math.round((totalAnswered / totalRequired) * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get answer value for a specific question
|
|
||||||
*/
|
|
||||||
export function getAnswerValue(
|
|
||||||
answers: ScopeProfilingAnswer[],
|
|
||||||
questionId: string
|
|
||||||
): unknown {
|
|
||||||
const answer = answers.find((a) => a.questionId === questionId)
|
|
||||||
return answer?.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all questions as a flat array
|
|
||||||
*/
|
|
||||||
export function getAllQuestions(): ScopeProfilingQuestion[] {
|
|
||||||
return SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions)
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,210 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo Controls for AI Compliance SDK
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Control } from '../types'
|
|
||||||
|
|
||||||
export const DEMO_CONTROLS: Control[] = [
|
|
||||||
// Zugangskontrolle
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-1',
|
|
||||||
name: 'Multi-Faktor-Authentifizierung',
|
|
||||||
description: 'Alle Systemzugriffe erfordern mindestens zwei unabhängige Authentifizierungsfaktoren (Wissen + Besitz).',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Zugangskontrolle',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-1'],
|
|
||||||
owner: 'IT-Sicherheit',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-2',
|
|
||||||
name: 'Rollenbasiertes Berechtigungskonzept',
|
|
||||||
description: 'Zugriffsrechte werden nach dem Least-Privilege-Prinzip anhand definierter Rollen vergeben und regelmäßig überprüft.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
category: 'Zugangskontrolle',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-2'],
|
|
||||||
owner: 'IT-Sicherheit',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Verfügbarkeit
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-3',
|
|
||||||
name: 'Automatisiertes Backup-System',
|
|
||||||
description: 'Tägliche inkrementelle Backups und wöchentliche Vollbackups aller kritischen Daten mit Verschlüsselung.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Verfügbarkeit',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-3'],
|
|
||||||
owner: 'IT-Betrieb',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-4',
|
|
||||||
name: 'Georedundante Datenspeicherung',
|
|
||||||
description: 'Kritische Daten werden synchron in zwei geographisch getrennten Rechenzentren gespeichert.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Verfügbarkeit',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-4'],
|
|
||||||
owner: 'IT-Betrieb',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
// KI-Fairness
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-5',
|
|
||||||
name: 'Bias-Monitoring',
|
|
||||||
description: 'Kontinuierliche Überwachung der KI-Modelle auf systematische Verzerrungen anhand definierter Fairness-Metriken.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'KI-Governance',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'MEDIUM',
|
|
||||||
evidence: ['demo-evi-5'],
|
|
||||||
owner: 'Data Science Lead',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-6',
|
|
||||||
name: 'Human-in-the-Loop',
|
|
||||||
description: 'Kritische automatisierte Entscheidungen werden vor Umsetzung durch qualifizierte Mitarbeiter überprüft.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
category: 'KI-Governance',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-6'],
|
|
||||||
owner: 'Fachbereich HR',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Transparenz
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-7',
|
|
||||||
name: 'Explainable AI Komponenten',
|
|
||||||
description: 'Einsatz von SHAP/LIME zur Erklärung von KI-Entscheidungen für nachvollziehbare Begründungen.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Transparenz',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'MEDIUM',
|
|
||||||
evidence: ['demo-evi-7'],
|
|
||||||
owner: 'Data Science Lead',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-8',
|
|
||||||
name: 'Verständliche Datenschutzinformationen',
|
|
||||||
description: 'Betroffene erhalten klare, verständliche Informationen über die Verarbeitung ihrer Daten gemäß Art. 13-14 DSGVO.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
category: 'Transparenz',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-8'],
|
|
||||||
owner: 'DSB',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Datensparsamkeit
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-9',
|
|
||||||
name: 'Zweckbindungskontrollen',
|
|
||||||
description: 'Technische Maßnahmen stellen sicher, dass Daten nur für definierte Zwecke verarbeitet werden.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Datensparsamkeit',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'MEDIUM',
|
|
||||||
evidence: ['demo-evi-9'],
|
|
||||||
owner: 'IT-Sicherheit',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-10',
|
|
||||||
name: 'Anonymisierungs-Pipeline',
|
|
||||||
description: 'Automatisierte Anonymisierung von Daten für Analysen, wo keine Personenbezug erforderlich ist.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Datensparsamkeit',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-10'],
|
|
||||||
owner: 'Data Engineering',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
// KI-Sicherheit
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-11',
|
|
||||||
name: 'Input-Validierung',
|
|
||||||
description: 'Strenge Validierung aller Eingabedaten zur Verhinderung von Adversarial Attacks auf KI-Modelle.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'KI-Sicherheit',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'MEDIUM',
|
|
||||||
evidence: ['demo-evi-11'],
|
|
||||||
owner: 'Data Science Lead',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-12',
|
|
||||||
name: 'Model Performance Monitoring',
|
|
||||||
description: 'Kontinuierliche Überwachung der Modell-Performance mit automatischen Alerts bei Abweichungen.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'KI-Sicherheit',
|
|
||||||
implementationStatus: 'PARTIAL',
|
|
||||||
effectiveness: 'MEDIUM',
|
|
||||||
evidence: [],
|
|
||||||
owner: 'Data Science Lead',
|
|
||||||
dueDate: new Date('2026-03-31'),
|
|
||||||
},
|
|
||||||
|
|
||||||
// Datenlebenszyklus
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-13',
|
|
||||||
name: 'Automatisierte Löschroutinen',
|
|
||||||
description: 'Technische Umsetzung der Aufbewahrungsfristen mit automatischer Löschung nach Fristablauf.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Datenlebenszyklus',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-13'],
|
|
||||||
owner: 'IT-Betrieb',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-14',
|
|
||||||
name: 'Löschprotokoll-Review',
|
|
||||||
description: 'Quartalsweise Überprüfung der Löschprotokolle durch den DSB.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
category: 'Datenlebenszyklus',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'MEDIUM',
|
|
||||||
evidence: ['demo-evi-14'],
|
|
||||||
owner: 'DSB',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Audit
|
|
||||||
{
|
|
||||||
id: 'demo-ctrl-15',
|
|
||||||
name: 'Umfassendes Audit-Logging',
|
|
||||||
description: 'Alle sicherheitsrelevanten Ereignisse werden manipulationssicher protokolliert und 10 Jahre aufbewahrt.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
category: 'Audit',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
effectiveness: 'HIGH',
|
|
||||||
evidence: ['demo-evi-15'],
|
|
||||||
owner: 'IT-Sicherheit',
|
|
||||||
dueDate: null,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function getDemoControls(): Control[] {
|
|
||||||
return DEMO_CONTROLS.map(ctrl => ({
|
|
||||||
...ctrl,
|
|
||||||
dueDate: ctrl.dueDate ? new Date(ctrl.dueDate) : null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo DSFA for AI Compliance SDK
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { DSFA, DSFASection, DSFAApproval } from '../types'
|
|
||||||
|
|
||||||
export const DEMO_DSFA: DSFA = {
|
|
||||||
id: 'demo-dsfa-1',
|
|
||||||
status: 'IN_REVIEW',
|
|
||||||
version: 2,
|
|
||||||
sections: [
|
|
||||||
{
|
|
||||||
id: 'dsfa-sec-1',
|
|
||||||
title: 'Systematische Beschreibung der Verarbeitungsvorgänge',
|
|
||||||
content: `## 1. Verarbeitungsbeschreibung
|
|
||||||
|
|
||||||
### 1.1 Gegenstand der Verarbeitung
|
|
||||||
Die geplante KI-gestützte Kundenanalyse verarbeitet personenbezogene Daten von Kunden und Interessenten zur Optimierung von Marketingmaßnahmen und Personalisierung von Angeboten.
|
|
||||||
|
|
||||||
### 1.2 Verarbeitungszwecke
|
|
||||||
- Kundensegmentierung basierend auf Kaufverhalten
|
|
||||||
- Churn-Prediction zur Kundenbindung
|
|
||||||
- Personalisierte Produktempfehlungen
|
|
||||||
- Optimierung von Marketing-Kampagnen
|
|
||||||
|
|
||||||
### 1.3 Kategorien personenbezogener Daten
|
|
||||||
- **Stammdaten**: Name, Adresse, E-Mail, Telefon
|
|
||||||
- **Transaktionsdaten**: Käufe, Bestellungen, Retouren
|
|
||||||
- **Nutzungsdaten**: Clickstreams, Seitenaufrufe, Verweildauer
|
|
||||||
- **Demographische Daten**: Alter, Geschlecht, PLZ-Region
|
|
||||||
|
|
||||||
### 1.4 Kategorien betroffener Personen
|
|
||||||
- Bestandskunden (ca. 250.000 aktive Kunden)
|
|
||||||
- Registrierte Interessenten (ca. 100.000)
|
|
||||||
- Newsletter-Abonnenten (ca. 180.000)
|
|
||||||
|
|
||||||
### 1.5 Rechtsgrundlage
|
|
||||||
**Primär**: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)
|
|
||||||
**Sekundär**: Art. 6 Abs. 1 lit. a DSGVO (Einwilligung für erweiterte Profiling-Maßnahmen)
|
|
||||||
|
|
||||||
Das berechtigte Interesse liegt in der Verbesserung des Kundenerlebnisses und der Effizienzsteigerung des Marketings.`,
|
|
||||||
status: 'COMPLETED',
|
|
||||||
order: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsfa-sec-2',
|
|
||||||
title: 'Bewertung der Notwendigkeit und Verhältnismäßigkeit',
|
|
||||||
content: `## 2. Notwendigkeit und Verhältnismäßigkeit
|
|
||||||
|
|
||||||
### 2.1 Notwendigkeit der Verarbeitung
|
|
||||||
Die Verarbeitung ist notwendig, um:
|
|
||||||
- Kunden individuell relevante Angebote zu unterbreiten
|
|
||||||
- Abwanderungsgefährdete Kunden frühzeitig zu identifizieren
|
|
||||||
- Marketing-Budget effizienter einzusetzen
|
|
||||||
- Wettbewerbsfähigkeit zu erhalten
|
|
||||||
|
|
||||||
### 2.2 Verhältnismäßigkeitsprüfung
|
|
||||||
|
|
||||||
**Alternative Methoden geprüft:**
|
|
||||||
1. **Manuelle Analyse**: Nicht praktikabel bei 250.000+ Kunden
|
|
||||||
2. **Regelbasierte Systeme**: Zu ungenau, führt zu höherem Datenverbrauch
|
|
||||||
3. **Aggregierte Analysen**: Keine ausreichende Personalisierung möglich
|
|
||||||
|
|
||||||
**Ergebnis**: Die KI-gestützte Analyse stellt die mildeste effektive Maßnahme dar.
|
|
||||||
|
|
||||||
### 2.3 Datensparsamkeit
|
|
||||||
- Nur für den Zweck notwendige Daten werden verarbeitet
|
|
||||||
- Sensitive Kategorien (Art. 9 DSGVO) werden ausgeschlossen
|
|
||||||
- Automatische Löschung nach definierten Fristen
|
|
||||||
|
|
||||||
### 2.4 Interessenabwägung
|
|
||||||
| Interesse des Verantwortlichen | Interesse der Betroffenen |
|
|
||||||
|-------------------------------|---------------------------|
|
|
||||||
| Effizientes Marketing | Privatsphäre |
|
|
||||||
| Kundenbindung | Keine unerwünschte Profilbildung |
|
|
||||||
| Umsatzsteigerung | Transparenz über Verarbeitung |
|
|
||||||
|
|
||||||
**Ausgleichende Maßnahmen:**
|
|
||||||
- Umfassende Informationen nach Art. 13/14 DSGVO
|
|
||||||
- Einfacher Opt-out für Profiling
|
|
||||||
- Human-Review bei kritischen Entscheidungen`,
|
|
||||||
status: 'COMPLETED',
|
|
||||||
order: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsfa-sec-3',
|
|
||||||
title: 'Risikobewertung',
|
|
||||||
content: `## 3. Risiken für Rechte und Freiheiten
|
|
||||||
|
|
||||||
### 3.1 Identifizierte Risiken
|
|
||||||
|
|
||||||
| # | Risiko | Eintritt | Schwere | Gesamt |
|
|
||||||
|---|--------|----------|---------|--------|
|
|
||||||
| R1 | Unbefugter Zugriff auf Profildaten | Mittel | Hoch | HOCH |
|
|
||||||
| R2 | Diskriminierende Entscheidungen durch Bias | Mittel | Hoch | HOCH |
|
|
||||||
| R3 | Unzulässige Profilbildung | Mittel | Mittel | MITTEL |
|
|
||||||
| R4 | Fehlende Nachvollziehbarkeit | Hoch | Mittel | MITTEL |
|
|
||||||
| R5 | Übermäßige Datensammlung | Niedrig | Mittel | NIEDRIG |
|
|
||||||
|
|
||||||
### 3.2 Detailanalyse kritischer Risiken
|
|
||||||
|
|
||||||
**R1 - Unbefugter Zugriff**
|
|
||||||
- Quelle: Externe Angreifer, Insider-Bedrohung
|
|
||||||
- Auswirkung: Identitätsdiebstahl, Reputationsschaden
|
|
||||||
- Betroffene: Alle Kunden
|
|
||||||
|
|
||||||
**R2 - Diskriminierende Entscheidungen**
|
|
||||||
- Quelle: Historische Verzerrungen in Trainingsdaten
|
|
||||||
- Auswirkung: Benachteiligung bestimmter Gruppen
|
|
||||||
- Betroffene: Potentiell alle, besonders geschützte Gruppen`,
|
|
||||||
status: 'COMPLETED',
|
|
||||||
order: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsfa-sec-4',
|
|
||||||
title: 'Maßnahmen zur Risikominderung',
|
|
||||||
content: `## 4. Abhilfemaßnahmen
|
|
||||||
|
|
||||||
### 4.1 Technische Maßnahmen
|
|
||||||
|
|
||||||
| Maßnahme | Risiko | Status | Wirksamkeit |
|
|
||||||
|----------|--------|--------|-------------|
|
|
||||||
| Multi-Faktor-Authentifizierung | R1 | ✅ Umgesetzt | Hoch |
|
|
||||||
| Verschlüsselung (AES-256) | R1 | ✅ Umgesetzt | Hoch |
|
|
||||||
| Bias-Monitoring | R2 | ✅ Umgesetzt | Mittel |
|
|
||||||
| Explainable AI | R4 | ✅ Umgesetzt | Mittel |
|
|
||||||
| Zweckbindungskontrollen | R3 | ✅ Umgesetzt | Hoch |
|
|
||||||
| Audit-Logging | R1, R4 | ✅ Umgesetzt | Hoch |
|
|
||||||
|
|
||||||
### 4.2 Organisatorische Maßnahmen
|
|
||||||
|
|
||||||
| Maßnahme | Risiko | Status | Wirksamkeit |
|
|
||||||
|----------|--------|--------|-------------|
|
|
||||||
| Rollenbasierte Zugriffskontrolle | R1 | ✅ Umgesetzt | Hoch |
|
|
||||||
| Human-in-the-Loop | R2 | ✅ Umgesetzt | Hoch |
|
|
||||||
| Datenschutz-Schulungen | R1, R3 | ✅ Umgesetzt | Mittel |
|
|
||||||
| Regelmäßige Audits | Alle | ⏳ Geplant | Hoch |
|
|
||||||
|
|
||||||
### 4.3 Restrisikobewertung
|
|
||||||
|
|
||||||
Nach Implementierung aller Maßnahmen:
|
|
||||||
- **R1**: HOCH → MITTEL (akzeptabel)
|
|
||||||
- **R2**: HOCH → MITTEL (akzeptabel)
|
|
||||||
- **R3**: MITTEL → NIEDRIG (akzeptabel)
|
|
||||||
- **R4**: MITTEL → NIEDRIG (akzeptabel)
|
|
||||||
- **R5**: NIEDRIG → NIEDRIG (akzeptabel)`,
|
|
||||||
status: 'COMPLETED',
|
|
||||||
order: 4,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsfa-sec-5',
|
|
||||||
title: 'Stellungnahme des Datenschutzbeauftragten',
|
|
||||||
content: `## 5. Stellungnahme DSB
|
|
||||||
|
|
||||||
### 5.1 Bewertung
|
|
||||||
|
|
||||||
Der Datenschutzbeauftragte hat die DSFA geprüft und kommt zu folgender Einschätzung:
|
|
||||||
|
|
||||||
**Positiv:**
|
|
||||||
- Umfassende Risikoanalyse durchgeführt
|
|
||||||
- Technische Schutzmaßnahmen dem Stand der Technik entsprechend
|
|
||||||
- Transparenzpflichten angemessen berücksichtigt
|
|
||||||
- Interessenabwägung nachvollziehbar dokumentiert
|
|
||||||
|
|
||||||
**Verbesserungspotenzial:**
|
|
||||||
- Regelmäßige Überprüfung der Bias-Metriken sollte quartalsweise erfolgen
|
|
||||||
- Informationen für Betroffene könnten noch verständlicher formuliert werden
|
|
||||||
- Löschkonzept sollte um automatische Überprüfungsmechanismen ergänzt werden
|
|
||||||
|
|
||||||
### 5.2 Empfehlung
|
|
||||||
|
|
||||||
Der DSB empfiehlt die **Genehmigung** der Verarbeitungstätigkeit unter der Voraussetzung, dass:
|
|
||||||
1. Die identifizierten Verbesserungsmaßnahmen innerhalb von 3 Monaten umgesetzt werden
|
|
||||||
2. Eine jährliche Überprüfung der DSFA erfolgt
|
|
||||||
3. Bei wesentlichen Änderungen eine Aktualisierung vorgenommen wird
|
|
||||||
|
|
||||||
---
|
|
||||||
*Datum: 2026-01-28*
|
|
||||||
*Unterschrift: [DSB]*`,
|
|
||||||
status: 'COMPLETED',
|
|
||||||
order: 5,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
approvals: [
|
|
||||||
{
|
|
||||||
id: 'dsfa-appr-1',
|
|
||||||
approver: 'Dr. Thomas Schmidt',
|
|
||||||
role: 'Datenschutzbeauftragter',
|
|
||||||
status: 'APPROVED',
|
|
||||||
comment: 'Unter den genannten Voraussetzungen genehmigt.',
|
|
||||||
approvedAt: new Date('2026-01-28'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsfa-appr-2',
|
|
||||||
approver: 'Maria Weber',
|
|
||||||
role: 'CISO',
|
|
||||||
status: 'APPROVED',
|
|
||||||
comment: 'Technische Maßnahmen sind angemessen.',
|
|
||||||
approvedAt: new Date('2026-01-29'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsfa-appr-3',
|
|
||||||
approver: 'Michael Bauer',
|
|
||||||
role: 'Geschäftsführung',
|
|
||||||
status: 'PENDING',
|
|
||||||
comment: null,
|
|
||||||
approvedAt: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
createdAt: new Date('2026-01-15'),
|
|
||||||
updatedAt: new Date('2026-02-01'),
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDemoDSFA(): DSFA {
|
|
||||||
return {
|
|
||||||
...DEMO_DSFA,
|
|
||||||
approvals: DEMO_DSFA.approvals.map(a => ({
|
|
||||||
...a,
|
|
||||||
approvedAt: a.approvedAt ? new Date(a.approvedAt) : null,
|
|
||||||
})),
|
|
||||||
createdAt: new Date(DEMO_DSFA.createdAt),
|
|
||||||
updatedAt: new Date(DEMO_DSFA.updatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,556 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo Data Seeding for AI Compliance SDK
|
|
||||||
*
|
|
||||||
* IMPORTANT: Demo data is NOT hardcoded in the frontend.
|
|
||||||
* This module provides seed data that gets stored via the API,
|
|
||||||
* exactly like real customer data would be stored.
|
|
||||||
*
|
|
||||||
* The seedDemoData() function writes data through the API,
|
|
||||||
* and the data is then loaded from the database like any other data.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SDKState } from '../types'
|
|
||||||
import { getSDKApiClient } from '../api-client'
|
|
||||||
|
|
||||||
// Seed data imports (these are templates, not runtime data)
|
|
||||||
import { getDemoUseCases, DEMO_USE_CASES } from './use-cases'
|
|
||||||
import { getDemoRisks, DEMO_RISKS } from './risks'
|
|
||||||
import { getDemoControls, DEMO_CONTROLS } from './controls'
|
|
||||||
import { getDemoDSFA, DEMO_DSFA } from './dsfa'
|
|
||||||
import { getDemoTOMs, DEMO_TOMS } from './toms'
|
|
||||||
import { getDemoProcessingActivities, getDemoRetentionPolicies, DEMO_PROCESSING_ACTIVITIES, DEMO_RETENTION_POLICIES } from './vvt'
|
|
||||||
|
|
||||||
// Re-export for direct access to seed templates (for testing/development)
|
|
||||||
export {
|
|
||||||
getDemoUseCases,
|
|
||||||
getDemoRisks,
|
|
||||||
getDemoControls,
|
|
||||||
getDemoDSFA,
|
|
||||||
getDemoTOMs,
|
|
||||||
getDemoProcessingActivities,
|
|
||||||
getDemoRetentionPolicies,
|
|
||||||
// Raw data exports
|
|
||||||
DEMO_USE_CASES,
|
|
||||||
DEMO_RISKS,
|
|
||||||
DEMO_CONTROLS,
|
|
||||||
DEMO_DSFA,
|
|
||||||
DEMO_TOMS,
|
|
||||||
DEMO_PROCESSING_ACTIVITIES,
|
|
||||||
DEMO_RETENTION_POLICIES,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a complete demo state object
|
|
||||||
* This is used as seed data for the API, not as runtime data
|
|
||||||
*/
|
|
||||||
export function generateDemoState(tenantId: string, userId: string): Partial<SDKState> {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Metadata
|
|
||||||
version: '1.0.0',
|
|
||||||
lastModified: now,
|
|
||||||
|
|
||||||
// Tenant & User
|
|
||||||
tenantId,
|
|
||||||
userId,
|
|
||||||
subscription: 'PROFESSIONAL',
|
|
||||||
|
|
||||||
// Customer Type
|
|
||||||
customerType: 'new',
|
|
||||||
|
|
||||||
// Company Profile (Demo: TechStart GmbH - SaaS-Startup aus Berlin)
|
|
||||||
companyProfile: {
|
|
||||||
companyName: 'TechStart GmbH',
|
|
||||||
legalForm: 'gmbh',
|
|
||||||
industry: 'Technologie / IT',
|
|
||||||
foundedYear: 2022,
|
|
||||||
businessModel: 'B2B_B2C',
|
|
||||||
offerings: ['app_web', 'software_saas', 'services_consulting'],
|
|
||||||
companySize: 'small',
|
|
||||||
employeeCount: '10-49',
|
|
||||||
annualRevenue: '2-10 Mio',
|
|
||||||
headquartersCountry: 'DE',
|
|
||||||
headquartersCity: 'Berlin',
|
|
||||||
hasInternationalLocations: false,
|
|
||||||
internationalCountries: [],
|
|
||||||
targetMarkets: ['germany_only', 'dach'],
|
|
||||||
primaryJurisdiction: 'DE',
|
|
||||||
isDataController: true,
|
|
||||||
isDataProcessor: true,
|
|
||||||
usesAI: true,
|
|
||||||
aiUseCases: ['KI-gestützte Kundenberatung', 'Automatisierte Dokumentenanalyse'],
|
|
||||||
dpoName: 'Max Mustermann',
|
|
||||||
dpoEmail: 'dsb@techstart.de',
|
|
||||||
legalContactName: null,
|
|
||||||
legalContactEmail: null,
|
|
||||||
isComplete: true,
|
|
||||||
completedAt: new Date('2026-01-14'),
|
|
||||||
},
|
|
||||||
|
|
||||||
// Progress - showing a realistic partially completed workflow
|
|
||||||
currentPhase: 2,
|
|
||||||
currentStep: 'tom',
|
|
||||||
completedSteps: [
|
|
||||||
'company-profile',
|
|
||||||
'use-case-assessment',
|
|
||||||
'screening',
|
|
||||||
'modules',
|
|
||||||
'requirements',
|
|
||||||
'controls',
|
|
||||||
'evidence',
|
|
||||||
'audit-checklist',
|
|
||||||
'risks',
|
|
||||||
'ai-act',
|
|
||||||
'obligations',
|
|
||||||
'dsfa',
|
|
||||||
],
|
|
||||||
checkpoints: {
|
|
||||||
'CP-PROF': { checkpointId: 'CP-PROF', passed: true, validatedAt: new Date('2026-01-14'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-UC': { checkpointId: 'CP-UC', passed: true, validatedAt: new Date('2026-01-15'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-SCAN': { checkpointId: 'CP-SCAN', passed: true, validatedAt: new Date('2026-01-16'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-MOD': { checkpointId: 'CP-MOD', passed: true, validatedAt: new Date('2026-01-17'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-REQ': { checkpointId: 'CP-REQ', passed: true, validatedAt: new Date('2026-01-18'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-CTRL': { checkpointId: 'CP-CTRL', passed: true, validatedAt: new Date('2026-01-19'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-EVI': { checkpointId: 'CP-EVI', passed: true, validatedAt: new Date('2026-01-20'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-CHK': { checkpointId: 'CP-CHK', passed: true, validatedAt: new Date('2026-01-21'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-RISK': { checkpointId: 'CP-RISK', passed: true, validatedAt: new Date('2026-01-22'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-AI': { checkpointId: 'CP-AI', passed: true, validatedAt: new Date('2026-01-25'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-OBL': { checkpointId: 'CP-OBL', passed: true, validatedAt: new Date('2026-01-27'), validatedBy: 'demo-user', errors: [], warnings: [] },
|
|
||||||
'CP-DSFA': { checkpointId: 'CP-DSFA', passed: true, validatedAt: new Date('2026-01-30'), validatedBy: 'DSB', errors: [], warnings: [] },
|
|
||||||
},
|
|
||||||
|
|
||||||
// Phase 1 Data
|
|
||||||
useCases: getDemoUseCases(),
|
|
||||||
activeUseCase: 'demo-uc-1',
|
|
||||||
screening: {
|
|
||||||
id: 'demo-scan-1',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
startedAt: new Date('2026-01-16T09:00:00'),
|
|
||||||
completedAt: new Date('2026-01-16T09:15:00'),
|
|
||||||
sbom: {
|
|
||||||
format: 'CycloneDX',
|
|
||||||
version: '1.4',
|
|
||||||
components: [
|
|
||||||
{
|
|
||||||
name: 'tensorflow',
|
|
||||||
version: '2.15.0',
|
|
||||||
type: 'library',
|
|
||||||
purl: 'pkg:pypi/tensorflow@2.15.0',
|
|
||||||
licenses: ['Apache-2.0'],
|
|
||||||
vulnerabilities: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'scikit-learn',
|
|
||||||
version: '1.4.0',
|
|
||||||
type: 'library',
|
|
||||||
purl: 'pkg:pypi/scikit-learn@1.4.0',
|
|
||||||
licenses: ['BSD-3-Clause'],
|
|
||||||
vulnerabilities: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'pandas',
|
|
||||||
version: '2.2.0',
|
|
||||||
type: 'library',
|
|
||||||
purl: 'pkg:pypi/pandas@2.2.0',
|
|
||||||
licenses: ['BSD-3-Clause'],
|
|
||||||
vulnerabilities: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dependencies: [],
|
|
||||||
generatedAt: new Date('2026-01-16T09:10:00'),
|
|
||||||
},
|
|
||||||
securityScan: {
|
|
||||||
totalIssues: 3,
|
|
||||||
critical: 0,
|
|
||||||
high: 1,
|
|
||||||
medium: 1,
|
|
||||||
low: 1,
|
|
||||||
issues: [
|
|
||||||
{
|
|
||||||
id: 'sec-issue-1',
|
|
||||||
severity: 'HIGH',
|
|
||||||
title: 'Outdated cryptography library',
|
|
||||||
description: 'The cryptography library version 41.0.0 has known vulnerabilities',
|
|
||||||
cve: 'CVE-2024-1234',
|
|
||||||
cvss: 7.5,
|
|
||||||
affectedComponent: 'cryptography',
|
|
||||||
remediation: 'Upgrade to cryptography >= 42.0.0',
|
|
||||||
status: 'RESOLVED',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sec-issue-2',
|
|
||||||
severity: 'MEDIUM',
|
|
||||||
title: 'Insecure default configuration',
|
|
||||||
description: 'Debug mode enabled in production configuration',
|
|
||||||
cve: null,
|
|
||||||
cvss: 5.3,
|
|
||||||
affectedComponent: 'app-config',
|
|
||||||
remediation: 'Set DEBUG=false in production',
|
|
||||||
status: 'RESOLVED',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sec-issue-3',
|
|
||||||
severity: 'LOW',
|
|
||||||
title: 'Missing security headers',
|
|
||||||
description: 'X-Content-Type-Options header not set',
|
|
||||||
cve: null,
|
|
||||||
cvss: 3.1,
|
|
||||||
affectedComponent: 'web-server',
|
|
||||||
remediation: 'Add security headers middleware',
|
|
||||||
status: 'RESOLVED',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
error: null,
|
|
||||||
},
|
|
||||||
modules: [
|
|
||||||
{
|
|
||||||
id: 'demo-mod-1',
|
|
||||||
name: 'Kundendaten-Modul',
|
|
||||||
description: 'Verarbeitung von Kundendaten für Marketing und Analyse',
|
|
||||||
regulations: ['DSGVO', 'TTDSG'],
|
|
||||||
criticality: 'HIGH',
|
|
||||||
processesPersonalData: true,
|
|
||||||
hasAIComponents: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mod-2',
|
|
||||||
name: 'HR-Modul',
|
|
||||||
description: 'Bewerbermanagement und Personalverwaltung',
|
|
||||||
regulations: ['DSGVO', 'AGG', 'AI Act'],
|
|
||||||
criticality: 'HIGH',
|
|
||||||
processesPersonalData: true,
|
|
||||||
hasAIComponents: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mod-3',
|
|
||||||
name: 'Support-Modul',
|
|
||||||
description: 'Kundenservice und Chatbot-System',
|
|
||||||
regulations: ['DSGVO', 'AI Act'],
|
|
||||||
criticality: 'MEDIUM',
|
|
||||||
processesPersonalData: true,
|
|
||||||
hasAIComponents: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
requirements: [
|
|
||||||
{
|
|
||||||
id: 'demo-req-1',
|
|
||||||
regulation: 'DSGVO',
|
|
||||||
article: 'Art. 5',
|
|
||||||
title: 'Grundsätze der Verarbeitung',
|
|
||||||
description: 'Einhaltung der Grundsätze für die Verarbeitung personenbezogener Daten',
|
|
||||||
criticality: 'CRITICAL',
|
|
||||||
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
|
|
||||||
status: 'IMPLEMENTED',
|
|
||||||
controls: ['demo-ctrl-1', 'demo-ctrl-2', 'demo-ctrl-9'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-req-2',
|
|
||||||
regulation: 'DSGVO',
|
|
||||||
article: 'Art. 32',
|
|
||||||
title: 'Sicherheit der Verarbeitung',
|
|
||||||
description: 'Geeignete technische und organisatorische Maßnahmen',
|
|
||||||
criticality: 'CRITICAL',
|
|
||||||
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
|
|
||||||
status: 'IMPLEMENTED',
|
|
||||||
controls: ['demo-ctrl-1', 'demo-ctrl-3', 'demo-ctrl-4'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-req-3',
|
|
||||||
regulation: 'DSGVO',
|
|
||||||
article: 'Art. 25',
|
|
||||||
title: 'Datenschutz durch Technikgestaltung',
|
|
||||||
description: 'Privacy by Design und Privacy by Default',
|
|
||||||
criticality: 'HIGH',
|
|
||||||
applicableModules: ['demo-mod-1', 'demo-mod-2'],
|
|
||||||
status: 'IMPLEMENTED',
|
|
||||||
controls: ['demo-ctrl-9', 'demo-ctrl-10'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-req-4',
|
|
||||||
regulation: 'AI Act',
|
|
||||||
article: 'Art. 13',
|
|
||||||
title: 'Transparenz',
|
|
||||||
description: 'Transparenzanforderungen für KI-Systeme',
|
|
||||||
criticality: 'HIGH',
|
|
||||||
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
|
|
||||||
status: 'IMPLEMENTED',
|
|
||||||
controls: ['demo-ctrl-7', 'demo-ctrl-8'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-req-5',
|
|
||||||
regulation: 'AI Act',
|
|
||||||
article: 'Art. 9',
|
|
||||||
title: 'Risikomanagement',
|
|
||||||
description: 'Risikomanagementsystem für Hochrisiko-KI',
|
|
||||||
criticality: 'HIGH',
|
|
||||||
applicableModules: ['demo-mod-2'],
|
|
||||||
status: 'IMPLEMENTED',
|
|
||||||
controls: ['demo-ctrl-5', 'demo-ctrl-6', 'demo-ctrl-11', 'demo-ctrl-12'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
controls: getDemoControls(),
|
|
||||||
evidence: [
|
|
||||||
{
|
|
||||||
id: 'demo-evi-1',
|
|
||||||
controlId: 'demo-ctrl-1',
|
|
||||||
type: 'SCREENSHOT',
|
|
||||||
name: 'MFA-Konfiguration Azure AD',
|
|
||||||
description: 'Screenshot der MFA-Einstellungen im Azure AD Admin Portal',
|
|
||||||
fileUrl: null,
|
|
||||||
validFrom: new Date('2026-01-01'),
|
|
||||||
validUntil: new Date('2027-01-01'),
|
|
||||||
uploadedBy: 'IT-Security',
|
|
||||||
uploadedAt: new Date('2026-01-10'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-evi-2',
|
|
||||||
controlId: 'demo-ctrl-2',
|
|
||||||
type: 'DOCUMENT',
|
|
||||||
name: 'Berechtigungskonzept v2.1',
|
|
||||||
description: 'Dokumentiertes Berechtigungskonzept mit Rollenmatrix',
|
|
||||||
fileUrl: null,
|
|
||||||
validFrom: new Date('2026-01-01'),
|
|
||||||
validUntil: null,
|
|
||||||
uploadedBy: 'IT-Security',
|
|
||||||
uploadedAt: new Date('2026-01-05'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-evi-5',
|
|
||||||
controlId: 'demo-ctrl-5',
|
|
||||||
type: 'AUDIT_REPORT',
|
|
||||||
name: 'Bias-Audit Q1/2026',
|
|
||||||
description: 'Externer Audit-Bericht zur Fairness des KI-Modells',
|
|
||||||
fileUrl: null,
|
|
||||||
validFrom: new Date('2026-01-15'),
|
|
||||||
validUntil: new Date('2026-04-15'),
|
|
||||||
uploadedBy: 'Data Science Lead',
|
|
||||||
uploadedAt: new Date('2026-01-20'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
checklist: [
|
|
||||||
{
|
|
||||||
id: 'demo-chk-1',
|
|
||||||
requirementId: 'demo-req-1',
|
|
||||||
title: 'Rechtmäßigkeit der Verarbeitung geprüft',
|
|
||||||
description: 'Dokumentierte Prüfung der Rechtsgrundlagen',
|
|
||||||
status: 'PASSED',
|
|
||||||
notes: 'Geprüft durch DSB',
|
|
||||||
verifiedBy: 'DSB',
|
|
||||||
verifiedAt: new Date('2026-01-20'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-chk-2',
|
|
||||||
requirementId: 'demo-req-2',
|
|
||||||
title: 'TOMs dokumentiert und umgesetzt',
|
|
||||||
description: 'Technische und organisatorische Maßnahmen',
|
|
||||||
status: 'PASSED',
|
|
||||||
notes: 'Alle TOMs implementiert',
|
|
||||||
verifiedBy: 'CISO',
|
|
||||||
verifiedAt: new Date('2026-01-21'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
risks: getDemoRisks(),
|
|
||||||
|
|
||||||
// Phase 2 Data
|
|
||||||
aiActClassification: {
|
|
||||||
riskCategory: 'HIGH',
|
|
||||||
systemType: 'Beschäftigungsbezogenes KI-System (Art. 6 Abs. 2 AI Act)',
|
|
||||||
obligations: [
|
|
||||||
{
|
|
||||||
id: 'demo-ai-obl-1',
|
|
||||||
article: 'Art. 9',
|
|
||||||
title: 'Risikomanagementsystem',
|
|
||||||
description: 'Einrichtung eines KI-Risikomanagementsystems',
|
|
||||||
deadline: new Date('2026-08-01'),
|
|
||||||
status: 'IN_PROGRESS',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ai-obl-2',
|
|
||||||
article: 'Art. 10',
|
|
||||||
title: 'Daten-Governance',
|
|
||||||
description: 'Anforderungen an Trainingsdaten',
|
|
||||||
deadline: new Date('2026-08-01'),
|
|
||||||
status: 'COMPLETED',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ai-obl-3',
|
|
||||||
article: 'Art. 13',
|
|
||||||
title: 'Transparenz',
|
|
||||||
description: 'Dokumentation für Nutzer',
|
|
||||||
deadline: new Date('2026-08-01'),
|
|
||||||
status: 'COMPLETED',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
assessmentDate: new Date('2026-01-25'),
|
|
||||||
assessedBy: 'Compliance Team',
|
|
||||||
justification: 'Das System fällt unter Art. 6 Abs. 2 lit. a AI Act (Einstellung und Auswahl von Personen).',
|
|
||||||
},
|
|
||||||
obligations: [
|
|
||||||
{
|
|
||||||
id: 'demo-obl-1',
|
|
||||||
regulation: 'DSGVO',
|
|
||||||
article: 'Art. 30',
|
|
||||||
title: 'Verarbeitungsverzeichnis',
|
|
||||||
description: 'Führung eines Verzeichnisses der Verarbeitungstätigkeiten',
|
|
||||||
deadline: null,
|
|
||||||
penalty: 'Bis zu 10 Mio. EUR oder 2% des Jahresumsatzes',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
responsible: 'DSB',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-obl-2',
|
|
||||||
regulation: 'DSGVO',
|
|
||||||
article: 'Art. 35',
|
|
||||||
title: 'Datenschutz-Folgenabschätzung',
|
|
||||||
description: 'Durchführung einer DSFA für Hochrisiko-Verarbeitungen',
|
|
||||||
deadline: null,
|
|
||||||
penalty: 'Bis zu 10 Mio. EUR oder 2% des Jahresumsatzes',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
responsible: 'DSB',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-obl-3',
|
|
||||||
regulation: 'AI Act',
|
|
||||||
article: 'Art. 49',
|
|
||||||
title: 'CE-Kennzeichnung',
|
|
||||||
description: 'CE-Kennzeichnung für Hochrisiko-KI-Systeme',
|
|
||||||
deadline: new Date('2026-08-01'),
|
|
||||||
penalty: 'Bis zu 35 Mio. EUR oder 7% des Jahresumsatzes',
|
|
||||||
status: 'PENDING',
|
|
||||||
responsible: 'Compliance',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
dsfa: getDemoDSFA(),
|
|
||||||
toms: getDemoTOMs(),
|
|
||||||
retentionPolicies: getDemoRetentionPolicies(),
|
|
||||||
vvt: getDemoProcessingActivities(),
|
|
||||||
|
|
||||||
// Documents, Cookie Banner, etc. - partially filled
|
|
||||||
documents: [],
|
|
||||||
cookieBanner: null,
|
|
||||||
consents: [],
|
|
||||||
dsrConfig: null,
|
|
||||||
escalationWorkflows: [],
|
|
||||||
|
|
||||||
// Security
|
|
||||||
sbom: null,
|
|
||||||
securityIssues: [],
|
|
||||||
securityBacklog: [],
|
|
||||||
|
|
||||||
// UI State
|
|
||||||
commandBarHistory: [],
|
|
||||||
recentSearches: ['DSGVO Art. 5', 'Bias-Monitoring', 'TOM Verschlüsselung'],
|
|
||||||
preferences: {
|
|
||||||
language: 'de',
|
|
||||||
theme: 'light',
|
|
||||||
compactMode: false,
|
|
||||||
showHints: true,
|
|
||||||
autoSave: true,
|
|
||||||
autoValidate: true,
|
|
||||||
allowParallelWork: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seed demo data into the database via API
|
|
||||||
* This ensures demo data is stored exactly like real customer data
|
|
||||||
*/
|
|
||||||
export async function seedDemoData(
|
|
||||||
tenantId: string = 'demo-tenant',
|
|
||||||
userId: string = 'demo-user',
|
|
||||||
apiBaseUrl?: string
|
|
||||||
): Promise<{ success: boolean; message: string }> {
|
|
||||||
try {
|
|
||||||
const apiClient = getSDKApiClient(tenantId)
|
|
||||||
|
|
||||||
// Generate the demo state
|
|
||||||
const demoState = generateDemoState(tenantId, userId) as SDKState
|
|
||||||
|
|
||||||
// Save via the same API that real data uses
|
|
||||||
await apiClient.saveState(demoState)
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `Demo data successfully seeded for tenant ${tenantId}`,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to seed demo data:', error)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: error instanceof Error ? error.message : 'Unknown error during seeding',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if demo data exists for a tenant
|
|
||||||
*/
|
|
||||||
export async function hasDemoData(tenantId: string = 'demo-tenant'): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const apiClient = getSDKApiClient(tenantId)
|
|
||||||
const response = await apiClient.getState()
|
|
||||||
|
|
||||||
// Check if we have any use cases (indicating data exists)
|
|
||||||
return response !== null && response.state && Array.isArray(response.state.useCases) && response.state.useCases.length > 0
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear demo data for a tenant
|
|
||||||
*/
|
|
||||||
export async function clearDemoData(tenantId: string = 'demo-tenant'): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const apiClient = getSDKApiClient(tenantId)
|
|
||||||
await apiClient.deleteState()
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seed demo data via direct API call (for use outside of React context)
|
|
||||||
* This is useful for server-side seeding or CLI tools
|
|
||||||
*/
|
|
||||||
export async function seedDemoDataDirect(
|
|
||||||
baseUrl: string,
|
|
||||||
tenantId: string = 'demo-tenant',
|
|
||||||
userId: string = 'demo-user'
|
|
||||||
): Promise<{ success: boolean; message: string }> {
|
|
||||||
try {
|
|
||||||
const demoState = generateDemoState(tenantId, userId)
|
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/api/sdk/v1/state`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
tenantId,
|
|
||||||
userId,
|
|
||||||
state: demoState,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
|
|
||||||
throw new Error(error.message || `HTTP ${response.status}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: `Demo data successfully seeded for tenant ${tenantId}`,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to seed demo data:', error)
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: error instanceof Error ? error.message : 'Unknown error during seeding',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo Risks for AI Compliance SDK
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Risk, RiskMitigation } from '../types'
|
|
||||||
|
|
||||||
export const DEMO_RISKS: Risk[] = [
|
|
||||||
{
|
|
||||||
id: 'demo-risk-1',
|
|
||||||
title: 'Unbefugter Zugriff auf personenbezogene Daten',
|
|
||||||
description: 'Risiko des unbefugten Zugriffs auf Kundendaten durch externe Angreifer oder interne Mitarbeiter ohne entsprechende Berechtigung.',
|
|
||||||
category: 'Datensicherheit',
|
|
||||||
likelihood: 3,
|
|
||||||
impact: 5,
|
|
||||||
severity: 'CRITICAL',
|
|
||||||
inherentRiskScore: 15,
|
|
||||||
residualRiskScore: 6,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'demo-mit-1a',
|
|
||||||
description: 'Implementierung von Multi-Faktor-Authentifizierung für alle Systemzugriffe',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 40,
|
|
||||||
controlId: 'demo-ctrl-1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mit-1b',
|
|
||||||
description: 'Rollenbasiertes Zugriffskonzept mit Least-Privilege-Prinzip',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 30,
|
|
||||||
controlId: 'demo-ctrl-2',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'CISO',
|
|
||||||
relatedControls: ['demo-ctrl-1', 'demo-ctrl-2'],
|
|
||||||
relatedRequirements: ['demo-req-1', 'demo-req-2'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-risk-2',
|
|
||||||
title: 'KI-Bias bei automatisierten Entscheidungen',
|
|
||||||
description: 'Das KI-System könnte systematische Verzerrungen aufweisen, die zu diskriminierenden Entscheidungen führen, insbesondere bei der Bewerbungsvorauswahl.',
|
|
||||||
category: 'KI-Ethik',
|
|
||||||
likelihood: 4,
|
|
||||||
impact: 4,
|
|
||||||
severity: 'HIGH',
|
|
||||||
inherentRiskScore: 16,
|
|
||||||
residualRiskScore: 8,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'demo-mit-2a',
|
|
||||||
description: 'Regelmäßiges Bias-Monitoring mit Fairness-Metriken',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 30,
|
|
||||||
controlId: 'demo-ctrl-5',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mit-2b',
|
|
||||||
description: 'Human-in-the-Loop bei kritischen Entscheidungen',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 25,
|
|
||||||
controlId: 'demo-ctrl-6',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'Data Science Lead',
|
|
||||||
relatedControls: ['demo-ctrl-5', 'demo-ctrl-6'],
|
|
||||||
relatedRequirements: ['demo-req-5', 'demo-req-6'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-risk-3',
|
|
||||||
title: 'Datenverlust durch Systemausfall',
|
|
||||||
description: 'Verlust von Kundendaten und KI-Modellen durch Hardware-Defekte, Softwarefehler oder Naturkatastrophen.',
|
|
||||||
category: 'Verfügbarkeit',
|
|
||||||
likelihood: 2,
|
|
||||||
impact: 5,
|
|
||||||
severity: 'HIGH',
|
|
||||||
inherentRiskScore: 10,
|
|
||||||
residualRiskScore: 3,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'demo-mit-3a',
|
|
||||||
description: 'Tägliche inkrementelle und wöchentliche Vollbackups',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 40,
|
|
||||||
controlId: 'demo-ctrl-3',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mit-3b',
|
|
||||||
description: 'Georedundante Datenspeicherung in zwei Rechenzentren',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 35,
|
|
||||||
controlId: 'demo-ctrl-4',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'IT-Leiter',
|
|
||||||
relatedControls: ['demo-ctrl-3', 'demo-ctrl-4'],
|
|
||||||
relatedRequirements: ['demo-req-3'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-risk-4',
|
|
||||||
title: 'Unzureichende Transparenz bei KI-Entscheidungen',
|
|
||||||
description: 'Betroffene verstehen nicht, wie KI-Entscheidungen zustande kommen, was zu Beschwerden und regulatorischen Problemen führen kann.',
|
|
||||||
category: 'Transparenz',
|
|
||||||
likelihood: 4,
|
|
||||||
impact: 3,
|
|
||||||
severity: 'MEDIUM',
|
|
||||||
inherentRiskScore: 12,
|
|
||||||
residualRiskScore: 4,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'demo-mit-4a',
|
|
||||||
description: 'Explainable AI Komponenten für nachvollziehbare Entscheidungen',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 40,
|
|
||||||
controlId: 'demo-ctrl-7',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mit-4b',
|
|
||||||
description: 'Verständliche Informationen für Betroffene gem. Art. 13-14 DSGVO',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 30,
|
|
||||||
controlId: 'demo-ctrl-8',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'DSB',
|
|
||||||
relatedControls: ['demo-ctrl-7', 'demo-ctrl-8'],
|
|
||||||
relatedRequirements: ['demo-req-4'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-risk-5',
|
|
||||||
title: 'Unerlaubte Profilbildung',
|
|
||||||
description: 'Durch die Zusammenführung verschiedener Datenquellen könnte eine unzulässige umfassende Profilbildung von Personen entstehen.',
|
|
||||||
category: 'Datenschutz',
|
|
||||||
likelihood: 3,
|
|
||||||
impact: 4,
|
|
||||||
severity: 'HIGH',
|
|
||||||
inherentRiskScore: 12,
|
|
||||||
residualRiskScore: 6,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'demo-mit-5a',
|
|
||||||
description: 'Strenge Zweckbindung der Datenverarbeitung',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 25,
|
|
||||||
controlId: 'demo-ctrl-9',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mit-5b',
|
|
||||||
description: 'Datensparsamkeit durch Aggregation und Anonymisierung',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 30,
|
|
||||||
controlId: 'demo-ctrl-10',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'DSB',
|
|
||||||
relatedControls: ['demo-ctrl-9', 'demo-ctrl-10'],
|
|
||||||
relatedRequirements: ['demo-req-7', 'demo-req-8'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-risk-6',
|
|
||||||
title: 'Mangelnde Modell-Robustheit',
|
|
||||||
description: 'KI-Modelle könnten durch Adversarial Attacks oder veränderte Inputdaten manipuliert werden und falsche Ergebnisse liefern.',
|
|
||||||
category: 'KI-Sicherheit',
|
|
||||||
likelihood: 2,
|
|
||||||
impact: 4,
|
|
||||||
severity: 'MEDIUM',
|
|
||||||
inherentRiskScore: 8,
|
|
||||||
residualRiskScore: 4,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'demo-mit-6a',
|
|
||||||
description: 'Input-Validierung und Anomalie-Erkennung',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 30,
|
|
||||||
controlId: 'demo-ctrl-11',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mit-6b',
|
|
||||||
description: 'Regelmäßige Modell-Retraining und Performance-Monitoring',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'IN_PROGRESS',
|
|
||||||
effectiveness: 20,
|
|
||||||
controlId: 'demo-ctrl-12',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'Data Science Lead',
|
|
||||||
relatedControls: ['demo-ctrl-11', 'demo-ctrl-12'],
|
|
||||||
relatedRequirements: ['demo-req-9'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-risk-7',
|
|
||||||
title: 'Verstoß gegen Aufbewahrungsfristen',
|
|
||||||
description: 'Daten werden länger als zulässig gespeichert oder zu früh gelöscht, was zu Compliance-Verstößen führt.',
|
|
||||||
category: 'Datenschutz',
|
|
||||||
likelihood: 3,
|
|
||||||
impact: 3,
|
|
||||||
severity: 'MEDIUM',
|
|
||||||
inherentRiskScore: 9,
|
|
||||||
residualRiskScore: 3,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'demo-mit-7a',
|
|
||||||
description: 'Automatisierte Löschroutinen mit Retention-Policy-Enforcement',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 40,
|
|
||||||
controlId: 'demo-ctrl-13',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-mit-7b',
|
|
||||||
description: 'Quartalsmäßige Überprüfung der Löschprotokolle',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 25,
|
|
||||||
controlId: 'demo-ctrl-14',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'DSB',
|
|
||||||
relatedControls: ['demo-ctrl-13', 'demo-ctrl-14'],
|
|
||||||
relatedRequirements: ['demo-req-10'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-risk-8',
|
|
||||||
title: 'Fehlende Nachvollziehbarkeit im Audit',
|
|
||||||
description: 'Bei Prüfungen können Verarbeitungsvorgänge nicht lückenlos nachvollzogen werden.',
|
|
||||||
category: 'Compliance',
|
|
||||||
likelihood: 2,
|
|
||||||
impact: 3,
|
|
||||||
severity: 'MEDIUM',
|
|
||||||
inherentRiskScore: 6,
|
|
||||||
residualRiskScore: 2,
|
|
||||||
status: 'MITIGATED',
|
|
||||||
mitigation: [
|
|
||||||
{
|
|
||||||
id: 'demo-mit-8a',
|
|
||||||
description: 'Umfassendes Audit-Logging aller Verarbeitungsvorgänge',
|
|
||||||
type: 'MITIGATE',
|
|
||||||
status: 'COMPLETED',
|
|
||||||
effectiveness: 50,
|
|
||||||
controlId: 'demo-ctrl-15',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
owner: 'IT-Leiter',
|
|
||||||
relatedControls: ['demo-ctrl-15'],
|
|
||||||
relatedRequirements: ['demo-req-11'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function getDemoRisks(): Risk[] {
|
|
||||||
return DEMO_RISKS
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo TOMs (Technical & Organizational Measures) for AI Compliance SDK
|
|
||||||
* These are seed data structures - actual data is stored in database
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { TOM } from '../types'
|
|
||||||
|
|
||||||
export const DEMO_TOMS: TOM[] = [
|
|
||||||
// Zugangskontrolle
|
|
||||||
{
|
|
||||||
id: 'demo-tom-1',
|
|
||||||
category: 'Zugangskontrolle',
|
|
||||||
name: 'Physische Zutrittskontrolle',
|
|
||||||
description: 'Elektronische Zugangskontrollsysteme mit personenbezogenen Zutrittskarten für alle Serverräume und Rechenzentren. Protokollierung aller Zutritte.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'Facility Management',
|
|
||||||
implementationDate: new Date('2025-06-01'),
|
|
||||||
reviewDate: new Date('2026-06-01'),
|
|
||||||
evidence: ['demo-evi-tom-1'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-2',
|
|
||||||
category: 'Zugangskontrolle',
|
|
||||||
name: 'Besuchermanagement',
|
|
||||||
description: 'Registrierung aller Besucher mit Identitätsprüfung, Ausgabe von Besucherausweisen und permanente Begleitung in sicherheitsrelevanten Bereichen.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'MEDIUM',
|
|
||||||
responsiblePerson: 'Empfang/Security',
|
|
||||||
implementationDate: new Date('2025-03-15'),
|
|
||||||
reviewDate: new Date('2026-03-15'),
|
|
||||||
evidence: ['demo-evi-tom-2'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Zugriffskontrolle
|
|
||||||
{
|
|
||||||
id: 'demo-tom-3',
|
|
||||||
category: 'Zugriffskontrolle',
|
|
||||||
name: 'Identity & Access Management (IAM)',
|
|
||||||
description: 'Zentrales IAM-System mit automatischer Provisionierung, Deprovisionierung und regelmäßiger Rezertifizierung aller Benutzerkonten.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'CRITICAL',
|
|
||||||
responsiblePerson: 'IT-Sicherheit',
|
|
||||||
implementationDate: new Date('2025-01-01'),
|
|
||||||
reviewDate: new Date('2026-01-01'),
|
|
||||||
evidence: ['demo-evi-tom-3'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-4',
|
|
||||||
category: 'Zugriffskontrolle',
|
|
||||||
name: 'Privileged Access Management (PAM)',
|
|
||||||
description: 'Spezielles Management für administrative Zugänge mit Session-Recording, automatischer Passwortrotation und Just-in-Time-Berechtigungen.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'CRITICAL',
|
|
||||||
responsiblePerson: 'IT-Sicherheit',
|
|
||||||
implementationDate: new Date('2025-04-01'),
|
|
||||||
reviewDate: new Date('2026-04-01'),
|
|
||||||
evidence: ['demo-evi-tom-4'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-5',
|
|
||||||
category: 'Zugriffskontrolle',
|
|
||||||
name: 'Berechtigungskonzept-Review',
|
|
||||||
description: 'Halbjährliche Überprüfung aller Berechtigungen durch die jeweiligen Fachbereichsleiter mit dokumentierter Rezertifizierung.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'Fachbereichsleiter',
|
|
||||||
implementationDate: new Date('2025-02-01'),
|
|
||||||
reviewDate: new Date('2026-02-01'),
|
|
||||||
evidence: ['demo-evi-tom-5'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Verschlüsselung
|
|
||||||
{
|
|
||||||
id: 'demo-tom-6',
|
|
||||||
category: 'Verschlüsselung',
|
|
||||||
name: 'Datenverschlüsselung at Rest',
|
|
||||||
description: 'AES-256 Verschlüsselung aller personenbezogenen Daten in Datenbanken und Dateisystemen. Key Management über HSM.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'CRITICAL',
|
|
||||||
responsiblePerson: 'IT-Sicherheit',
|
|
||||||
implementationDate: new Date('2025-01-15'),
|
|
||||||
reviewDate: new Date('2026-01-15'),
|
|
||||||
evidence: ['demo-evi-tom-6'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-7',
|
|
||||||
category: 'Verschlüsselung',
|
|
||||||
name: 'Transportverschlüsselung',
|
|
||||||
description: 'TLS 1.3 für alle externen Verbindungen, mTLS für interne Service-Kommunikation. Regelmäßige Überprüfung der Cipher Suites.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'CRITICAL',
|
|
||||||
responsiblePerson: 'IT-Sicherheit',
|
|
||||||
implementationDate: new Date('2025-01-01'),
|
|
||||||
reviewDate: new Date('2026-01-01'),
|
|
||||||
evidence: ['demo-evi-tom-7'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Pseudonymisierung
|
|
||||||
{
|
|
||||||
id: 'demo-tom-8',
|
|
||||||
category: 'Pseudonymisierung',
|
|
||||||
name: 'Pseudonymisierungs-Pipeline',
|
|
||||||
description: 'Automatisierte Pseudonymisierung von Daten vor der Verarbeitung in Analytics-Systemen. Reversible Zuordnung nur durch autorisierten Prozess.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'Data Engineering',
|
|
||||||
implementationDate: new Date('2025-05-01'),
|
|
||||||
reviewDate: new Date('2026-05-01'),
|
|
||||||
evidence: ['demo-evi-tom-8'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Integrität
|
|
||||||
{
|
|
||||||
id: 'demo-tom-9',
|
|
||||||
category: 'Integrität',
|
|
||||||
name: 'Datenintegritätsprüfung',
|
|
||||||
description: 'Checksummen-Validierung bei allen Datentransfers, Hash-Verifikation gespeicherter Daten, automatische Alerts bei Abweichungen.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'IT-Betrieb',
|
|
||||||
implementationDate: new Date('2025-03-01'),
|
|
||||||
reviewDate: new Date('2026-03-01'),
|
|
||||||
evidence: ['demo-evi-tom-9'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-10',
|
|
||||||
category: 'Integrität',
|
|
||||||
name: 'Change Management',
|
|
||||||
description: 'Dokumentierter Change-Prozess mit Vier-Augen-Prinzip für alle Änderungen an produktiven Systemen. CAB-Freigabe für kritische Changes.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'IT-Leitung',
|
|
||||||
implementationDate: new Date('2025-01-01'),
|
|
||||||
reviewDate: new Date('2026-01-01'),
|
|
||||||
evidence: ['demo-evi-tom-10'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Verfügbarkeit
|
|
||||||
{
|
|
||||||
id: 'demo-tom-11',
|
|
||||||
category: 'Verfügbarkeit',
|
|
||||||
name: 'Disaster Recovery Plan',
|
|
||||||
description: 'Dokumentierter und getesteter DR-Plan mit RTO <4h und RPO <1h. Jährliche DR-Tests mit Dokumentation.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'CRITICAL',
|
|
||||||
responsiblePerson: 'IT-Leitung',
|
|
||||||
implementationDate: new Date('2025-02-01'),
|
|
||||||
reviewDate: new Date('2026-02-01'),
|
|
||||||
evidence: ['demo-evi-tom-11'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-12',
|
|
||||||
category: 'Verfügbarkeit',
|
|
||||||
name: 'High Availability Cluster',
|
|
||||||
description: 'Aktiv-Aktiv-Cluster für alle kritischen Systeme mit automatischem Failover. 99,9% Verfügbarkeits-SLA.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'CRITICAL',
|
|
||||||
responsiblePerson: 'IT-Betrieb',
|
|
||||||
implementationDate: new Date('2025-01-01'),
|
|
||||||
reviewDate: new Date('2026-01-01'),
|
|
||||||
evidence: ['demo-evi-tom-12'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Belastbarkeit
|
|
||||||
{
|
|
||||||
id: 'demo-tom-13',
|
|
||||||
category: 'Belastbarkeit',
|
|
||||||
name: 'Load Balancing & Auto-Scaling',
|
|
||||||
description: 'Dynamische Skalierung basierend auf Last-Metriken. Load Balancer mit Health Checks und automatischer Traffic-Umleitung.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'IT-Betrieb',
|
|
||||||
implementationDate: new Date('2025-04-01'),
|
|
||||||
reviewDate: new Date('2026-04-01'),
|
|
||||||
evidence: ['demo-evi-tom-13'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-14',
|
|
||||||
category: 'Belastbarkeit',
|
|
||||||
name: 'DDoS-Schutz',
|
|
||||||
description: 'Cloudbasierter DDoS-Schutz mit automatischer Traffic-Filterung. Kapazität für 10x Normal-Traffic.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'IT-Sicherheit',
|
|
||||||
implementationDate: new Date('2025-01-01'),
|
|
||||||
reviewDate: new Date('2026-01-01'),
|
|
||||||
evidence: ['demo-evi-tom-14'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Wiederherstellbarkeit
|
|
||||||
{
|
|
||||||
id: 'demo-tom-15',
|
|
||||||
category: 'Wiederherstellbarkeit',
|
|
||||||
name: 'Backup-Strategie',
|
|
||||||
description: '3-2-1 Backup-Strategie: 3 Kopien, 2 verschiedene Medien, 1 Offsite. Tägliche inkrementelle, wöchentliche Vollbackups.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'CRITICAL',
|
|
||||||
responsiblePerson: 'IT-Betrieb',
|
|
||||||
implementationDate: new Date('2025-01-01'),
|
|
||||||
reviewDate: new Date('2026-01-01'),
|
|
||||||
evidence: ['demo-evi-tom-15'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-16',
|
|
||||||
category: 'Wiederherstellbarkeit',
|
|
||||||
name: 'Restore-Tests',
|
|
||||||
description: 'Monatliche Restore-Tests mit zufällig ausgewählten Daten. Dokumentation der Recovery-Zeit und Vollständigkeit.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'IT-Betrieb',
|
|
||||||
implementationDate: new Date('2025-02-01'),
|
|
||||||
reviewDate: new Date('2026-02-01'),
|
|
||||||
evidence: ['demo-evi-tom-16'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// Überprüfung & Bewertung
|
|
||||||
{
|
|
||||||
id: 'demo-tom-17',
|
|
||||||
category: 'Überprüfung & Bewertung',
|
|
||||||
name: 'Penetration Tests',
|
|
||||||
description: 'Jährliche externe Penetration Tests durch zertifizierte Dienstleister. Zusätzliche Tests nach größeren Änderungen.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'IT-Sicherheit',
|
|
||||||
implementationDate: new Date('2025-03-01'),
|
|
||||||
reviewDate: new Date('2026-03-01'),
|
|
||||||
evidence: ['demo-evi-tom-17'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-18',
|
|
||||||
category: 'Überprüfung & Bewertung',
|
|
||||||
name: 'Security Awareness Training',
|
|
||||||
description: 'Verpflichtendes Security-Training für alle Mitarbeiter bei Einstellung und jährlich. Phishing-Simulationen quartalsweise.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'MEDIUM',
|
|
||||||
responsiblePerson: 'HR / IT-Sicherheit',
|
|
||||||
implementationDate: new Date('2025-01-15'),
|
|
||||||
reviewDate: new Date('2026-01-15'),
|
|
||||||
evidence: ['demo-evi-tom-18'],
|
|
||||||
},
|
|
||||||
|
|
||||||
// KI-spezifische TOMs
|
|
||||||
{
|
|
||||||
id: 'demo-tom-19',
|
|
||||||
category: 'KI-Governance',
|
|
||||||
name: 'Model Governance Framework',
|
|
||||||
description: 'Dokumentierter Prozess für Entwicklung, Test, Deployment und Monitoring von KI-Modellen. Model Cards für alle produktiven Modelle.',
|
|
||||||
type: 'ORGANIZATIONAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'Data Science Lead',
|
|
||||||
implementationDate: new Date('2025-06-01'),
|
|
||||||
reviewDate: new Date('2026-06-01'),
|
|
||||||
evidence: ['demo-evi-tom-19'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-tom-20',
|
|
||||||
category: 'KI-Governance',
|
|
||||||
name: 'Bias Detection & Monitoring',
|
|
||||||
description: 'Automatisiertes Monitoring der Modell-Outputs auf Bias. Alerting bei signifikanten Abweichungen von Fairness-Metriken.',
|
|
||||||
type: 'TECHNICAL',
|
|
||||||
implementationStatus: 'IMPLEMENTED',
|
|
||||||
priority: 'HIGH',
|
|
||||||
responsiblePerson: 'Data Science Lead',
|
|
||||||
implementationDate: new Date('2025-07-01'),
|
|
||||||
reviewDate: new Date('2026-07-01'),
|
|
||||||
evidence: ['demo-evi-tom-20'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function getDemoTOMs(): TOM[] {
|
|
||||||
return DEMO_TOMS.map(tom => ({
|
|
||||||
...tom,
|
|
||||||
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
|
|
||||||
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo Use Cases for AI Compliance SDK
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { UseCaseAssessment, AssessmentResult } from '../types'
|
|
||||||
|
|
||||||
export const DEMO_USE_CASES: UseCaseAssessment[] = [
|
|
||||||
{
|
|
||||||
id: 'demo-uc-1',
|
|
||||||
name: 'KI-gestützte Kundenanalyse',
|
|
||||||
description: 'Analyse von Kundenverhalten und Präferenzen mittels Machine Learning zur Personalisierung von Angeboten und Verbesserung des Customer Lifetime Value. Das System verarbeitet Transaktionsdaten, Clickstreams und demographische Informationen.',
|
|
||||||
category: 'Marketing',
|
|
||||||
stepsCompleted: 5,
|
|
||||||
steps: [
|
|
||||||
{ id: 'uc1-step-1', name: 'Grunddaten', completed: true, data: { type: 'customer-analytics', department: 'Marketing' } },
|
|
||||||
{ id: 'uc1-step-2', name: 'Datenquellen', completed: true, data: { sources: ['CRM', 'Webshop', 'Newsletter'] } },
|
|
||||||
{ id: 'uc1-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['Clustering', 'Recommender', 'Churn-Prediction'] } },
|
|
||||||
{ id: 'uc1-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Kunden', 'Interessenten'] } },
|
|
||||||
{ id: 'uc1-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'HIGH' } },
|
|
||||||
],
|
|
||||||
assessmentResult: {
|
|
||||||
riskLevel: 'HIGH',
|
|
||||||
applicableRegulations: ['DSGVO', 'AI Act', 'TTDSG'],
|
|
||||||
recommendedControls: ['Einwilligungsmanagement', 'Profilbildungstransparenz', 'Opt-out-Mechanismus'],
|
|
||||||
dsfaRequired: true,
|
|
||||||
aiActClassification: 'LIMITED',
|
|
||||||
},
|
|
||||||
createdAt: new Date('2026-01-15'),
|
|
||||||
updatedAt: new Date('2026-02-01'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-uc-2',
|
|
||||||
name: 'Automatisierte Bewerbungsvorauswahl',
|
|
||||||
description: 'KI-System zur Vorauswahl von Bewerbungen basierend auf Lebenslauf-Analyse, Qualifikationsabgleich und Erfahrungsbewertung. Ziel ist die Effizienzsteigerung im Recruiting-Prozess bei gleichzeitiger Gewährleistung von Fairness.',
|
|
||||||
category: 'HR',
|
|
||||||
stepsCompleted: 5,
|
|
||||||
steps: [
|
|
||||||
{ id: 'uc2-step-1', name: 'Grunddaten', completed: true, data: { type: 'hr-screening', department: 'Personal' } },
|
|
||||||
{ id: 'uc2-step-2', name: 'Datenquellen', completed: true, data: { sources: ['Bewerbungsportal', 'LinkedIn', 'XING'] } },
|
|
||||||
{ id: 'uc2-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['NLP', 'Matching', 'Scoring'] } },
|
|
||||||
{ id: 'uc2-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Bewerber'] } },
|
|
||||||
{ id: 'uc2-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'HIGH' } },
|
|
||||||
],
|
|
||||||
assessmentResult: {
|
|
||||||
riskLevel: 'HIGH',
|
|
||||||
applicableRegulations: ['DSGVO', 'AI Act', 'AGG'],
|
|
||||||
recommendedControls: ['Bias-Monitoring', 'Human-in-the-Loop', 'Transparenzpflichten'],
|
|
||||||
dsfaRequired: true,
|
|
||||||
aiActClassification: 'HIGH',
|
|
||||||
},
|
|
||||||
createdAt: new Date('2026-01-20'),
|
|
||||||
updatedAt: new Date('2026-02-02'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-uc-3',
|
|
||||||
name: 'Chatbot für Kundenservice',
|
|
||||||
description: 'Konversationeller KI-Assistent für die automatisierte Beantwortung von Kundenanfragen im First-Level-Support. Basiert auf Large Language Models mit firmeneigenem Wissen.',
|
|
||||||
category: 'Kundenservice',
|
|
||||||
stepsCompleted: 5,
|
|
||||||
steps: [
|
|
||||||
{ id: 'uc3-step-1', name: 'Grunddaten', completed: true, data: { type: 'chatbot', department: 'Support' } },
|
|
||||||
{ id: 'uc3-step-2', name: 'Datenquellen', completed: true, data: { sources: ['FAQ', 'Wissensdatenbank', 'Ticketsystem'] } },
|
|
||||||
{ id: 'uc3-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['LLM', 'RAG', 'Intent-Classification'] } },
|
|
||||||
{ id: 'uc3-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Kunden', 'Interessenten'] } },
|
|
||||||
{ id: 'uc3-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'MEDIUM' } },
|
|
||||||
],
|
|
||||||
assessmentResult: {
|
|
||||||
riskLevel: 'MEDIUM',
|
|
||||||
applicableRegulations: ['DSGVO', 'AI Act'],
|
|
||||||
recommendedControls: ['KI-Kennzeichnung', 'Übergabe an Menschen', 'Datensparsamkeit'],
|
|
||||||
dsfaRequired: false,
|
|
||||||
aiActClassification: 'LIMITED',
|
|
||||||
},
|
|
||||||
createdAt: new Date('2026-01-25'),
|
|
||||||
updatedAt: new Date('2026-02-03'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function getDemoUseCases(): UseCaseAssessment[] {
|
|
||||||
return DEMO_USE_CASES.map(uc => ({
|
|
||||||
...uc,
|
|
||||||
createdAt: new Date(uc.createdAt),
|
|
||||||
updatedAt: new Date(uc.updatedAt),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
/**
|
|
||||||
* Demo VVT (Verarbeitungsverzeichnis / Processing Activities Register) for AI Compliance SDK
|
|
||||||
* Art. 30 DSGVO - These are seed data structures - actual data is stored in database
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ProcessingActivity, RetentionPolicy } from '../types'
|
|
||||||
|
|
||||||
export const DEMO_PROCESSING_ACTIVITIES: ProcessingActivity[] = [
|
|
||||||
{
|
|
||||||
id: 'demo-pa-1',
|
|
||||||
name: 'KI-gestützte Kundenanalyse',
|
|
||||||
purpose: 'Analyse von Kundenverhalten und Präferenzen zur Personalisierung von Angeboten, Churn-Prediction und Marketing-Optimierung',
|
|
||||||
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse) / Art. 6 Abs. 1 lit. a DSGVO (Einwilligung für erweitertes Profiling)',
|
|
||||||
dataCategories: [
|
|
||||||
'Stammdaten (Name, Adresse, E-Mail, Telefon)',
|
|
||||||
'Transaktionsdaten (Käufe, Bestellungen, Retouren)',
|
|
||||||
'Nutzungsdaten (Clickstreams, Seitenaufrufe, Verweildauer)',
|
|
||||||
'Demographische Daten (Alter, Geschlecht, PLZ-Region)',
|
|
||||||
],
|
|
||||||
dataSubjects: [
|
|
||||||
'Bestandskunden (ca. 250.000 aktive)',
|
|
||||||
'Registrierte Interessenten (ca. 100.000)',
|
|
||||||
'Newsletter-Abonnenten (ca. 180.000)',
|
|
||||||
],
|
|
||||||
recipients: [
|
|
||||||
'Interne Fachabteilungen (Marketing, Vertrieb)',
|
|
||||||
'E-Mail-Marketing-Dienstleister (AV-Vertrag vorhanden)',
|
|
||||||
'Cloud-Infrastruktur-Anbieter (AV-Vertrag vorhanden)',
|
|
||||||
],
|
|
||||||
thirdCountryTransfers: false,
|
|
||||||
retentionPeriod: '3 Jahre nach letzter Aktivität, danach Anonymisierung',
|
|
||||||
technicalMeasures: [
|
|
||||||
'AES-256 Verschlüsselung',
|
|
||||||
'Pseudonymisierung',
|
|
||||||
'Zugriffskontrolle mit MFA',
|
|
||||||
'Audit-Logging',
|
|
||||||
],
|
|
||||||
organizationalMeasures: [
|
|
||||||
'Rollenbasiertes Berechtigungskonzept',
|
|
||||||
'Verpflichtung auf Datengeheimnis',
|
|
||||||
'Regelmäßige Datenschutzschulungen',
|
|
||||||
'Dokumentierte Prozesse',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-pa-2',
|
|
||||||
name: 'Automatisierte Bewerbungsvorauswahl',
|
|
||||||
purpose: 'KI-gestützte Vorauswahl von Bewerbungen basierend auf Lebenslauf-Analyse und Qualifikationsabgleich zur Effizienzsteigerung im Recruiting',
|
|
||||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (vorvertragliche Maßnahmen) / § 26 BDSG (Beschäftigungsverhältnis)',
|
|
||||||
dataCategories: [
|
|
||||||
'Bewerberdaten (Name, Kontakt, Geburtsdatum)',
|
|
||||||
'Qualifikationen (Ausbildung, Berufserfahrung, Zertifikate)',
|
|
||||||
'Lebenslaufdaten (Werdegang, Fähigkeiten)',
|
|
||||||
'Bewerbungsschreiben',
|
|
||||||
],
|
|
||||||
dataSubjects: [
|
|
||||||
'Bewerber auf offene Stellen',
|
|
||||||
'Initiativbewerber',
|
|
||||||
],
|
|
||||||
recipients: [
|
|
||||||
'HR-Abteilung',
|
|
||||||
'Fachabteilungsleiter (nur finale Kandidaten)',
|
|
||||||
'Betriebsrat (Einsichtnahme möglich)',
|
|
||||||
],
|
|
||||||
thirdCountryTransfers: false,
|
|
||||||
retentionPeriod: '6 Monate nach Abschluss des Bewerbungsverfahrens (bei Ablehnung), länger nur mit Einwilligung für Talentpool',
|
|
||||||
technicalMeasures: [
|
|
||||||
'Verschlüsselte Speicherung',
|
|
||||||
'Zugangsbeschränkung auf HR',
|
|
||||||
'Automatische Löschroutinen',
|
|
||||||
'Bias-Monitoring',
|
|
||||||
],
|
|
||||||
organizationalMeasures: [
|
|
||||||
'Human-in-the-Loop für finale Entscheidungen',
|
|
||||||
'Dokumentierte KI-Entscheidungskriterien',
|
|
||||||
'Transparente Information an Bewerber',
|
|
||||||
'Regelmäßige Fairness-Audits',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-pa-3',
|
|
||||||
name: 'Kundenservice-Chatbot',
|
|
||||||
purpose: 'Automatisierte Beantwortung von Kundenanfragen im First-Level-Support mittels KI-gestütztem Dialogsystem',
|
|
||||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) / Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)',
|
|
||||||
dataCategories: [
|
|
||||||
'Kundenstammdaten (zur Identifikation)',
|
|
||||||
'Kommunikationsinhalte (Chat-Verläufe)',
|
|
||||||
'Technische Daten (Session-ID, Zeitstempel)',
|
|
||||||
'Serviceanfragen und deren Lösungen',
|
|
||||||
],
|
|
||||||
dataSubjects: [
|
|
||||||
'Kunden mit aktiven Verträgen',
|
|
||||||
'Interessenten mit Anfragen',
|
|
||||||
],
|
|
||||||
recipients: [
|
|
||||||
'Kundenservice-Team (bei Eskalation)',
|
|
||||||
'Cloud-Anbieter (Hosting, AV-Vertrag)',
|
|
||||||
],
|
|
||||||
thirdCountryTransfers: false,
|
|
||||||
retentionPeriod: '2 Jahre für Chat-Verläufe, danach Anonymisierung für Training',
|
|
||||||
technicalMeasures: [
|
|
||||||
'TLS-Verschlüsselung',
|
|
||||||
'Keine Speicherung sensitiver Daten im Chat',
|
|
||||||
'Automatische PII-Erkennung und Maskierung',
|
|
||||||
],
|
|
||||||
organizationalMeasures: [
|
|
||||||
'Klare KI-Kennzeichnung gegenüber Kunden',
|
|
||||||
'Jederzeit Übergabe an Menschen möglich',
|
|
||||||
'Schulung des Eskalations-Teams',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-pa-4',
|
|
||||||
name: 'Mitarbeiterverwaltung',
|
|
||||||
purpose: 'Verwaltung von Personalstammdaten, Gehaltsabrechnung, Zeiterfassung und Personalentwicklung',
|
|
||||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Arbeitsvertrag) / § 26 BDSG (Beschäftigungsverhältnis) / gesetzliche Pflichten (Steuer, SV)',
|
|
||||||
dataCategories: [
|
|
||||||
'Personalstammdaten (Name, Adresse, Geburtsdatum, SV-Nr.)',
|
|
||||||
'Vertragsdaten (Arbeitsvertrag, Gehalt, Arbeitszeit)',
|
|
||||||
'Zeiterfassungsdaten',
|
|
||||||
'Leistungsbeurteilungen',
|
|
||||||
'Bankverbindung',
|
|
||||||
],
|
|
||||||
dataSubjects: [
|
|
||||||
'Aktive Mitarbeiter',
|
|
||||||
'Ehemalige Mitarbeiter (Archiv)',
|
|
||||||
],
|
|
||||||
recipients: [
|
|
||||||
'HR-Abteilung',
|
|
||||||
'Lohnbuchhaltung / Steuerberater',
|
|
||||||
'Sozialversicherungsträger',
|
|
||||||
'Finanzamt',
|
|
||||||
],
|
|
||||||
thirdCountryTransfers: false,
|
|
||||||
retentionPeriod: '10 Jahre nach Ausscheiden (steuerliche Aufbewahrungspflichten)',
|
|
||||||
technicalMeasures: [
|
|
||||||
'Verschlüsselte Speicherung',
|
|
||||||
'Strenge Zugriffskontrolle',
|
|
||||||
'Getrennte Systeme für verschiedene Datenkategorien',
|
|
||||||
],
|
|
||||||
organizationalMeasures: [
|
|
||||||
'Need-to-know-Prinzip',
|
|
||||||
'Dokumentierte Prozesse',
|
|
||||||
'Betriebsvereinbarung zur Datenverarbeitung',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-pa-5',
|
|
||||||
name: 'Website-Analyse und Marketing',
|
|
||||||
purpose: 'Analyse des Nutzerverhaltens auf der Website zur Optimierung der User Experience und für personalisierte Marketing-Maßnahmen',
|
|
||||||
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung via Cookie-Banner)',
|
|
||||||
dataCategories: [
|
|
||||||
'Pseudonymisierte Nutzungsdaten',
|
|
||||||
'Cookie-IDs und Tracking-Identifier',
|
|
||||||
'Geräteinformationen',
|
|
||||||
'Interaktionsdaten (Klicks, Scrollverhalten)',
|
|
||||||
],
|
|
||||||
dataSubjects: [
|
|
||||||
'Website-Besucher (nur mit Einwilligung)',
|
|
||||||
],
|
|
||||||
recipients: [
|
|
||||||
'Marketing-Team',
|
|
||||||
'Analytics-Anbieter (AV-Vertrag)',
|
|
||||||
'Advertising-Partner (nur mit erweiterter Einwilligung)',
|
|
||||||
],
|
|
||||||
thirdCountryTransfers: true,
|
|
||||||
retentionPeriod: '13 Monate für Analytics-Daten, Cookie-Laufzeit max. 12 Monate',
|
|
||||||
technicalMeasures: [
|
|
||||||
'IP-Anonymisierung',
|
|
||||||
'Secure Cookies',
|
|
||||||
'Consent-Management-System',
|
|
||||||
],
|
|
||||||
organizationalMeasures: [
|
|
||||||
'Transparente Cookie-Richtlinie',
|
|
||||||
'Einfacher Widerruf möglich',
|
|
||||||
'Regelmäßige Cookie-Audits',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-pa-6',
|
|
||||||
name: 'Videoüberwachung',
|
|
||||||
purpose: 'Schutz von Eigentum und Personen, Prävention und Aufklärung von Straftaten in Geschäftsräumen',
|
|
||||||
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an Sicherheit)',
|
|
||||||
dataCategories: [
|
|
||||||
'Videoaufnahmen',
|
|
||||||
'Zeitstempel',
|
|
||||||
'Aufnahmeort',
|
|
||||||
],
|
|
||||||
dataSubjects: [
|
|
||||||
'Mitarbeiter in überwachten Bereichen',
|
|
||||||
'Besucher und Kunden',
|
|
||||||
'Lieferanten',
|
|
||||||
],
|
|
||||||
recipients: [
|
|
||||||
'Sicherheitspersonal',
|
|
||||||
'Geschäftsleitung (bei Vorfällen)',
|
|
||||||
'Strafverfolgungsbehörden (auf Anforderung)',
|
|
||||||
],
|
|
||||||
thirdCountryTransfers: false,
|
|
||||||
retentionPeriod: '72 Stunden, bei Vorfällen bis zur Abschluss der Untersuchung',
|
|
||||||
technicalMeasures: [
|
|
||||||
'Verschlüsselte Speicherung',
|
|
||||||
'Automatische Löschung nach Fristablauf',
|
|
||||||
'Eingeschränkter Zugriff',
|
|
||||||
],
|
|
||||||
organizationalMeasures: [
|
|
||||||
'Beschilderung der überwachten Bereiche',
|
|
||||||
'Betriebsvereinbarung mit Betriebsrat',
|
|
||||||
'Dokumentiertes Einsichtsprotokoll',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const DEMO_RETENTION_POLICIES: RetentionPolicy[] = [
|
|
||||||
{
|
|
||||||
id: 'demo-ret-1',
|
|
||||||
dataCategory: 'Kundenstammdaten',
|
|
||||||
description: 'Grundlegende Daten zur Kundenidentifikation (Name, Adresse, Kontaktdaten)',
|
|
||||||
legalBasis: 'Handels- und steuerrechtliche Aufbewahrungspflichten (§ 257 HGB, § 147 AO)',
|
|
||||||
retentionPeriod: '10 Jahre nach Vertragsende',
|
|
||||||
deletionMethod: 'Sichere Löschung mit Protokollierung, bei Papier: Aktenvernichtung DIN 66399',
|
|
||||||
exceptions: [
|
|
||||||
'Laufende Rechtsstreitigkeiten',
|
|
||||||
'Offene Forderungen',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ret-2',
|
|
||||||
dataCategory: 'Transaktionsdaten',
|
|
||||||
description: 'Bestellungen, Rechnungen, Zahlungen, Lieferungen',
|
|
||||||
legalBasis: '§ 257 HGB, § 147 AO (handels- und steuerrechtliche Aufbewahrung)',
|
|
||||||
retentionPeriod: '10 Jahre ab Ende des Geschäftsjahres',
|
|
||||||
deletionMethod: 'Automatisierte Löschung nach Fristablauf',
|
|
||||||
exceptions: [
|
|
||||||
'Garantiefälle (bis Ende der Garantiezeit)',
|
|
||||||
'Prüfungen durch Finanzbehörden',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ret-3',
|
|
||||||
dataCategory: 'Bewerberdaten',
|
|
||||||
description: 'Lebenslauf, Anschreiben, Zeugnisse, Korrespondenz',
|
|
||||||
legalBasis: 'AGG (Diskriminierungsschutz) / § 26 BDSG',
|
|
||||||
retentionPeriod: '6 Monate nach Abschluss des Verfahrens',
|
|
||||||
deletionMethod: 'Sichere Löschung, bei Papier: Aktenvernichtung',
|
|
||||||
exceptions: [
|
|
||||||
'Aufnahme in Talentpool (mit Einwilligung): 2 Jahre',
|
|
||||||
'Diskriminierungsklagen: bis Abschluss',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ret-4',
|
|
||||||
dataCategory: 'Personalakten',
|
|
||||||
description: 'Arbeitsverträge, Gehaltsabrechnungen, Beurteilungen, Abmahnungen',
|
|
||||||
legalBasis: '§ 257 HGB, § 147 AO, Sozialversicherungsrecht',
|
|
||||||
retentionPeriod: '10 Jahre nach Ausscheiden (teilweise 30 Jahre für Rentenansprüche)',
|
|
||||||
deletionMethod: 'Sichere Löschung mit Dokumentation',
|
|
||||||
exceptions: [
|
|
||||||
'Arbeitsrechtliche Streitigkeiten',
|
|
||||||
'Rentenversicherungsnachweise (lebenslang empfohlen)',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ret-5',
|
|
||||||
dataCategory: 'Marketing-Profile',
|
|
||||||
description: 'Analysedaten, Segmentierungen, Präferenzen, Kaufhistorie',
|
|
||||||
legalBasis: 'Einwilligung (Art. 6 Abs. 1 lit. a DSGVO)',
|
|
||||||
retentionPeriod: '3 Jahre nach letzter Aktivität, dann Anonymisierung',
|
|
||||||
deletionMethod: 'Pseudonymisierung → Anonymisierung → Löschung',
|
|
||||||
exceptions: [
|
|
||||||
'Widerruf der Einwilligung (sofortige Löschung)',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ret-6',
|
|
||||||
dataCategory: 'Videoaufnahmen',
|
|
||||||
description: 'Aufnahmen der Sicherheitskameras',
|
|
||||||
legalBasis: 'Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO)',
|
|
||||||
retentionPeriod: '72 Stunden',
|
|
||||||
deletionMethod: 'Automatisches Überschreiben',
|
|
||||||
exceptions: [
|
|
||||||
'Sicherheitsvorfälle (bis Abschluss der Untersuchung)',
|
|
||||||
'Anforderung durch Strafverfolgungsbehörden',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ret-7',
|
|
||||||
dataCategory: 'KI-Trainingsdaten',
|
|
||||||
description: 'Anonymisierte Datensätze für Modell-Training',
|
|
||||||
legalBasis: 'Berechtigtes Interesse / ursprüngliche Zweckbindung (bei Kompatibilität)',
|
|
||||||
retentionPeriod: 'Solange Modell aktiv, danach Löschung mit Modell-Archivierung',
|
|
||||||
deletionMethod: 'Sichere Löschung bei Modell-Retirement',
|
|
||||||
exceptions: [
|
|
||||||
'Audit-Trail für Modell-Herkunft (anonymisierte Metadaten)',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'demo-ret-8',
|
|
||||||
dataCategory: 'Audit-Logs',
|
|
||||||
description: 'Protokolle von Datenzugriffen und Systemereignissen',
|
|
||||||
legalBasis: 'Nachweispflichten DSGVO, Compliance-Anforderungen',
|
|
||||||
retentionPeriod: '10 Jahre',
|
|
||||||
deletionMethod: 'Automatisierte Löschung nach Fristablauf',
|
|
||||||
exceptions: [
|
|
||||||
'Laufende Untersuchungen oder Audits',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export function getDemoProcessingActivities(): ProcessingActivity[] {
|
|
||||||
return DEMO_PROCESSING_ACTIVITIES
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDemoRetentionPolicies(): RetentionPolicy[] {
|
|
||||||
return DEMO_RETENTION_POLICIES
|
|
||||||
}
|
|
||||||
@@ -1,548 +0,0 @@
|
|||||||
/**
|
|
||||||
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
|
|
||||||
* in den Dokumentengenerator.
|
|
||||||
*
|
|
||||||
* Diese Funktionen generieren DSGVO-konforme Textbausteine basierend auf
|
|
||||||
* den vom Benutzer ausgewählten Datenpunkten.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DataPoint,
|
|
||||||
DataPointCategory,
|
|
||||||
LegalBasis,
|
|
||||||
RetentionPeriod,
|
|
||||||
RiskLevel,
|
|
||||||
CATEGORY_METADATA,
|
|
||||||
LEGAL_BASIS_INFO,
|
|
||||||
RETENTION_PERIOD_INFO,
|
|
||||||
RISK_LEVEL_STYLING,
|
|
||||||
LocalizedText,
|
|
||||||
SupportedLanguage
|
|
||||||
} from '@/lib/sdk/einwilligungen/types'
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// TYPES
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sprach-Option für alle Helper-Funktionen
|
|
||||||
*/
|
|
||||||
export type Language = SupportedLanguage
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generierte Platzhalter-Map für den Dokumentengenerator
|
|
||||||
*/
|
|
||||||
export interface DataPointPlaceholders {
|
|
||||||
'[DATENPUNKTE_COUNT]': string
|
|
||||||
'[DATENPUNKTE_LIST]': string
|
|
||||||
'[DATENPUNKTE_TABLE]': string
|
|
||||||
'[VERARBEITUNGSZWECKE]': string
|
|
||||||
'[RECHTSGRUNDLAGEN]': string
|
|
||||||
'[SPEICHERFRISTEN]': string
|
|
||||||
'[EMPFAENGER]': string
|
|
||||||
'[BESONDERE_KATEGORIEN]': string
|
|
||||||
'[DRITTLAND_TRANSFERS]': string
|
|
||||||
'[RISIKO_ZUSAMMENFASSUNG]': string
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HELPER FUNCTIONS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extrahiert Text aus LocalizedText basierend auf Sprache
|
|
||||||
*/
|
|
||||||
function getText(text: LocalizedText, lang: Language): string {
|
|
||||||
return text[lang] || text.de
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert eine Markdown-Tabelle der Datenpunkte
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der ausgewählten Datenpunkte
|
|
||||||
* @param lang - Sprache für die Ausgabe
|
|
||||||
* @returns Markdown-Tabelle als String
|
|
||||||
*/
|
|
||||||
export function generateDataPointsTable(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
lang: Language = 'de'
|
|
||||||
): string {
|
|
||||||
if (dataPoints.length === 0) {
|
|
||||||
return lang === 'de'
|
|
||||||
? '*Keine Datenpunkte ausgewählt.*'
|
|
||||||
: '*No data points selected.*'
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = lang === 'de'
|
|
||||||
? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |'
|
|
||||||
: '| Data Point | Category | Purpose | Legal Basis | Retention Period |'
|
|
||||||
const separator = '|------------|-----------|-------|-----------------|---------------|'
|
|
||||||
|
|
||||||
const rows = dataPoints.map(dp => {
|
|
||||||
const category = CATEGORY_METADATA[dp.category]
|
|
||||||
const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis]
|
|
||||||
const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod]
|
|
||||||
|
|
||||||
const name = getText(dp.name, lang)
|
|
||||||
const categoryName = getText(category.name, lang)
|
|
||||||
const purpose = getText(dp.purpose, lang)
|
|
||||||
const legalBasisName = getText(legalBasis.name, lang)
|
|
||||||
const retentionLabel = getText(retention.label, lang)
|
|
||||||
|
|
||||||
// Truncate long texts for table readability
|
|
||||||
const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
|
|
||||||
|
|
||||||
return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
|
|
||||||
}).join('\n')
|
|
||||||
|
|
||||||
return `${header}\n${separator}\n${rows}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gruppiert Datenpunkte nach Speicherfrist
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @returns Record mit Speicherfrist als Key und Datenpunkten als Value
|
|
||||||
*/
|
|
||||||
export function groupByRetention(
|
|
||||||
dataPoints: DataPoint[]
|
|
||||||
): Record<RetentionPeriod, DataPoint[]> {
|
|
||||||
return dataPoints.reduce((acc, dp) => {
|
|
||||||
const key = dp.retentionPeriod
|
|
||||||
if (!acc[key]) {
|
|
||||||
acc[key] = []
|
|
||||||
}
|
|
||||||
acc[key].push(dp)
|
|
||||||
return acc
|
|
||||||
}, {} as Record<RetentionPeriod, DataPoint[]>)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gruppiert Datenpunkte nach Kategorie
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @returns Record mit Kategorie als Key und Datenpunkten als Value
|
|
||||||
*/
|
|
||||||
export function groupByCategory(
|
|
||||||
dataPoints: DataPoint[]
|
|
||||||
): Record<DataPointCategory, DataPoint[]> {
|
|
||||||
return dataPoints.reduce((acc, dp) => {
|
|
||||||
const key = dp.category
|
|
||||||
if (!acc[key]) {
|
|
||||||
acc[key] = []
|
|
||||||
}
|
|
||||||
acc[key].push(dp)
|
|
||||||
return acc
|
|
||||||
}, {} as Record<DataPointCategory, DataPoint[]>)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert DSGVO-konformen Abschnitt für besondere Kategorien (Art. 9 DSGVO)
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param lang - Sprache für die Ausgabe
|
|
||||||
* @returns Markdown-Abschnitt als String (leer wenn keine Art. 9 Daten)
|
|
||||||
*/
|
|
||||||
export function generateSpecialCategorySection(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
lang: Language = 'de'
|
|
||||||
): string {
|
|
||||||
const special = dataPoints.filter(dp => dp.isSpecialCategory)
|
|
||||||
|
|
||||||
if (special.length === 0) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lang === 'de') {
|
|
||||||
const items = special.map(dp =>
|
|
||||||
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
|
|
||||||
).join('\n')
|
|
||||||
|
|
||||||
return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
|
|
||||||
|
|
||||||
Wir verarbeiten folgende besondere Kategorien personenbezogener Daten:
|
|
||||||
|
|
||||||
${items}
|
|
||||||
|
|
||||||
Die Verarbeitung erfolgt auf Grundlage Ihrer ausdrücklichen Einwilligung gemäß Art. 9 Abs. 2 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen.`
|
|
||||||
} else {
|
|
||||||
const items = special.map(dp =>
|
|
||||||
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
|
|
||||||
).join('\n')
|
|
||||||
|
|
||||||
return `## Processing of Special Categories of Personal Data (Art. 9 GDPR)
|
|
||||||
|
|
||||||
We process the following special categories of personal data:
|
|
||||||
|
|
||||||
${items}
|
|
||||||
|
|
||||||
Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert Liste aller eindeutigen Verarbeitungszwecke
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param lang - Sprache für die Ausgabe
|
|
||||||
* @returns Kommaseparierte Liste der Zwecke
|
|
||||||
*/
|
|
||||||
export function generatePurposesList(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
lang: Language = 'de'
|
|
||||||
): string {
|
|
||||||
const purposes = new Set<string>()
|
|
||||||
|
|
||||||
dataPoints.forEach(dp => {
|
|
||||||
purposes.add(getText(dp.purpose, lang))
|
|
||||||
})
|
|
||||||
|
|
||||||
return [...purposes].join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert Liste aller verwendeten Rechtsgrundlagen
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param lang - Sprache für die Ausgabe
|
|
||||||
* @returns Formatierte Liste der Rechtsgrundlagen
|
|
||||||
*/
|
|
||||||
export function generateLegalBasisList(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
lang: Language = 'de'
|
|
||||||
): string {
|
|
||||||
const bases = new Set<LegalBasis>()
|
|
||||||
|
|
||||||
dataPoints.forEach(dp => {
|
|
||||||
bases.add(dp.legalBasis)
|
|
||||||
})
|
|
||||||
|
|
||||||
return [...bases].map(basis => {
|
|
||||||
const info = LEGAL_BASIS_INFO[basis]
|
|
||||||
return `${info.article} (${getText(info.name, lang)})`
|
|
||||||
}).join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert Liste aller Speicherfristen gruppiert
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param lang - Sprache für die Ausgabe
|
|
||||||
* @returns Formatierte Liste der Speicherfristen mit zugehörigen Kategorien
|
|
||||||
*/
|
|
||||||
export function generateRetentionList(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
lang: Language = 'de'
|
|
||||||
): string {
|
|
||||||
const grouped = groupByRetention(dataPoints)
|
|
||||||
const entries: string[] = []
|
|
||||||
|
|
||||||
for (const [period, points] of Object.entries(grouped)) {
|
|
||||||
const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod]
|
|
||||||
const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))]
|
|
||||||
|
|
||||||
entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return entries.join('; ')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert Liste aller Empfänger/Drittparteien
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @returns Kommaseparierte Liste der Empfänger
|
|
||||||
*/
|
|
||||||
export function generateRecipientsList(dataPoints: DataPoint[]): string {
|
|
||||||
const recipients = new Set<string>()
|
|
||||||
|
|
||||||
dataPoints.forEach(dp => {
|
|
||||||
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
|
|
||||||
})
|
|
||||||
|
|
||||||
if (recipients.size === 0) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...recipients].join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert Abschnitt für Drittland-Übermittlungen
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte mit thirdCountryTransfer === true
|
|
||||||
* @param lang - Sprache für die Ausgabe
|
|
||||||
* @returns Markdown-Abschnitt als String
|
|
||||||
*/
|
|
||||||
export function generateThirdCountrySection(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
lang: Language = 'de'
|
|
||||||
): string {
|
|
||||||
// Note: We assume dataPoints have been filtered for thirdCountryTransfer
|
|
||||||
// The actual flag would need to be added to the DataPoint interface
|
|
||||||
// For now, we check if any thirdPartyRecipients suggest third country
|
|
||||||
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare']
|
|
||||||
|
|
||||||
const thirdCountryPoints = dataPoints.filter(dp =>
|
|
||||||
dp.thirdPartyRecipients?.some(r =>
|
|
||||||
thirdCountryIndicators.some(indicator =>
|
|
||||||
r.toLowerCase().includes(indicator.toLowerCase())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (thirdCountryPoints.length === 0) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipients = new Set<string>()
|
|
||||||
thirdCountryPoints.forEach(dp => {
|
|
||||||
dp.thirdPartyRecipients?.forEach(r => {
|
|
||||||
if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
|
|
||||||
recipients.add(r)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (lang === 'de') {
|
|
||||||
return `## Übermittlung in Drittländer
|
|
||||||
|
|
||||||
Wir übermitteln personenbezogene Daten an folgende Empfänger in Drittländern (außerhalb der EU/des EWR):
|
|
||||||
|
|
||||||
${[...recipients].map(r => `- ${r}`).join('\n')}
|
|
||||||
|
|
||||||
Die Übermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).`
|
|
||||||
} else {
|
|
||||||
return `## Transfers to Third Countries
|
|
||||||
|
|
||||||
We transfer personal data to the following recipients in third countries (outside the EU/EEA):
|
|
||||||
|
|
||||||
${[...recipients].map(r => `- ${r}`).join('\n')}
|
|
||||||
|
|
||||||
The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert Risiko-Zusammenfassung
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param lang - Sprache für die Ausgabe
|
|
||||||
* @returns Formatierte Risiko-Zusammenfassung
|
|
||||||
*/
|
|
||||||
export function generateRiskSummary(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
lang: Language = 'de'
|
|
||||||
): string {
|
|
||||||
const riskCounts: Record<RiskLevel, number> = {
|
|
||||||
LOW: 0,
|
|
||||||
MEDIUM: 0,
|
|
||||||
HIGH: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
dataPoints.forEach(dp => {
|
|
||||||
riskCounts[dp.riskLevel]++
|
|
||||||
})
|
|
||||||
|
|
||||||
const parts = Object.entries(riskCounts)
|
|
||||||
.filter(([, count]) => count > 0)
|
|
||||||
.map(([level, count]) => {
|
|
||||||
const styling = RISK_LEVEL_STYLING[level as RiskLevel]
|
|
||||||
return `${count} ${getText(styling.label, lang).toLowerCase()}`
|
|
||||||
})
|
|
||||||
|
|
||||||
return parts.join(', ')
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generiert alle Platzhalter für den Dokumentengenerator
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der ausgewählten Datenpunkte
|
|
||||||
* @param lang - Sprache für die Ausgabe
|
|
||||||
* @returns Objekt mit allen Platzhaltern
|
|
||||||
*/
|
|
||||||
export function generateAllPlaceholders(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
lang: Language = 'de'
|
|
||||||
): DataPointPlaceholders {
|
|
||||||
return {
|
|
||||||
'[DATENPUNKTE_COUNT]': String(dataPoints.length),
|
|
||||||
'[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '),
|
|
||||||
'[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang),
|
|
||||||
'[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang),
|
|
||||||
'[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang),
|
|
||||||
'[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang),
|
|
||||||
'[EMPFAENGER]': generateRecipientsList(dataPoints),
|
|
||||||
'[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang),
|
|
||||||
'[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang),
|
|
||||||
'[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// VALIDATION HELPERS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validierungswarnung für den Dokumentengenerator
|
|
||||||
*/
|
|
||||||
export interface ValidationWarning {
|
|
||||||
type: 'error' | 'warning' | 'info'
|
|
||||||
code: string
|
|
||||||
message: string
|
|
||||||
suggestion: string
|
|
||||||
affectedDataPoints?: DataPoint[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob besondere Kategorien vorhanden sind aber kein entsprechender Abschnitt
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param documentContent - Der generierte Dokumentinhalt
|
|
||||||
* @param lang - Sprache
|
|
||||||
* @returns ValidationWarning oder null
|
|
||||||
*/
|
|
||||||
export function checkSpecialCategoriesWarning(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
documentContent: string,
|
|
||||||
lang: Language = 'de'
|
|
||||||
): ValidationWarning | null {
|
|
||||||
const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory)
|
|
||||||
|
|
||||||
if (specialCategories.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSection = lang === 'de'
|
|
||||||
? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie')
|
|
||||||
: documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor')
|
|
||||||
|
|
||||||
if (!hasSection) {
|
|
||||||
return {
|
|
||||||
type: 'error',
|
|
||||||
code: 'MISSING_ART9_SECTION',
|
|
||||||
message: lang === 'de'
|
|
||||||
? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewählt, aber kein entsprechender Abschnitt im Dokument gefunden.`
|
|
||||||
: `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`,
|
|
||||||
suggestion: lang === 'de'
|
|
||||||
? 'Fügen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.'
|
|
||||||
: 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.',
|
|
||||||
affectedDataPoints: specialCategories
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob Drittland-Übermittlungen vorhanden sind aber keine SCC erwähnt werden
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param documentContent - Der generierte Dokumentinhalt
|
|
||||||
* @param lang - Sprache
|
|
||||||
* @returns ValidationWarning oder null
|
|
||||||
*/
|
|
||||||
export function checkThirdCountryWarning(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
documentContent: string,
|
|
||||||
lang: Language = 'de'
|
|
||||||
): ValidationWarning | null {
|
|
||||||
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US']
|
|
||||||
|
|
||||||
const thirdCountryPoints = dataPoints.filter(dp =>
|
|
||||||
dp.thirdPartyRecipients?.some(r =>
|
|
||||||
thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (thirdCountryPoints.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasSCCMention = lang === 'de'
|
|
||||||
? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
|
|
||||||
: documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
|
|
||||||
|
|
||||||
if (!hasSCCMention) {
|
|
||||||
return {
|
|
||||||
type: 'warning',
|
|
||||||
code: 'MISSING_SCC_SECTION',
|
|
||||||
message: lang === 'de'
|
|
||||||
? `Drittland-Übermittlung für ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwähnt.`
|
|
||||||
: `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`,
|
|
||||||
suggestion: lang === 'de'
|
|
||||||
? 'Erwägen Sie die Aufnahme eines Abschnitts zu Drittland-Übermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.'
|
|
||||||
: 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.',
|
|
||||||
affectedDataPoints: thirdCountryPoints
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prüft ob Datenpunkte mit expliziter Einwilligung korrekt behandelt werden
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param documentContent - Der generierte Dokumentinhalt
|
|
||||||
* @param lang - Sprache
|
|
||||||
* @returns ValidationWarning oder null
|
|
||||||
*/
|
|
||||||
export function checkExplicitConsentWarning(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
documentContent: string,
|
|
||||||
lang: Language = 'de'
|
|
||||||
): ValidationWarning | null {
|
|
||||||
const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent)
|
|
||||||
|
|
||||||
if (explicitConsentPoints.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasConsentSection = lang === 'de'
|
|
||||||
? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7')
|
|
||||||
: documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7')
|
|
||||||
|
|
||||||
if (!hasConsentSection) {
|
|
||||||
return {
|
|
||||||
type: 'warning',
|
|
||||||
code: 'MISSING_CONSENT_SECTION',
|
|
||||||
message: lang === 'de'
|
|
||||||
? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrückliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.`
|
|
||||||
: `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`,
|
|
||||||
suggestion: lang === 'de'
|
|
||||||
? 'Fügen Sie einen Abschnitt zum Widerrufsrecht hinzu.'
|
|
||||||
: 'Add a section about the right to withdraw consent.',
|
|
||||||
affectedDataPoints: explicitConsentPoints
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Führt alle Validierungsprüfungen durch
|
|
||||||
*
|
|
||||||
* @param dataPoints - Liste der Datenpunkte
|
|
||||||
* @param documentContent - Der generierte Dokumentinhalt
|
|
||||||
* @param lang - Sprache
|
|
||||||
* @returns Array aller Warnungen
|
|
||||||
*/
|
|
||||||
export function validateDocument(
|
|
||||||
dataPoints: DataPoint[],
|
|
||||||
documentContent: string,
|
|
||||||
lang: Language = 'de'
|
|
||||||
): ValidationWarning[] {
|
|
||||||
const warnings: ValidationWarning[] = []
|
|
||||||
|
|
||||||
const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang)
|
|
||||||
if (specialCatWarning) warnings.push(specialCatWarning)
|
|
||||||
|
|
||||||
const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang)
|
|
||||||
if (thirdCountryWarning) warnings.push(thirdCountryWarning)
|
|
||||||
|
|
||||||
const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang)
|
|
||||||
if (consentWarning) warnings.push(consentWarning)
|
|
||||||
|
|
||||||
return warnings
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* Document Generator Library
|
|
||||||
*
|
|
||||||
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
|
|
||||||
* in den Dokumentengenerator.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './datapoint-helpers'
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import { ConstraintEnforcer } from '../constraint-enforcer'
|
|
||||||
import type { ScopeDecision } from '../../compliance-scope-types'
|
|
||||||
|
|
||||||
describe('ConstraintEnforcer', () => {
|
|
||||||
const enforcer = new ConstraintEnforcer()
|
|
||||||
|
|
||||||
// Helper: minimal valid ScopeDecision
|
|
||||||
function makeDecision(overrides: Partial<ScopeDecision> = {}): ScopeDecision {
|
|
||||||
return {
|
|
||||||
id: 'test-decision',
|
|
||||||
determinedLevel: 'L2',
|
|
||||||
scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 },
|
|
||||||
triggeredHardTriggers: [],
|
|
||||||
requiredDocuments: [
|
|
||||||
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
|
|
||||||
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '3h', triggeredBy: [] },
|
|
||||||
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
|
|
||||||
],
|
|
||||||
riskFlags: [],
|
|
||||||
gaps: [],
|
|
||||||
nextActions: [],
|
|
||||||
reasoning: [],
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
...overrides,
|
|
||||||
} as ScopeDecision
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('check - no decision', () => {
|
|
||||||
it('should allow basic documents (vvt, tom, dsi) without decision', () => {
|
|
||||||
const result = enforcer.check('vvt', null)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
expect(result.adjustments.length).toBeGreaterThan(0)
|
|
||||||
expect(result.checkedRules).toContain('RULE-NO-DECISION')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow tom without decision', () => {
|
|
||||||
const result = enforcer.check('tom', null)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow dsi without decision', () => {
|
|
||||||
const result = enforcer.check('dsi', null)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should block non-basic documents without decision', () => {
|
|
||||||
const result = enforcer.check('dsfa', null)
|
|
||||||
expect(result.allowed).toBe(false)
|
|
||||||
expect(result.violations.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should block av_vertrag without decision', () => {
|
|
||||||
const result = enforcer.check('av_vertrag', null)
|
|
||||||
expect(result.allowed).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('check - RULE-DOC-REQUIRED', () => {
|
|
||||||
it('should allow required documents', () => {
|
|
||||||
const decision = makeDecision()
|
|
||||||
const result = enforcer.check('vvt', decision)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should warn but allow optional documents', () => {
|
|
||||||
const decision = makeDecision({
|
|
||||||
requiredDocuments: [
|
|
||||||
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
const result = enforcer.check('dsfa', decision)
|
|
||||||
expect(result.allowed).toBe(true) // Only warns, does not block
|
|
||||||
expect(result.adjustments.some(a => a.includes('nicht als Pflicht'))).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('check - RULE-DEPTH-MATCH', () => {
|
|
||||||
it('should block when requested depth exceeds determined level', () => {
|
|
||||||
const decision = makeDecision({ determinedLevel: 'L2' })
|
|
||||||
const result = enforcer.check('vvt', decision, 'L4')
|
|
||||||
expect(result.allowed).toBe(false)
|
|
||||||
expect(result.violations.some(v => v.includes('ueberschreitet'))).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow when requested depth matches level', () => {
|
|
||||||
const decision = makeDecision({ determinedLevel: 'L2' })
|
|
||||||
const result = enforcer.check('vvt', decision, 'L2')
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should adjust when requested depth is below level', () => {
|
|
||||||
const decision = makeDecision({ determinedLevel: 'L3' })
|
|
||||||
const result = enforcer.check('vvt', decision, 'L1')
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
expect(result.adjustments.some(a => a.includes('angehoben'))).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow without requested depth level', () => {
|
|
||||||
const decision = makeDecision({ determinedLevel: 'L3' })
|
|
||||||
const result = enforcer.check('vvt', decision)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('check - RULE-DSFA-ENFORCEMENT', () => {
|
|
||||||
it('should note when DSFA is not required but requested', () => {
|
|
||||||
const decision = makeDecision({ determinedLevel: 'L2' })
|
|
||||||
const result = enforcer.check('dsfa', decision)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
expect(result.adjustments.some(a => a.includes('nicht verpflichtend'))).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should allow DSFA when hard triggers require it', () => {
|
|
||||||
const decision = makeDecision({
|
|
||||||
determinedLevel: 'L3',
|
|
||||||
triggeredHardTriggers: [{
|
|
||||||
rule: {
|
|
||||||
id: 'HT-ART9',
|
|
||||||
label: 'Art. 9 Daten',
|
|
||||||
description: '',
|
|
||||||
conditionField: '',
|
|
||||||
conditionOperator: 'EQUALS' as const,
|
|
||||||
conditionValue: null,
|
|
||||||
minimumLevel: 'L3',
|
|
||||||
mandatoryDocuments: ['dsfa'],
|
|
||||||
dsfaRequired: true,
|
|
||||||
legalReference: 'Art. 35 DSGVO',
|
|
||||||
},
|
|
||||||
matchedValue: null,
|
|
||||||
explanation: 'Art. 9 Daten verarbeitet',
|
|
||||||
}],
|
|
||||||
})
|
|
||||||
const result = enforcer.check('dsfa', decision)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should warn about DSFA when drafting non-DSFA but DSFA is required', () => {
|
|
||||||
const decision = makeDecision({
|
|
||||||
determinedLevel: 'L3',
|
|
||||||
triggeredHardTriggers: [{
|
|
||||||
rule: {
|
|
||||||
id: 'HT-ART9',
|
|
||||||
label: 'Art. 9 Daten',
|
|
||||||
description: '',
|
|
||||||
conditionField: '',
|
|
||||||
conditionOperator: 'EQUALS' as const,
|
|
||||||
conditionValue: null,
|
|
||||||
minimumLevel: 'L3',
|
|
||||||
mandatoryDocuments: ['dsfa'],
|
|
||||||
dsfaRequired: true,
|
|
||||||
legalReference: 'Art. 35 DSGVO',
|
|
||||||
},
|
|
||||||
matchedValue: null,
|
|
||||||
explanation: '',
|
|
||||||
}],
|
|
||||||
requiredDocuments: [
|
|
||||||
{ documentType: 'dsfa', label: 'DSFA', required: true, depth: 'Vollstaendig', detailItems: [], estimatedEffort: '8h', triggeredBy: [] },
|
|
||||||
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
const result = enforcer.check('vvt', decision)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
expect(result.adjustments.some(a => a.includes('DSFA') && a.includes('verpflichtend'))).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('check - RULE-RISK-FLAGS', () => {
|
|
||||||
it('should note critical risk flags', () => {
|
|
||||||
const decision = makeDecision({
|
|
||||||
riskFlags: [
|
|
||||||
{ id: 'rf-1', severity: 'CRITICAL', title: 'Offene Art. 9 Verarbeitung', description: '', recommendation: 'DSFA durchfuehren' },
|
|
||||||
{ id: 'rf-2', severity: 'HIGH', title: 'Fehlende Verschluesselung', description: '', recommendation: 'TOM erstellen' },
|
|
||||||
{ id: 'rf-3', severity: 'LOW', title: 'Dokumentation unvollstaendig', description: '', recommendation: '' },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
const result = enforcer.check('vvt', decision)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
expect(result.adjustments.some(a => a.includes('2 kritische/hohe Risiko-Flags'))).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not flag when no risk flags present', () => {
|
|
||||||
const decision = makeDecision({ riskFlags: [] })
|
|
||||||
const result = enforcer.check('vvt', decision)
|
|
||||||
expect(result.adjustments.every(a => !a.includes('Risiko-Flags'))).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('check - checkedRules tracking', () => {
|
|
||||||
it('should track all checked rules', () => {
|
|
||||||
const decision = makeDecision()
|
|
||||||
const result = enforcer.check('vvt', decision)
|
|
||||||
expect(result.checkedRules).toContain('RULE-DOC-REQUIRED')
|
|
||||||
expect(result.checkedRules).toContain('RULE-DEPTH-MATCH')
|
|
||||||
expect(result.checkedRules).toContain('RULE-DSFA-ENFORCEMENT')
|
|
||||||
expect(result.checkedRules).toContain('RULE-RISK-FLAGS')
|
|
||||||
expect(result.checkedRules).toContain('RULE-HARD-TRIGGER-CONSISTENCY')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('checkFromContext', () => {
|
|
||||||
it('should reconstruct decision from DraftContext and check', () => {
|
|
||||||
const context = {
|
|
||||||
decisions: {
|
|
||||||
level: 'L2' as const,
|
|
||||||
scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 },
|
|
||||||
hardTriggers: [],
|
|
||||||
requiredDocuments: [
|
|
||||||
{ documentType: 'vvt' as const, depth: 'Standard', detailItems: [] },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
companyProfile: { name: 'Test GmbH', industry: 'IT', employeeCount: 50, businessModel: 'SaaS', isPublicSector: false },
|
|
||||||
constraints: {
|
|
||||||
depthRequirements: { required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h' },
|
|
||||||
riskFlags: [],
|
|
||||||
boundaries: [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const result = enforcer.checkFromContext('vvt', context)
|
|
||||||
expect(result.allowed).toBe(true)
|
|
||||||
expect(result.checkedRules.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { IntentClassifier } from '../intent-classifier'
|
|
||||||
|
|
||||||
describe('IntentClassifier', () => {
|
|
||||||
const classifier = new IntentClassifier()
|
|
||||||
|
|
||||||
describe('classify - Draft mode', () => {
|
|
||||||
it.each([
|
|
||||||
['Erstelle ein VVT fuer unseren Hauptprozess', 'draft'],
|
|
||||||
['Generiere eine TOM-Dokumentation', 'draft'],
|
|
||||||
['Schreibe eine Datenschutzerklaerung', 'draft'],
|
|
||||||
['Verfasse einen Entwurf fuer das Loeschkonzept', 'draft'],
|
|
||||||
['Create a DSFA document', 'draft'],
|
|
||||||
['Draft a privacy policy for us', 'draft'],
|
|
||||||
['Neues VVT anlegen', 'draft'],
|
|
||||||
])('"%s" should classify as %s', (input, expectedMode) => {
|
|
||||||
const result = classifier.classify(input)
|
|
||||||
expect(result.mode).toBe(expectedMode)
|
|
||||||
expect(result.confidence).toBeGreaterThan(0.7)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('classify - Validate mode', () => {
|
|
||||||
it.each([
|
|
||||||
['Pruefe die Konsistenz meiner Dokumente', 'validate'],
|
|
||||||
['Ist mein VVT korrekt?', 'validate'],
|
|
||||||
['Validiere die TOM gegen das VVT', 'validate'],
|
|
||||||
['Check die Vollstaendigkeit', 'validate'],
|
|
||||||
['Stimmt das mit der DSFA ueberein?', 'validate'],
|
|
||||||
['Cross-Check VVT und TOM', 'validate'],
|
|
||||||
])('"%s" should classify as %s', (input, expectedMode) => {
|
|
||||||
const result = classifier.classify(input)
|
|
||||||
expect(result.mode).toBe(expectedMode)
|
|
||||||
expect(result.confidence).toBeGreaterThan(0.7)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('classify - Ask mode', () => {
|
|
||||||
it.each([
|
|
||||||
['Was fehlt noch in meinem Profil?', 'ask'],
|
|
||||||
['Zeige mir die Luecken', 'ask'],
|
|
||||||
['Welche Dokumente fehlen noch?', 'ask'],
|
|
||||||
['Was ist der naechste Schritt?', 'ask'],
|
|
||||||
['Welche Informationen brauche ich noch?', 'ask'],
|
|
||||||
])('"%s" should classify as %s', (input, expectedMode) => {
|
|
||||||
const result = classifier.classify(input)
|
|
||||||
expect(result.mode).toBe(expectedMode)
|
|
||||||
expect(result.confidence).toBeGreaterThan(0.6)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('classify - Explain mode (fallback)', () => {
|
|
||||||
it.each([
|
|
||||||
['Was ist DSGVO?', 'explain'],
|
|
||||||
['Erklaere mir Art. 30', 'explain'],
|
|
||||||
['Hallo', 'explain'],
|
|
||||||
['Danke fuer die Hilfe', 'explain'],
|
|
||||||
])('"%s" should classify as %s (fallback)', (input, expectedMode) => {
|
|
||||||
const result = classifier.classify(input)
|
|
||||||
expect(result.mode).toBe(expectedMode)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('classify - confidence thresholds', () => {
|
|
||||||
it('should have high confidence for clear draft intents', () => {
|
|
||||||
const result = classifier.classify('Erstelle ein neues VVT')
|
|
||||||
expect(result.confidence).toBeGreaterThanOrEqual(0.85)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should have lower confidence for ambiguous inputs', () => {
|
|
||||||
const result = classifier.classify('Hallo')
|
|
||||||
expect(result.confidence).toBeLessThan(0.6)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should boost confidence with document type detection', () => {
|
|
||||||
const withDoc = classifier.classify('Erstelle VVT')
|
|
||||||
const withoutDoc = classifier.classify('Erstelle etwas')
|
|
||||||
expect(withDoc.confidence).toBeGreaterThanOrEqual(withoutDoc.confidence)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should boost confidence with multiple pattern matches', () => {
|
|
||||||
const single = classifier.classify('Erstelle Dokument')
|
|
||||||
const multi = classifier.classify('Erstelle und generiere ein neues Dokument')
|
|
||||||
expect(multi.confidence).toBeGreaterThanOrEqual(single.confidence)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('detectDocumentType', () => {
|
|
||||||
it.each([
|
|
||||||
['VVT erstellen', 'vvt'],
|
|
||||||
['Verarbeitungsverzeichnis', 'vvt'],
|
|
||||||
['Art. 30 Dokumentation', 'vvt'],
|
|
||||||
['TOM definieren', 'tom'],
|
|
||||||
['technisch organisatorische Massnahmen', 'tom'],
|
|
||||||
['Art. 32 Massnahmen', 'tom'],
|
|
||||||
['DSFA durchfuehren', 'dsfa'],
|
|
||||||
['Datenschutz-Folgenabschaetzung', 'dsfa'],
|
|
||||||
['Art. 35 Pruefung', 'dsfa'],
|
|
||||||
['DPIA erstellen', 'dsfa'],
|
|
||||||
['Datenschutzerklaerung', 'dsi'],
|
|
||||||
['Privacy Policy', 'dsi'],
|
|
||||||
['Art. 13 Information', 'dsi'],
|
|
||||||
['Loeschfristen definieren', 'lf'],
|
|
||||||
['Loeschkonzept erstellen', 'lf'],
|
|
||||||
['Retention Policy', 'lf'],
|
|
||||||
['Auftragsverarbeitung', 'av_vertrag'],
|
|
||||||
['AVV erstellen', 'av_vertrag'],
|
|
||||||
['Art. 28 Vertrag', 'av_vertrag'],
|
|
||||||
['Einwilligung einholen', 'einwilligung'],
|
|
||||||
['Consent Management', 'einwilligung'],
|
|
||||||
['Cookie Banner', 'einwilligung'],
|
|
||||||
])('"%s" should detect document type %s', (input, expectedType) => {
|
|
||||||
const result = classifier.detectDocumentType(input)
|
|
||||||
expect(result).toBe(expectedType)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return undefined for unrecognized types', () => {
|
|
||||||
expect(classifier.detectDocumentType('Hallo Welt')).toBeUndefined()
|
|
||||||
expect(classifier.detectDocumentType('Was kostet das?')).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('classify - Umlaut handling', () => {
|
|
||||||
it('should handle German umlauts correctly', () => {
|
|
||||||
// With actual umlauts (ä, ö, ü)
|
|
||||||
const result1 = classifier.classify('Prüfe die Vollständigkeit')
|
|
||||||
expect(result1.mode).toBe('validate')
|
|
||||||
|
|
||||||
// With ae/oe/ue substitution
|
|
||||||
const result2 = classifier.classify('Pruefe die Vollstaendigkeit')
|
|
||||||
expect(result2.mode).toBe('validate')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle ß correctly', () => {
|
|
||||||
const result = classifier.classify('Schließe Lücken')
|
|
||||||
// Should still detect via normalized patterns
|
|
||||||
expect(result).toBeDefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('classify - combined mode + document type', () => {
|
|
||||||
it('should detect both mode and document type', () => {
|
|
||||||
const result = classifier.classify('Erstelle ein VVT fuer unsere Firma')
|
|
||||||
expect(result.mode).toBe('draft')
|
|
||||||
expect(result.detectedDocumentType).toBe('vvt')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should detect validate + document type', () => {
|
|
||||||
const result = classifier.classify('Pruefe mein TOM auf Konsistenz')
|
|
||||||
expect(result.mode).toBe('validate')
|
|
||||||
expect(result.detectedDocumentType).toBe('tom')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
import { StateProjector } from '../state-projector'
|
|
||||||
import type { SDKState } from '../../types'
|
|
||||||
|
|
||||||
describe('StateProjector', () => {
|
|
||||||
const projector = new StateProjector()
|
|
||||||
|
|
||||||
// Helper: minimal SDKState
|
|
||||||
function makeState(overrides: Partial<SDKState> = {}): SDKState {
|
|
||||||
return {
|
|
||||||
version: '1.0.0',
|
|
||||||
lastModified: new Date(),
|
|
||||||
tenantId: 'test',
|
|
||||||
userId: 'user1',
|
|
||||||
subscription: 'PROFESSIONAL',
|
|
||||||
customerType: null,
|
|
||||||
companyProfile: null,
|
|
||||||
complianceScope: null,
|
|
||||||
currentPhase: 1,
|
|
||||||
currentStep: 'company-profile',
|
|
||||||
completedSteps: [],
|
|
||||||
checkpoints: {},
|
|
||||||
importedDocuments: [],
|
|
||||||
gapAnalysis: null,
|
|
||||||
useCases: [],
|
|
||||||
activeUseCase: null,
|
|
||||||
screening: null,
|
|
||||||
modules: [],
|
|
||||||
requirements: [],
|
|
||||||
controls: [],
|
|
||||||
evidence: [],
|
|
||||||
checklist: [],
|
|
||||||
risks: [],
|
|
||||||
aiActClassification: null,
|
|
||||||
obligations: [],
|
|
||||||
dsfa: null,
|
|
||||||
toms: [],
|
|
||||||
retentionPolicies: [],
|
|
||||||
vvt: [],
|
|
||||||
documents: [],
|
|
||||||
cookieBanner: null,
|
|
||||||
consents: [],
|
|
||||||
dsrConfig: null,
|
|
||||||
escalationWorkflows: [],
|
|
||||||
preferences: {
|
|
||||||
language: 'de',
|
|
||||||
theme: 'light',
|
|
||||||
compactMode: false,
|
|
||||||
showHints: true,
|
|
||||||
autoSave: true,
|
|
||||||
autoValidate: true,
|
|
||||||
allowParallelWork: true,
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
} as SDKState
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeDecisionState(level: string = 'L2'): SDKState {
|
|
||||||
return makeState({
|
|
||||||
companyProfile: {
|
|
||||||
companyName: 'Test GmbH',
|
|
||||||
industry: 'IT-Dienstleistung',
|
|
||||||
employeeCount: 50,
|
|
||||||
businessModel: 'SaaS',
|
|
||||||
isPublicSector: false,
|
|
||||||
} as any,
|
|
||||||
complianceScope: {
|
|
||||||
decision: {
|
|
||||||
id: 'dec-1',
|
|
||||||
determinedLevel: level,
|
|
||||||
scores: { risk_score: 60, complexity_score: 50, assurance_need: 55, composite_score: 55 },
|
|
||||||
triggeredHardTriggers: [],
|
|
||||||
requiredDocuments: [
|
|
||||||
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: ['Bezeichnung', 'Zweck'], estimatedEffort: '2h', triggeredBy: [] },
|
|
||||||
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: ['Verschluesselung'], estimatedEffort: '3h', triggeredBy: [] },
|
|
||||||
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
|
|
||||||
],
|
|
||||||
riskFlags: [
|
|
||||||
{ id: 'rf-1', severity: 'MEDIUM', title: 'Cloud-Nutzung', description: '', recommendation: 'AVV pruefen' },
|
|
||||||
],
|
|
||||||
gaps: [
|
|
||||||
{ id: 'gap-1', severity: 'high', title: 'TOM fehlt', description: 'Keine TOM definiert', relatedDocuments: ['tom'] },
|
|
||||||
],
|
|
||||||
nextActions: [],
|
|
||||||
reasoning: [],
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
answers: [],
|
|
||||||
} as any,
|
|
||||||
vvt: [{ id: 'vvt-1', name: 'Kundenverwaltung' }] as any[],
|
|
||||||
toms: [],
|
|
||||||
retentionPolicies: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('projectForDraft', () => {
|
|
||||||
it('should return a DraftContext with correct structure', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
|
|
||||||
expect(result).toHaveProperty('decisions')
|
|
||||||
expect(result).toHaveProperty('companyProfile')
|
|
||||||
expect(result).toHaveProperty('constraints')
|
|
||||||
expect(result.decisions.level).toBe('L2')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should project company profile', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
|
|
||||||
expect(result.companyProfile.name).toBe('Test GmbH')
|
|
||||||
expect(result.companyProfile.industry).toBe('IT-Dienstleistung')
|
|
||||||
expect(result.companyProfile.employeeCount).toBe(50)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should provide defaults when no company profile', () => {
|
|
||||||
const state = makeState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
|
|
||||||
expect(result.companyProfile.name).toBe('Unbekannt')
|
|
||||||
expect(result.companyProfile.industry).toBe('Unbekannt')
|
|
||||||
expect(result.companyProfile.employeeCount).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should extract constraints and depth requirements', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
|
|
||||||
expect(result.constraints.depthRequirements).toBeDefined()
|
|
||||||
expect(result.constraints.boundaries.length).toBeGreaterThan(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should extract risk flags', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
|
|
||||||
expect(result.constraints.riskFlags.length).toBe(1)
|
|
||||||
expect(result.constraints.riskFlags[0].title).toBe('Cloud-Nutzung')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include existing document data when available', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
|
|
||||||
expect(result.existingDocumentData).toBeDefined()
|
|
||||||
expect((result.existingDocumentData as any).totalCount).toBe(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return undefined existingDocumentData when none exists', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForDraft(state, 'tom')
|
|
||||||
|
|
||||||
expect(result.existingDocumentData).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should filter required documents', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
|
|
||||||
expect(result.decisions.requiredDocuments.length).toBe(3)
|
|
||||||
expect(result.decisions.requiredDocuments.every(d => d.documentType)).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty state gracefully', () => {
|
|
||||||
const state = makeState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
|
|
||||||
expect(result.decisions.level).toBe('L1')
|
|
||||||
expect(result.decisions.hardTriggers).toEqual([])
|
|
||||||
expect(result.decisions.requiredDocuments).toEqual([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('projectForAsk', () => {
|
|
||||||
it('should return a GapContext with correct structure', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForAsk(state)
|
|
||||||
|
|
||||||
expect(result).toHaveProperty('unansweredQuestions')
|
|
||||||
expect(result).toHaveProperty('gaps')
|
|
||||||
expect(result).toHaveProperty('missingDocuments')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should identify missing documents', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
// vvt exists, tom and lf are missing
|
|
||||||
const result = projector.projectForAsk(state)
|
|
||||||
|
|
||||||
expect(result.missingDocuments.some(d => d.documentType === 'tom')).toBe(true)
|
|
||||||
expect(result.missingDocuments.some(d => d.documentType === 'lf')).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not list existing documents as missing', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForAsk(state)
|
|
||||||
|
|
||||||
// vvt exists in state
|
|
||||||
expect(result.missingDocuments.some(d => d.documentType === 'vvt')).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include gaps from scope decision', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForAsk(state)
|
|
||||||
|
|
||||||
expect(result.gaps.length).toBe(1)
|
|
||||||
expect(result.gaps[0].title).toBe('TOM fehlt')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty state', () => {
|
|
||||||
const state = makeState()
|
|
||||||
const result = projector.projectForAsk(state)
|
|
||||||
|
|
||||||
expect(result.gaps).toEqual([])
|
|
||||||
expect(result.missingDocuments).toEqual([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('projectForValidate', () => {
|
|
||||||
it('should return a ValidationContext with correct structure', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
|
||||||
|
|
||||||
expect(result).toHaveProperty('documents')
|
|
||||||
expect(result).toHaveProperty('crossReferences')
|
|
||||||
expect(result).toHaveProperty('scopeLevel')
|
|
||||||
expect(result).toHaveProperty('depthRequirements')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include all requested document types', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForValidate(state, ['vvt', 'tom'])
|
|
||||||
|
|
||||||
expect(result.documents.length).toBe(2)
|
|
||||||
expect(result.documents.map(d => d.type)).toContain('vvt')
|
|
||||||
expect(result.documents.map(d => d.type)).toContain('tom')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include cross-references', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
|
||||||
|
|
||||||
expect(result.crossReferences).toHaveProperty('vvtCategories')
|
|
||||||
expect(result.crossReferences).toHaveProperty('tomControls')
|
|
||||||
expect(result.crossReferences).toHaveProperty('retentionCategories')
|
|
||||||
expect(result.crossReferences.vvtCategories.length).toBe(1)
|
|
||||||
expect(result.crossReferences.vvtCategories[0]).toBe('Kundenverwaltung')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include scope level', () => {
|
|
||||||
const state = makeDecisionState('L3')
|
|
||||||
const result = projector.projectForValidate(state, ['vvt'])
|
|
||||||
|
|
||||||
expect(result.scopeLevel).toBe('L3')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should include depth requirements per document type', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForValidate(state, ['vvt', 'tom'])
|
|
||||||
|
|
||||||
expect(result.depthRequirements).toHaveProperty('vvt')
|
|
||||||
expect(result.depthRequirements).toHaveProperty('tom')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should summarize documents', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForValidate(state, ['vvt', 'tom'])
|
|
||||||
|
|
||||||
expect(result.documents[0].contentSummary).toContain('1')
|
|
||||||
expect(result.documents[1].contentSummary).toContain('Keine TOM')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should handle empty state', () => {
|
|
||||||
const state = makeState()
|
|
||||||
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
|
||||||
|
|
||||||
expect(result.scopeLevel).toBe('L1')
|
|
||||||
expect(result.crossReferences.vvtCategories).toEqual([])
|
|
||||||
expect(result.crossReferences.tomControls).toEqual([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('token budget estimation', () => {
|
|
||||||
it('projectForDraft should produce compact output', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForDraft(state, 'vvt')
|
|
||||||
const json = JSON.stringify(result)
|
|
||||||
|
|
||||||
// Rough token estimation: ~4 chars per token
|
|
||||||
const estimatedTokens = json.length / 4
|
|
||||||
expect(estimatedTokens).toBeLessThan(2000) // Budget is ~1500
|
|
||||||
})
|
|
||||||
|
|
||||||
it('projectForAsk should produce very compact output', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForAsk(state)
|
|
||||||
const json = JSON.stringify(result)
|
|
||||||
|
|
||||||
const estimatedTokens = json.length / 4
|
|
||||||
expect(estimatedTokens).toBeLessThan(1000) // Budget is ~600
|
|
||||||
})
|
|
||||||
|
|
||||||
it('projectForValidate should stay within budget', () => {
|
|
||||||
const state = makeDecisionState()
|
|
||||||
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
|
||||||
const json = JSON.stringify(result)
|
|
||||||
|
|
||||||
const estimatedTokens = json.length / 4
|
|
||||||
expect(estimatedTokens).toBeLessThan(3000) // Budget is ~2000
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
/**
|
|
||||||
* Constraint Enforcer - Hard Gate vor jedem Draft
|
|
||||||
*
|
|
||||||
* Stellt sicher, dass die Drafting Engine NIEMALS die deterministische
|
|
||||||
* Scope-Engine ueberschreibt. Prueft vor jedem Draft-Vorgang:
|
|
||||||
*
|
|
||||||
* 1. Ist der Dokumenttyp in requiredDocuments?
|
|
||||||
* 2. Passt die Draft-Tiefe zum Level?
|
|
||||||
* 3. Ist eine DSFA erforderlich (Hard Trigger)?
|
|
||||||
* 4. Werden Risiko-Flags beruecksichtigt?
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ScopeDecision, ScopeDocumentType, ComplianceDepthLevel } from '../compliance-scope-types'
|
|
||||||
import { DOCUMENT_SCOPE_MATRIX, getDepthLevelNumeric } from '../compliance-scope-types'
|
|
||||||
import type { ConstraintCheckResult, DraftContext } from './types'
|
|
||||||
|
|
||||||
export class ConstraintEnforcer {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prueft ob ein Draft fuer den gegebenen Dokumenttyp erlaubt ist.
|
|
||||||
* Dies ist ein HARD GATE - bei Violation wird der Draft blockiert.
|
|
||||||
*/
|
|
||||||
check(
|
|
||||||
documentType: ScopeDocumentType,
|
|
||||||
decision: ScopeDecision | null,
|
|
||||||
requestedDepthLevel?: ComplianceDepthLevel
|
|
||||||
): ConstraintCheckResult {
|
|
||||||
const violations: string[] = []
|
|
||||||
const adjustments: string[] = []
|
|
||||||
const checkedRules: string[] = []
|
|
||||||
|
|
||||||
// Wenn keine Decision vorhanden: Nur Basis-Drafts erlauben
|
|
||||||
if (!decision) {
|
|
||||||
checkedRules.push('RULE-NO-DECISION')
|
|
||||||
if (documentType !== 'vvt' && documentType !== 'tom' && documentType !== 'dsi') {
|
|
||||||
violations.push(
|
|
||||||
'Scope-Evaluierung fehlt. Bitte zuerst das Compliance-Profiling durchfuehren.'
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
adjustments.push(
|
|
||||||
'Ohne Scope-Evaluierung wird Level L1 (Basis) angenommen.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
allowed: violations.length === 0,
|
|
||||||
violations,
|
|
||||||
adjustments,
|
|
||||||
checkedRules,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const level = decision.determinedLevel
|
|
||||||
const levelNumeric = getDepthLevelNumeric(level)
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Rule 1: Dokumenttyp in requiredDocuments?
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
checkedRules.push('RULE-DOC-REQUIRED')
|
|
||||||
const isRequired = decision.requiredDocuments.some(
|
|
||||||
d => d.documentType === documentType && d.required
|
|
||||||
)
|
|
||||||
const scopeReq = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
|
|
||||||
|
|
||||||
if (!isRequired && scopeReq && !scopeReq.required) {
|
|
||||||
// Nicht blockieren, aber warnen
|
|
||||||
adjustments.push(
|
|
||||||
`Dokument "${documentType}" ist auf Level ${level} nicht als Pflicht eingestuft. ` +
|
|
||||||
`Entwurf ist moeglich, aber optional.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Rule 2: Draft-Tiefe passt zum Level?
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
checkedRules.push('RULE-DEPTH-MATCH')
|
|
||||||
if (requestedDepthLevel) {
|
|
||||||
const requestedNumeric = getDepthLevelNumeric(requestedDepthLevel)
|
|
||||||
|
|
||||||
if (requestedNumeric > levelNumeric) {
|
|
||||||
violations.push(
|
|
||||||
`Angefragte Tiefe ${requestedDepthLevel} ueberschreitet das bestimmte Level ${level}. ` +
|
|
||||||
`Die Scope-Engine hat Level ${level} festgelegt. ` +
|
|
||||||
`Ein Draft mit Tiefe ${requestedDepthLevel} ist nicht erlaubt.`
|
|
||||||
)
|
|
||||||
} else if (requestedNumeric < levelNumeric) {
|
|
||||||
adjustments.push(
|
|
||||||
`Angefragte Tiefe ${requestedDepthLevel} liegt unter dem bestimmten Level ${level}. ` +
|
|
||||||
`Draft wird auf Level ${level} angehoben.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Rule 3: DSFA-Enforcement
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
checkedRules.push('RULE-DSFA-ENFORCEMENT')
|
|
||||||
if (documentType === 'dsfa') {
|
|
||||||
const dsfaRequired = decision.triggeredHardTriggers.some(
|
|
||||||
t => t.rule.dsfaRequired
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!dsfaRequired && level !== 'L4') {
|
|
||||||
adjustments.push(
|
|
||||||
'DSFA ist laut Scope-Engine nicht verpflichtend. ' +
|
|
||||||
'Entwurf wird als freiwillige Massnahme gekennzeichnet.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Umgekehrt: Wenn DSFA verpflichtend und Typ != dsfa, ggf. hinweisen
|
|
||||||
if (documentType !== 'dsfa') {
|
|
||||||
const dsfaRequired = decision.triggeredHardTriggers.some(
|
|
||||||
t => t.rule.dsfaRequired
|
|
||||||
)
|
|
||||||
const dsfaInRequired = decision.requiredDocuments.some(
|
|
||||||
d => d.documentType === 'dsfa' && d.required
|
|
||||||
)
|
|
||||||
|
|
||||||
if (dsfaRequired && dsfaInRequired) {
|
|
||||||
// Nur ein Hinweis, kein Block
|
|
||||||
adjustments.push(
|
|
||||||
'Hinweis: Eine DSFA ist laut Scope-Engine verpflichtend. ' +
|
|
||||||
'Bitte sicherstellen, dass auch eine DSFA erstellt wird.'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Rule 4: Risiko-Flags beruecksichtigt?
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
checkedRules.push('RULE-RISK-FLAGS')
|
|
||||||
const criticalRisks = decision.riskFlags.filter(
|
|
||||||
f => f.severity === 'CRITICAL' || f.severity === 'HIGH'
|
|
||||||
)
|
|
||||||
|
|
||||||
if (criticalRisks.length > 0) {
|
|
||||||
adjustments.push(
|
|
||||||
`${criticalRisks.length} kritische/hohe Risiko-Flags erkannt. ` +
|
|
||||||
`Draft muss diese adressieren: ${criticalRisks.map(r => r.title).join(', ')}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
// Rule 5: Hard-Trigger Consistency
|
|
||||||
// -----------------------------------------------------------------------
|
|
||||||
checkedRules.push('RULE-HARD-TRIGGER-CONSISTENCY')
|
|
||||||
for (const trigger of decision.triggeredHardTriggers) {
|
|
||||||
const mandatoryDocs = trigger.rule.mandatoryDocuments
|
|
||||||
if (mandatoryDocs.includes(documentType)) {
|
|
||||||
// Gut - wir erstellen ein mandatory document
|
|
||||||
} else {
|
|
||||||
// Pruefen ob die mandatory documents des Triggers vorhanden sind
|
|
||||||
// (nur Hinweis, kein Block)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
allowed: violations.length === 0,
|
|
||||||
violations,
|
|
||||||
adjustments,
|
|
||||||
checkedRules,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience: Prueft aus einem DraftContext heraus.
|
|
||||||
*/
|
|
||||||
checkFromContext(
|
|
||||||
documentType: ScopeDocumentType,
|
|
||||||
context: DraftContext
|
|
||||||
): ConstraintCheckResult {
|
|
||||||
// Reconstruct a minimal ScopeDecision from context
|
|
||||||
const pseudoDecision: ScopeDecision = {
|
|
||||||
id: 'projected',
|
|
||||||
determinedLevel: context.decisions.level,
|
|
||||||
scores: context.decisions.scores,
|
|
||||||
triggeredHardTriggers: context.decisions.hardTriggers.map(t => ({
|
|
||||||
rule: {
|
|
||||||
id: t.id,
|
|
||||||
label: t.label,
|
|
||||||
description: '',
|
|
||||||
conditionField: '',
|
|
||||||
conditionOperator: 'EQUALS' as const,
|
|
||||||
conditionValue: null,
|
|
||||||
minimumLevel: context.decisions.level,
|
|
||||||
mandatoryDocuments: [],
|
|
||||||
dsfaRequired: false,
|
|
||||||
legalReference: t.legalReference,
|
|
||||||
},
|
|
||||||
matchedValue: null,
|
|
||||||
explanation: '',
|
|
||||||
})),
|
|
||||||
requiredDocuments: context.decisions.requiredDocuments.map(d => ({
|
|
||||||
documentType: d.documentType,
|
|
||||||
label: d.documentType,
|
|
||||||
required: true,
|
|
||||||
depth: d.depth,
|
|
||||||
detailItems: d.detailItems,
|
|
||||||
estimatedEffort: '',
|
|
||||||
triggeredBy: [],
|
|
||||||
})),
|
|
||||||
riskFlags: context.constraints.riskFlags.map(f => ({
|
|
||||||
id: `rf-${f.title}`,
|
|
||||||
severity: f.severity as 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL',
|
|
||||||
title: f.title,
|
|
||||||
description: '',
|
|
||||||
recommendation: f.recommendation,
|
|
||||||
})),
|
|
||||||
gaps: [],
|
|
||||||
nextActions: [],
|
|
||||||
reasoning: [],
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.check(documentType, pseudoDecision)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Singleton-Instanz */
|
|
||||||
export const constraintEnforcer = new ConstraintEnforcer()
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
/**
|
|
||||||
* Intent Classifier - Leichtgewichtiger Pattern-Matcher
|
|
||||||
*
|
|
||||||
* Erkennt den Agent-Modus anhand des Nutzer-Inputs ohne LLM-Call.
|
|
||||||
* Deutsche und englische Muster werden unterstuetzt.
|
|
||||||
*
|
|
||||||
* Confidence-Schwellen:
|
|
||||||
* - >0.8: Hohe Sicherheit, automatisch anwenden
|
|
||||||
* - 0.6-0.8: Mittel, Nutzer kann bestaetigen
|
|
||||||
* - <0.6: Fallback zu 'explain'
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { AgentMode, IntentClassification } from './types'
|
|
||||||
import type { ScopeDocumentType } from '../compliance-scope-types'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Pattern Definitions
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
interface ModePattern {
|
|
||||||
mode: AgentMode
|
|
||||||
patterns: RegExp[]
|
|
||||||
/** Base-Confidence wenn ein Pattern matched */
|
|
||||||
baseConfidence: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const MODE_PATTERNS: ModePattern[] = [
|
|
||||||
{
|
|
||||||
mode: 'draft',
|
|
||||||
baseConfidence: 0.85,
|
|
||||||
patterns: [
|
|
||||||
/\b(erstell|generier|entw[iu]rf|entwer[ft]|schreib|verfass|formulier|anlege)/i,
|
|
||||||
/\b(draft|create|generate|write|compose)\b/i,
|
|
||||||
/\b(neues?\s+(?:vvt|tom|dsfa|dokument|loeschkonzept|datenschutzerklaerung))\b/i,
|
|
||||||
/\b(vorlage|template)\s+(erstell|generier)/i,
|
|
||||||
/\bfuer\s+(?:uns|mich|unser)\b.*\b(erstell|schreib)/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mode: 'validate',
|
|
||||||
baseConfidence: 0.80,
|
|
||||||
patterns: [
|
|
||||||
/\b(pruef|validier|check|kontrollier|ueberpruef)\b/i,
|
|
||||||
/\b(korrekt|richtig|vollstaendig|konsistent|komplett)\b.*\?/i,
|
|
||||||
/\b(stimmt|passt)\b.*\b(das|mein|unser)\b/i,
|
|
||||||
/\b(validate|verify|check|review)\b/i,
|
|
||||||
/\b(fehler|luecken?|maengel)\b.*\b(find|such|zeig)\b/i,
|
|
||||||
/\bcross[\s-]?check\b/i,
|
|
||||||
/\b(vvt|tom|dsfa)\b.*\b(konsisten[tz]|widerspruch|uebereinstimm)/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
mode: 'ask',
|
|
||||||
baseConfidence: 0.75,
|
|
||||||
patterns: [
|
|
||||||
/\bwas\s+fehlt\b/i,
|
|
||||||
/\b(luecken?|gaps?)\b.*\b(zeig|find|identifizier|analysier)/i,
|
|
||||||
/\b(unvollstaendig|unfertig|offen)\b/i,
|
|
||||||
/\bwelche\s+(dokumente?|informationen?|daten)\b.*\b(fehlen?|brauch|benoetig)/i,
|
|
||||||
/\b(naechste[rn]?\s+schritt|next\s+step|todo)\b/i,
|
|
||||||
/\bworan\s+(muss|soll)\b/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
/** Dokumenttyp-Erkennung */
|
|
||||||
const DOCUMENT_TYPE_PATTERNS: Array<{
|
|
||||||
type: ScopeDocumentType
|
|
||||||
patterns: RegExp[]
|
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
type: 'vvt',
|
|
||||||
patterns: [
|
|
||||||
/\bv{1,2}t\b/i,
|
|
||||||
/\bverarbeitungsverzeichnis\b/i,
|
|
||||||
/\bverarbeitungstaetigkeit/i,
|
|
||||||
/\bprocessing\s+activit/i,
|
|
||||||
/\bart\.?\s*30\b/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'tom',
|
|
||||||
patterns: [
|
|
||||||
/\btom\b/i,
|
|
||||||
/\btechnisch.*organisatorisch.*massnahm/i,
|
|
||||||
/\bart\.?\s*32\b/i,
|
|
||||||
/\bsicherheitsmassnahm/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'dsfa',
|
|
||||||
patterns: [
|
|
||||||
/\bdsfa\b/i,
|
|
||||||
/\bdatenschutz[\s-]?folgenabschaetzung\b/i,
|
|
||||||
/\bdpia\b/i,
|
|
||||||
/\bart\.?\s*35\b/i,
|
|
||||||
/\bimpact\s+assessment\b/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'dsi',
|
|
||||||
patterns: [
|
|
||||||
/\bdatenschutzerklaerung\b/i,
|
|
||||||
/\bprivacy\s+policy\b/i,
|
|
||||||
/\bdsi\b/i,
|
|
||||||
/\bart\.?\s*13\b/i,
|
|
||||||
/\bart\.?\s*14\b/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'lf',
|
|
||||||
patterns: [
|
|
||||||
/\bloeschfrist/i,
|
|
||||||
/\bloeschkonzept/i,
|
|
||||||
/\bretention/i,
|
|
||||||
/\baufbewahr/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'av_vertrag',
|
|
||||||
patterns: [
|
|
||||||
/\bavv?\b/i,
|
|
||||||
/\bauftragsverarbeit/i,
|
|
||||||
/\bdata\s+processing\s+agreement/i,
|
|
||||||
/\bart\.?\s*28\b/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'betroffenenrechte',
|
|
||||||
patterns: [
|
|
||||||
/\bbetroffenenrecht/i,
|
|
||||||
/\bdata\s+subject\s+right/i,
|
|
||||||
/\bart\.?\s*15\b/i,
|
|
||||||
/\bauskunft/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'einwilligung',
|
|
||||||
patterns: [
|
|
||||||
/\beinwillig/i,
|
|
||||||
/\bconsent/i,
|
|
||||||
/\bcookie/i,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Classifier
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export class IntentClassifier {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Klassifiziert die Nutzerabsicht anhand des Inputs.
|
|
||||||
*
|
|
||||||
* @param input - Die Nutzer-Nachricht
|
|
||||||
* @returns IntentClassification mit Mode, Confidence, Patterns
|
|
||||||
*/
|
|
||||||
classify(input: string): IntentClassification {
|
|
||||||
const normalized = this.normalize(input)
|
|
||||||
let bestMatch: IntentClassification = {
|
|
||||||
mode: 'explain',
|
|
||||||
confidence: 0.3,
|
|
||||||
matchedPatterns: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const modePattern of MODE_PATTERNS) {
|
|
||||||
const matched: string[] = []
|
|
||||||
|
|
||||||
for (const pattern of modePattern.patterns) {
|
|
||||||
if (pattern.test(normalized)) {
|
|
||||||
matched.push(pattern.source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matched.length > 0) {
|
|
||||||
// Mehr Matches = hoehere Confidence (bis zum Maximum)
|
|
||||||
const matchBonus = Math.min(matched.length - 1, 2) * 0.05
|
|
||||||
const confidence = Math.min(modePattern.baseConfidence + matchBonus, 0.99)
|
|
||||||
|
|
||||||
if (confidence > bestMatch.confidence) {
|
|
||||||
bestMatch = {
|
|
||||||
mode: modePattern.mode,
|
|
||||||
confidence,
|
|
||||||
matchedPatterns: matched,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dokumenttyp erkennen
|
|
||||||
const detectedDocType = this.detectDocumentType(normalized)
|
|
||||||
if (detectedDocType) {
|
|
||||||
bestMatch.detectedDocumentType = detectedDocType
|
|
||||||
// Dokumenttyp-Erkennung erhoeht Confidence leicht
|
|
||||||
bestMatch.confidence = Math.min(bestMatch.confidence + 0.05, 0.99)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Bei Confidence <0.6 immer 'explain'
|
|
||||||
if (bestMatch.confidence < 0.6) {
|
|
||||||
bestMatch.mode = 'explain'
|
|
||||||
}
|
|
||||||
|
|
||||||
return bestMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erkennt den Dokumenttyp aus dem Input.
|
|
||||||
*/
|
|
||||||
detectDocumentType(input: string): ScopeDocumentType | undefined {
|
|
||||||
const normalized = this.normalize(input)
|
|
||||||
|
|
||||||
for (const docPattern of DOCUMENT_TYPE_PATTERNS) {
|
|
||||||
for (const pattern of docPattern.patterns) {
|
|
||||||
if (pattern.test(normalized)) {
|
|
||||||
return docPattern.type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalisiert den Input fuer Pattern-Matching.
|
|
||||||
* Ersetzt Umlaute, entfernt Sonderzeichen.
|
|
||||||
*/
|
|
||||||
private normalize(input: string): string {
|
|
||||||
return input
|
|
||||||
.replace(/ä/g, 'ae')
|
|
||||||
.replace(/ö/g, 'oe')
|
|
||||||
.replace(/ü/g, 'ue')
|
|
||||||
.replace(/ß/g, 'ss')
|
|
||||||
.replace(/Ä/g, 'Ae')
|
|
||||||
.replace(/Ö/g, 'Oe')
|
|
||||||
.replace(/Ü/g, 'Ue')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Singleton-Instanz */
|
|
||||||
export const intentClassifier = new IntentClassifier()
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Gap Analysis Prompt - Lueckenanalyse und gezielte Fragen
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { GapContext } from '../types'
|
|
||||||
|
|
||||||
export interface GapAnalysisInput {
|
|
||||||
context: GapContext
|
|
||||||
instructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildGapAnalysisPrompt(input: GapAnalysisInput): string {
|
|
||||||
const { context, instructions } = input
|
|
||||||
|
|
||||||
return `## Aufgabe: Compliance-Lueckenanalyse
|
|
||||||
|
|
||||||
### Identifizierte Luecken:
|
|
||||||
${context.gaps.length > 0
|
|
||||||
? context.gaps.map(g => `- [${g.severity}] ${g.title}: ${g.description}`).join('\n')
|
|
||||||
: '- Keine Luecken identifiziert'}
|
|
||||||
|
|
||||||
### Fehlende Pflichtdokumente:
|
|
||||||
${context.missingDocuments.length > 0
|
|
||||||
? context.missingDocuments.map(d => `- ${d.label} (Tiefe: ${d.depth}, Aufwand: ${d.estimatedEffort})`).join('\n')
|
|
||||||
: '- Alle Pflichtdokumente vorhanden'}
|
|
||||||
|
|
||||||
### Unbeantwortete Fragen:
|
|
||||||
${context.unansweredQuestions.length > 0
|
|
||||||
? context.unansweredQuestions.map(q => `- [${q.blockId}] ${q.question}`).join('\n')
|
|
||||||
: '- Alle Fragen beantwortet'}
|
|
||||||
|
|
||||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
|
||||||
|
|
||||||
### Aufgabe:
|
|
||||||
Analysiere den Stand und stelle EINE gezielte Frage, die die wichtigste Luecke adressiert.
|
|
||||||
Priorisiere nach:
|
|
||||||
1. Fehlende Pflichtdokumente
|
|
||||||
2. Kritische Luecken (HIGH/CRITICAL severity)
|
|
||||||
3. Unbeantwortete Pflichtfragen
|
|
||||||
4. Mittlere Luecken
|
|
||||||
|
|
||||||
### Antwort-Format:
|
|
||||||
Antworte in dieser Struktur:
|
|
||||||
1. **Statusuebersicht**: Kurze Zusammenfassung des Compliance-Stands (2-3 Saetze)
|
|
||||||
2. **Wichtigste Luecke**: Was fehlt am dringendsten?
|
|
||||||
3. **Gezielte Frage**: Eine konkrete Frage an den Nutzer
|
|
||||||
4. **Warum wichtig**: Warum muss diese Luecke geschlossen werden?
|
|
||||||
5. **Empfohlener naechster Schritt**: Link/Verweis zum SDK-Modul`
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/**
|
|
||||||
* DSFA Draft Prompt - Datenschutz-Folgenabschaetzung (Art. 35 DSGVO)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { DraftContext } from '../types'
|
|
||||||
|
|
||||||
export interface DSFADraftInput {
|
|
||||||
context: DraftContext
|
|
||||||
processingDescription?: string
|
|
||||||
instructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildDSFADraftPrompt(input: DSFADraftInput): string {
|
|
||||||
const { context, processingDescription, instructions } = input
|
|
||||||
const level = context.decisions.level
|
|
||||||
const depthItems = context.constraints.depthRequirements.detailItems
|
|
||||||
const hardTriggers = context.decisions.hardTriggers
|
|
||||||
|
|
||||||
return `## Aufgabe: DSFA entwerfen (Art. 35 DSGVO)
|
|
||||||
|
|
||||||
### Unternehmensprofil
|
|
||||||
- Name: ${context.companyProfile.name}
|
|
||||||
- Branche: ${context.companyProfile.industry}
|
|
||||||
- Mitarbeiter: ${context.companyProfile.employeeCount}
|
|
||||||
|
|
||||||
### Compliance-Level: ${level}
|
|
||||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
|
||||||
|
|
||||||
### Hard Triggers (Gruende fuer DSFA-Pflicht):
|
|
||||||
${hardTriggers.length > 0
|
|
||||||
? hardTriggers.map(t => `- ${t.id}: ${t.label} (${t.legalReference})`).join('\n')
|
|
||||||
: '- Keine Hard Triggers (DSFA auf Wunsch)'}
|
|
||||||
|
|
||||||
### Erforderliche Inhalte:
|
|
||||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
|
||||||
|
|
||||||
${processingDescription ? `### Beschreibung der Verarbeitung: ${processingDescription}` : ''}
|
|
||||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
|
||||||
|
|
||||||
### Antwort-Format
|
|
||||||
Antworte als JSON:
|
|
||||||
{
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"id": "beschreibung",
|
|
||||||
"title": "Systematische Beschreibung der Verarbeitung",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "processingDescription"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "notwendigkeit",
|
|
||||||
"title": "Notwendigkeit und Verhaeltnismaessigkeit",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "necessityAssessment"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "risikobewertung",
|
|
||||||
"title": "Bewertung der Risiken fuer die Rechte und Freiheiten",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "riskAssessment"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "massnahmen",
|
|
||||||
"title": "Massnahmen zur Eindaemmung der Risiken",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "mitigationMeasures"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "stellungnahme_dsb",
|
|
||||||
"title": "Stellungnahme des Datenschutzbeauftragten",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "dpoOpinion"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "standpunkt_betroffene",
|
|
||||||
"title": "Standpunkt der betroffenen Personen",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "dataSubjectView"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ergebnis",
|
|
||||||
"title": "Ergebnis und Empfehlung",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "conclusion"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Halte die Tiefe exakt auf Level ${level}.
|
|
||||||
Nutze WP248-Kriterien als Leitfaden fuer die Risikobewertung.`
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/**
|
|
||||||
* Loeschfristen Draft Prompt - Loeschkonzept
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { DraftContext } from '../types'
|
|
||||||
|
|
||||||
export interface LoeschfristenDraftInput {
|
|
||||||
context: DraftContext
|
|
||||||
instructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildLoeschfristenDraftPrompt(input: LoeschfristenDraftInput): string {
|
|
||||||
const { context, instructions } = input
|
|
||||||
const level = context.decisions.level
|
|
||||||
const depthItems = context.constraints.depthRequirements.detailItems
|
|
||||||
|
|
||||||
return `## Aufgabe: Loeschkonzept / Loeschfristen entwerfen
|
|
||||||
|
|
||||||
### Unternehmensprofil
|
|
||||||
- Name: ${context.companyProfile.name}
|
|
||||||
- Branche: ${context.companyProfile.industry}
|
|
||||||
- Mitarbeiter: ${context.companyProfile.employeeCount}
|
|
||||||
|
|
||||||
### Compliance-Level: ${level}
|
|
||||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
|
||||||
|
|
||||||
### Erforderliche Inhalte:
|
|
||||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
|
||||||
|
|
||||||
${context.existingDocumentData ? `### Bestehende Loeschfristen: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
|
|
||||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
|
||||||
|
|
||||||
### Antwort-Format
|
|
||||||
Antworte als JSON:
|
|
||||||
{
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"id": "grundsaetze",
|
|
||||||
"title": "Grundsaetze der Datenlöschung",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "principles"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "kategorien",
|
|
||||||
"title": "Datenkategorien und Loeschfristen",
|
|
||||||
"content": "Tabellarische Uebersicht...",
|
|
||||||
"schemaField": "retentionSchedule"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "gesetzliche_fristen",
|
|
||||||
"title": "Gesetzliche Aufbewahrungsfristen",
|
|
||||||
"content": "HGB, AO, weitere...",
|
|
||||||
"schemaField": "legalRetention"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "loeschprozess",
|
|
||||||
"title": "Technischer Loeschprozess",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "deletionProcess"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "verantwortlichkeiten",
|
|
||||||
"title": "Verantwortlichkeiten",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "responsibilities"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "ausnahmen",
|
|
||||||
"title": "Ausnahmen und Sonderfaelle",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "exceptions"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Halte die Tiefe exakt auf Level ${level}.
|
|
||||||
Beruecksichtige branchenspezifische Aufbewahrungsfristen fuer ${context.companyProfile.industry}.`
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
/**
|
|
||||||
* Privacy Policy Draft Prompt - Datenschutzerklaerung (Art. 13/14 DSGVO)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { DraftContext } from '../types'
|
|
||||||
|
|
||||||
export interface PrivacyPolicyDraftInput {
|
|
||||||
context: DraftContext
|
|
||||||
websiteUrl?: string
|
|
||||||
instructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildPrivacyPolicyDraftPrompt(input: PrivacyPolicyDraftInput): string {
|
|
||||||
const { context, websiteUrl, instructions } = input
|
|
||||||
const level = context.decisions.level
|
|
||||||
const depthItems = context.constraints.depthRequirements.detailItems
|
|
||||||
|
|
||||||
return `## Aufgabe: Datenschutzerklaerung entwerfen (Art. 13/14 DSGVO)
|
|
||||||
|
|
||||||
### Unternehmensprofil
|
|
||||||
- Name: ${context.companyProfile.name}
|
|
||||||
- Branche: ${context.companyProfile.industry}
|
|
||||||
${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : ''}
|
|
||||||
${websiteUrl ? `- Website: ${websiteUrl}` : ''}
|
|
||||||
|
|
||||||
### Compliance-Level: ${level}
|
|
||||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
|
||||||
|
|
||||||
### Erforderliche Inhalte:
|
|
||||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
|
||||||
|
|
||||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
|
||||||
|
|
||||||
### Antwort-Format
|
|
||||||
Antworte als JSON:
|
|
||||||
{
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"id": "verantwortlicher",
|
|
||||||
"title": "Verantwortlicher",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "controller"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "dsb",
|
|
||||||
"title": "Datenschutzbeauftragter",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "dpo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "verarbeitungen",
|
|
||||||
"title": "Verarbeitungstaetigkeiten und Zwecke",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "processingPurposes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "rechtsgrundlagen",
|
|
||||||
"title": "Rechtsgrundlagen der Verarbeitung",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "legalBases"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empfaenger",
|
|
||||||
"title": "Empfaenger und Datenweitergabe",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "recipients"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "drittland",
|
|
||||||
"title": "Uebermittlung in Drittlaender",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "thirdCountryTransfers"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "speicherdauer",
|
|
||||||
"title": "Speicherdauer",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "retentionPeriods"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "betroffenenrechte",
|
|
||||||
"title": "Ihre Rechte als betroffene Person",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "dataSubjectRights"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cookies",
|
|
||||||
"title": "Cookies und Tracking",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "cookies"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "aenderungen",
|
|
||||||
"title": "Aenderungen dieser Datenschutzerklaerung",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "changes"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Halte die Tiefe exakt auf Level ${level}.`
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
/**
|
|
||||||
* TOM Draft Prompt - Technische und Organisatorische Massnahmen (Art. 32 DSGVO)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { DraftContext } from '../types'
|
|
||||||
|
|
||||||
export interface TOMDraftInput {
|
|
||||||
context: DraftContext
|
|
||||||
focusArea?: string
|
|
||||||
instructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildTOMDraftPrompt(input: TOMDraftInput): string {
|
|
||||||
const { context, focusArea, instructions } = input
|
|
||||||
const level = context.decisions.level
|
|
||||||
const depthItems = context.constraints.depthRequirements.detailItems
|
|
||||||
|
|
||||||
return `## Aufgabe: TOM-Dokument entwerfen (Art. 32 DSGVO)
|
|
||||||
|
|
||||||
### Unternehmensprofil
|
|
||||||
- Name: ${context.companyProfile.name}
|
|
||||||
- Branche: ${context.companyProfile.industry}
|
|
||||||
- Mitarbeiter: ${context.companyProfile.employeeCount}
|
|
||||||
|
|
||||||
### Compliance-Level: ${level}
|
|
||||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
|
||||||
|
|
||||||
### Erforderliche Inhalte fuer Level ${level}:
|
|
||||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
|
||||||
|
|
||||||
### Constraints
|
|
||||||
${context.constraints.boundaries.map(b => `- ${b}`).join('\n')}
|
|
||||||
|
|
||||||
${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags
|
|
||||||
${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}`).join('\n')}` : ''}
|
|
||||||
|
|
||||||
${focusArea ? `### Fokusbereich: ${focusArea}` : ''}
|
|
||||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
|
||||||
|
|
||||||
${context.existingDocumentData ? `### Bestehende TOM: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
|
|
||||||
|
|
||||||
### Antwort-Format
|
|
||||||
Antworte als JSON:
|
|
||||||
{
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"id": "zutrittskontrolle",
|
|
||||||
"title": "Zutrittskontrolle",
|
|
||||||
"content": "Massnahmen die unbefugten Zutritt zu Datenverarbeitungsanlagen verhindern...",
|
|
||||||
"schemaField": "accessControl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zugangskontrolle",
|
|
||||||
"title": "Zugangskontrolle",
|
|
||||||
"content": "Massnahmen gegen unbefugte Systemnutzung...",
|
|
||||||
"schemaField": "systemAccessControl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zugriffskontrolle",
|
|
||||||
"title": "Zugriffskontrolle",
|
|
||||||
"content": "Massnahmen zur Sicherstellung berechtigter Datenzugriffe...",
|
|
||||||
"schemaField": "dataAccessControl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "weitergabekontrolle",
|
|
||||||
"title": "Weitergabekontrolle / Uebertragungssicherheit",
|
|
||||||
"content": "Massnahmen bei Datenuebertragung und -transport...",
|
|
||||||
"schemaField": "transferControl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "eingabekontrolle",
|
|
||||||
"title": "Eingabekontrolle",
|
|
||||||
"content": "Nachvollziehbarkeit von Dateneingaben...",
|
|
||||||
"schemaField": "inputControl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "auftragskontrolle",
|
|
||||||
"title": "Auftragskontrolle",
|
|
||||||
"content": "Massnahmen zur weisungsgemaessen Auftragsverarbeitung...",
|
|
||||||
"schemaField": "orderControl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "verfuegbarkeitskontrolle",
|
|
||||||
"title": "Verfuegbarkeitskontrolle",
|
|
||||||
"content": "Schutz gegen Datenverlust...",
|
|
||||||
"schemaField": "availabilityControl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "trennungsgebot",
|
|
||||||
"title": "Trennungsgebot",
|
|
||||||
"content": "Getrennte Verarbeitung fuer verschiedene Zwecke...",
|
|
||||||
"schemaField": "separationControl"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Fuelle fehlende Informationen mit [PLATZHALTER: ...].
|
|
||||||
Halte die Tiefe exakt auf Level ${level}.`
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
/**
|
|
||||||
* VVT Draft Prompt - Verarbeitungsverzeichnis (Art. 30 DSGVO)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { DraftContext } from '../types'
|
|
||||||
|
|
||||||
export interface VVTDraftInput {
|
|
||||||
context: DraftContext
|
|
||||||
activityName?: string
|
|
||||||
activityPurpose?: string
|
|
||||||
instructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildVVTDraftPrompt(input: VVTDraftInput): string {
|
|
||||||
const { context, activityName, activityPurpose, instructions } = input
|
|
||||||
const level = context.decisions.level
|
|
||||||
const depthItems = context.constraints.depthRequirements.detailItems
|
|
||||||
|
|
||||||
return `## Aufgabe: VVT-Eintrag entwerfen (Art. 30 DSGVO)
|
|
||||||
|
|
||||||
### Unternehmensprofil
|
|
||||||
- Name: ${context.companyProfile.name}
|
|
||||||
- Branche: ${context.companyProfile.industry}
|
|
||||||
- Mitarbeiter: ${context.companyProfile.employeeCount}
|
|
||||||
- Geschaeftsmodell: ${context.companyProfile.businessModel}
|
|
||||||
${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : '- DSB: Nicht benannt'}
|
|
||||||
|
|
||||||
### Compliance-Level: ${level}
|
|
||||||
Tiefe: ${context.constraints.depthRequirements.depth}
|
|
||||||
|
|
||||||
### Erforderliche Inhalte fuer Level ${level}:
|
|
||||||
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
|
|
||||||
|
|
||||||
### Constraints
|
|
||||||
${context.constraints.boundaries.map(b => `- ${b}`).join('\n')}
|
|
||||||
|
|
||||||
${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags
|
|
||||||
${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}: ${f.recommendation}`).join('\n')}` : ''}
|
|
||||||
|
|
||||||
${activityName ? `### Gewuenschte Verarbeitungstaetigkeit: ${activityName}` : ''}
|
|
||||||
${activityPurpose ? `### Zweck: ${activityPurpose}` : ''}
|
|
||||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
|
||||||
|
|
||||||
${context.existingDocumentData ? `### Bestehende VVT-Eintraege: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
|
|
||||||
|
|
||||||
### Antwort-Format
|
|
||||||
Antworte als JSON:
|
|
||||||
{
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"id": "bezeichnung",
|
|
||||||
"title": "Bezeichnung der Verarbeitungstaetigkeit",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "name"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "verantwortlicher",
|
|
||||||
"title": "Verantwortlicher",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "controller"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "zweck",
|
|
||||||
"title": "Zweck der Verarbeitung",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "purpose"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "rechtsgrundlage",
|
|
||||||
"title": "Rechtsgrundlage",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "legalBasis"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "betroffene",
|
|
||||||
"title": "Kategorien betroffener Personen",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "dataSubjects"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "datenkategorien",
|
|
||||||
"title": "Kategorien personenbezogener Daten",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "dataCategories"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "empfaenger",
|
|
||||||
"title": "Empfaenger",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "recipients"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "speicherdauer",
|
|
||||||
"title": "Speicherdauer / Loeschfristen",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "retentionPeriod"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "tom_referenz",
|
|
||||||
"title": "TOM-Referenz",
|
|
||||||
"content": "...",
|
|
||||||
"schemaField": "tomReference"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Fuelle fehlende Informationen mit [PLATZHALTER: Beschreibung was hier eingetragen werden muss].
|
|
||||||
Halte die Tiefe exakt auf Level ${level} (${context.constraints.depthRequirements.depth}).`
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/**
|
|
||||||
* Drafting Engine Prompts - Re-Exports
|
|
||||||
*/
|
|
||||||
|
|
||||||
export { buildVVTDraftPrompt, type VVTDraftInput } from './draft-vvt'
|
|
||||||
export { buildTOMDraftPrompt, type TOMDraftInput } from './draft-tom'
|
|
||||||
export { buildDSFADraftPrompt, type DSFADraftInput } from './draft-dsfa'
|
|
||||||
export { buildPrivacyPolicyDraftPrompt, type PrivacyPolicyDraftInput } from './draft-privacy-policy'
|
|
||||||
export { buildLoeschfristenDraftPrompt, type LoeschfristenDraftInput } from './draft-loeschfristen'
|
|
||||||
export { buildCrossCheckPrompt, type CrossCheckInput } from './validate-cross-check'
|
|
||||||
export { buildGapAnalysisPrompt, type GapAnalysisInput } from './ask-gap-analysis'
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
/**
|
|
||||||
* Cross-Document Validation Prompt
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ValidationContext } from '../types'
|
|
||||||
|
|
||||||
export interface CrossCheckInput {
|
|
||||||
context: ValidationContext
|
|
||||||
focusDocuments?: string[]
|
|
||||||
instructions?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildCrossCheckPrompt(input: CrossCheckInput): string {
|
|
||||||
const { context, focusDocuments, instructions } = input
|
|
||||||
|
|
||||||
return `## Aufgabe: Cross-Dokument-Konsistenzpruefung
|
|
||||||
|
|
||||||
### Scope-Level: ${context.scopeLevel}
|
|
||||||
|
|
||||||
### Vorhandene Dokumente:
|
|
||||||
${context.documents.map(d => `- ${d.type}: ${d.contentSummary}`).join('\n')}
|
|
||||||
|
|
||||||
### Cross-Referenzen:
|
|
||||||
- VVT-Kategorien: ${context.crossReferences.vvtCategories.join(', ') || 'Keine'}
|
|
||||||
- DSFA-Risiken: ${context.crossReferences.dsfaRisks.join(', ') || 'Keine'}
|
|
||||||
- TOM-Controls: ${context.crossReferences.tomControls.join(', ') || 'Keine'}
|
|
||||||
- Loeschfristen-Kategorien: ${context.crossReferences.retentionCategories.join(', ') || 'Keine'}
|
|
||||||
|
|
||||||
### Tiefenpruefung pro Dokument:
|
|
||||||
${context.documents.map(d => {
|
|
||||||
const req = context.depthRequirements[d.type]
|
|
||||||
return req ? `- ${d.type}: Erforderlich=${req.required}, Tiefe=${req.depth}` : `- ${d.type}: Keine Requirements`
|
|
||||||
}).join('\n')}
|
|
||||||
|
|
||||||
${focusDocuments ? `### Fokus auf: ${focusDocuments.join(', ')}` : ''}
|
|
||||||
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
|
|
||||||
|
|
||||||
### Pruefkriterien:
|
|
||||||
1. Jede VVT-Taetigkeit muss einen TOM-Verweis haben
|
|
||||||
2. Jede VVT-Kategorie muss eine Loeschfrist haben
|
|
||||||
3. Bei DSFA-pflichtigen Verarbeitungen muss eine DSFA existieren
|
|
||||||
4. TOM-Massnahmen muessen zum Risikoprofil passen
|
|
||||||
5. Loeschfristen duerfen gesetzliche Minima nicht unterschreiten
|
|
||||||
6. Dokument-Tiefe muss Level ${context.scopeLevel} entsprechen
|
|
||||||
|
|
||||||
### Antwort-Format
|
|
||||||
Antworte als JSON:
|
|
||||||
{
|
|
||||||
"passed": true/false,
|
|
||||||
"errors": [
|
|
||||||
{
|
|
||||||
"id": "ERR-001",
|
|
||||||
"severity": "error",
|
|
||||||
"category": "scope_violation|inconsistency|missing_content|depth_mismatch|cross_reference",
|
|
||||||
"title": "...",
|
|
||||||
"description": "...",
|
|
||||||
"documentType": "vvt|tom|dsfa|...",
|
|
||||||
"crossReferenceType": "...",
|
|
||||||
"legalReference": "Art. ... DSGVO",
|
|
||||||
"suggestion": "..."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"warnings": [...],
|
|
||||||
"suggestions": [...]
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
/**
|
|
||||||
* State Projector - Token-budgetierte Projektion des SDK-State
|
|
||||||
*
|
|
||||||
* Extrahiert aus dem vollen SDKState (der ~50k Tokens betragen kann) nur die
|
|
||||||
* relevanten Slices fuer den jeweiligen Agent-Modus.
|
|
||||||
*
|
|
||||||
* Token-Budgets:
|
|
||||||
* - Draft: ~1500 Tokens
|
|
||||||
* - Ask: ~600 Tokens
|
|
||||||
* - Validate: ~2000 Tokens
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { SDKState, CompanyProfile } from '../types'
|
|
||||||
import type {
|
|
||||||
ComplianceScopeState,
|
|
||||||
ScopeDecision,
|
|
||||||
ScopeDocumentType,
|
|
||||||
ScopeGap,
|
|
||||||
RequiredDocument,
|
|
||||||
RiskFlag,
|
|
||||||
DOCUMENT_SCOPE_MATRIX,
|
|
||||||
DocumentDepthRequirement,
|
|
||||||
} from '../compliance-scope-types'
|
|
||||||
import { DOCUMENT_SCOPE_MATRIX as DOC_MATRIX, DOCUMENT_TYPE_LABELS } from '../compliance-scope-types'
|
|
||||||
import type {
|
|
||||||
DraftContext,
|
|
||||||
GapContext,
|
|
||||||
ValidationContext,
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// State Projector
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export class StateProjector {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Projiziert den SDKState fuer Draft-Operationen.
|
|
||||||
* Fokus: Scope-Decision, Company-Profile, Dokument-spezifische Constraints.
|
|
||||||
*
|
|
||||||
* ~1500 Tokens
|
|
||||||
*/
|
|
||||||
projectForDraft(
|
|
||||||
state: SDKState,
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
): DraftContext {
|
|
||||||
const decision = state.complianceScope?.decision ?? null
|
|
||||||
const level = decision?.determinedLevel ?? 'L1'
|
|
||||||
const depthReq = DOC_MATRIX[documentType]?.[level] ?? {
|
|
||||||
required: false,
|
|
||||||
depth: 'Basis',
|
|
||||||
detailItems: [],
|
|
||||||
estimatedEffort: 'N/A',
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
decisions: {
|
|
||||||
level,
|
|
||||||
scores: decision?.scores ?? {
|
|
||||||
risk_score: 0,
|
|
||||||
complexity_score: 0,
|
|
||||||
assurance_need: 0,
|
|
||||||
composite_score: 0,
|
|
||||||
},
|
|
||||||
hardTriggers: (decision?.triggeredHardTriggers ?? []).map(t => ({
|
|
||||||
id: t.rule.id,
|
|
||||||
label: t.rule.label,
|
|
||||||
legalReference: t.rule.legalReference,
|
|
||||||
})),
|
|
||||||
requiredDocuments: (decision?.requiredDocuments ?? [])
|
|
||||||
.filter(d => d.required)
|
|
||||||
.map(d => ({
|
|
||||||
documentType: d.documentType,
|
|
||||||
depth: d.depth,
|
|
||||||
detailItems: d.detailItems,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
companyProfile: this.projectCompanyProfile(state.companyProfile),
|
|
||||||
constraints: {
|
|
||||||
depthRequirements: depthReq,
|
|
||||||
riskFlags: (decision?.riskFlags ?? []).map(f => ({
|
|
||||||
severity: f.severity,
|
|
||||||
title: f.title,
|
|
||||||
recommendation: f.recommendation,
|
|
||||||
})),
|
|
||||||
boundaries: this.deriveBoundaries(decision, documentType),
|
|
||||||
},
|
|
||||||
existingDocumentData: this.extractExistingDocumentData(state, documentType),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Projiziert den SDKState fuer Ask-Operationen.
|
|
||||||
* Fokus: Luecken, unbeantwortete Fragen, fehlende Dokumente.
|
|
||||||
*
|
|
||||||
* ~600 Tokens
|
|
||||||
*/
|
|
||||||
projectForAsk(state: SDKState): GapContext {
|
|
||||||
const decision = state.complianceScope?.decision ?? null
|
|
||||||
|
|
||||||
// Fehlende Pflichtdokumente ermitteln
|
|
||||||
const requiredDocs = (decision?.requiredDocuments ?? []).filter(d => d.required)
|
|
||||||
const existingDocTypes = this.getExistingDocumentTypes(state)
|
|
||||||
const missingDocuments = requiredDocs
|
|
||||||
.filter(d => !existingDocTypes.includes(d.documentType))
|
|
||||||
.map(d => ({
|
|
||||||
documentType: d.documentType,
|
|
||||||
label: DOCUMENT_TYPE_LABELS[d.documentType] ?? d.documentType,
|
|
||||||
depth: d.depth,
|
|
||||||
estimatedEffort: d.estimatedEffort,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Gaps aus der Scope-Decision
|
|
||||||
const gaps = (decision?.gaps ?? []).map(g => ({
|
|
||||||
id: g.id,
|
|
||||||
severity: g.severity,
|
|
||||||
title: g.title,
|
|
||||||
description: g.description,
|
|
||||||
relatedDocuments: g.relatedDocuments,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Unbeantwortete Fragen (aus dem Scope-Profiling)
|
|
||||||
const answers = state.complianceScope?.answers ?? []
|
|
||||||
const answeredIds = new Set(answers.map(a => a.questionId))
|
|
||||||
|
|
||||||
return {
|
|
||||||
unansweredQuestions: [], // Populated dynamically from question catalog
|
|
||||||
gaps,
|
|
||||||
missingDocuments,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Projiziert den SDKState fuer Validate-Operationen.
|
|
||||||
* Fokus: Cross-Dokument-Konsistenz, Scope-Compliance.
|
|
||||||
*
|
|
||||||
* ~2000 Tokens
|
|
||||||
*/
|
|
||||||
projectForValidate(
|
|
||||||
state: SDKState,
|
|
||||||
documentTypes: ScopeDocumentType[]
|
|
||||||
): ValidationContext {
|
|
||||||
const decision = state.complianceScope?.decision ?? null
|
|
||||||
const level = decision?.determinedLevel ?? 'L1'
|
|
||||||
|
|
||||||
// Dokument-Zusammenfassungen sammeln
|
|
||||||
const documents = documentTypes.map(type => ({
|
|
||||||
type,
|
|
||||||
contentSummary: this.summarizeDocument(state, type),
|
|
||||||
structuredData: this.extractExistingDocumentData(state, type),
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Cross-Referenzen extrahieren
|
|
||||||
const crossReferences = {
|
|
||||||
vvtCategories: (state.vvt ?? []).map(v =>
|
|
||||||
typeof v === 'object' && v !== null && 'name' in v ? String((v as Record<string, unknown>).name) : ''
|
|
||||||
).filter(Boolean),
|
|
||||||
dsfaRisks: state.dsfa
|
|
||||||
? ['DSFA vorhanden']
|
|
||||||
: [],
|
|
||||||
tomControls: (state.toms ?? []).map(t =>
|
|
||||||
typeof t === 'object' && t !== null && 'name' in t ? String((t as Record<string, unknown>).name) : ''
|
|
||||||
).filter(Boolean),
|
|
||||||
retentionCategories: (state.retentionPolicies ?? []).map(p =>
|
|
||||||
typeof p === 'object' && p !== null && 'name' in p ? String((p as Record<string, unknown>).name) : ''
|
|
||||||
).filter(Boolean),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Depth-Requirements fuer alle angefragten Typen
|
|
||||||
const depthRequirements: Record<string, DocumentDepthRequirement> = {}
|
|
||||||
for (const type of documentTypes) {
|
|
||||||
depthRequirements[type] = DOC_MATRIX[type]?.[level] ?? {
|
|
||||||
required: false,
|
|
||||||
depth: 'Basis',
|
|
||||||
detailItems: [],
|
|
||||||
estimatedEffort: 'N/A',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
documents,
|
|
||||||
crossReferences,
|
|
||||||
scopeLevel: level,
|
|
||||||
depthRequirements: depthRequirements as Record<ScopeDocumentType, DocumentDepthRequirement>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==========================================================================
|
|
||||||
// Private Helpers
|
|
||||||
// ==========================================================================
|
|
||||||
|
|
||||||
private projectCompanyProfile(
|
|
||||||
profile: CompanyProfile | null
|
|
||||||
): DraftContext['companyProfile'] {
|
|
||||||
if (!profile) {
|
|
||||||
return {
|
|
||||||
name: 'Unbekannt',
|
|
||||||
industry: 'Unbekannt',
|
|
||||||
employeeCount: 0,
|
|
||||||
businessModel: 'Unbekannt',
|
|
||||||
isPublicSector: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: profile.companyName ?? profile.name ?? 'Unbekannt',
|
|
||||||
industry: profile.industry ?? 'Unbekannt',
|
|
||||||
employeeCount: typeof profile.employeeCount === 'number'
|
|
||||||
? profile.employeeCount
|
|
||||||
: parseInt(String(profile.employeeCount ?? '0'), 10) || 0,
|
|
||||||
businessModel: profile.businessModel ?? 'Unbekannt',
|
|
||||||
isPublicSector: profile.isPublicSector ?? false,
|
|
||||||
...(profile.dataProtectionOfficer ? {
|
|
||||||
dataProtectionOfficer: {
|
|
||||||
name: profile.dataProtectionOfficer.name ?? '',
|
|
||||||
email: profile.dataProtectionOfficer.email ?? '',
|
|
||||||
},
|
|
||||||
} : {}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leitet Grenzen (Boundaries) ab, die der Agent nicht ueberschreiten darf.
|
|
||||||
*/
|
|
||||||
private deriveBoundaries(
|
|
||||||
decision: ScopeDecision | null,
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
): string[] {
|
|
||||||
const boundaries: string[] = []
|
|
||||||
const level = decision?.determinedLevel ?? 'L1'
|
|
||||||
|
|
||||||
// Grundregel: Scope-Engine ist autoritativ
|
|
||||||
boundaries.push(
|
|
||||||
`Maximale Dokumenttiefe: ${level} (${DOC_MATRIX[documentType]?.[level]?.depth ?? 'Basis'})`
|
|
||||||
)
|
|
||||||
|
|
||||||
// DSFA-Boundary
|
|
||||||
if (documentType === 'dsfa') {
|
|
||||||
const dsfaRequired = decision?.triggeredHardTriggers?.some(
|
|
||||||
t => t.rule.dsfaRequired
|
|
||||||
) ?? false
|
|
||||||
if (!dsfaRequired && level !== 'L4') {
|
|
||||||
boundaries.push('DSFA ist laut Scope-Engine NICHT erforderlich. Nur auf expliziten Wunsch erstellen.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dokument nicht in requiredDocuments?
|
|
||||||
const isRequired = decision?.requiredDocuments?.some(
|
|
||||||
d => d.documentType === documentType && d.required
|
|
||||||
) ?? false
|
|
||||||
if (!isRequired) {
|
|
||||||
boundaries.push(
|
|
||||||
`Dokument "${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}" ist auf Level ${level} nicht als Pflicht eingestuft.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return boundaries
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extrahiert bereits vorhandene Dokumentdaten aus dem SDK-State.
|
|
||||||
*/
|
|
||||||
private extractExistingDocumentData(
|
|
||||||
state: SDKState,
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
): Record<string, unknown> | undefined {
|
|
||||||
switch (documentType) {
|
|
||||||
case 'vvt':
|
|
||||||
return state.vvt?.length ? { entries: state.vvt.slice(0, 5), totalCount: state.vvt.length } : undefined
|
|
||||||
case 'tom':
|
|
||||||
return state.toms?.length ? { entries: state.toms.slice(0, 5), totalCount: state.toms.length } : undefined
|
|
||||||
case 'lf':
|
|
||||||
return state.retentionPolicies?.length
|
|
||||||
? { entries: state.retentionPolicies.slice(0, 5), totalCount: state.retentionPolicies.length }
|
|
||||||
: undefined
|
|
||||||
case 'dsfa':
|
|
||||||
return state.dsfa ? { assessment: state.dsfa } : undefined
|
|
||||||
case 'dsi':
|
|
||||||
return state.documents?.length
|
|
||||||
? { entries: state.documents.slice(0, 3), totalCount: state.documents.length }
|
|
||||||
: undefined
|
|
||||||
case 'einwilligung':
|
|
||||||
return state.consents?.length
|
|
||||||
? { entries: state.consents.slice(0, 5), totalCount: state.consents.length }
|
|
||||||
: undefined
|
|
||||||
default:
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ermittelt welche Dokumenttypen bereits im State vorhanden sind.
|
|
||||||
*/
|
|
||||||
private getExistingDocumentTypes(state: SDKState): ScopeDocumentType[] {
|
|
||||||
const types: ScopeDocumentType[] = []
|
|
||||||
if (state.vvt?.length) types.push('vvt')
|
|
||||||
if (state.toms?.length) types.push('tom')
|
|
||||||
if (state.retentionPolicies?.length) types.push('lf')
|
|
||||||
if (state.dsfa) types.push('dsfa')
|
|
||||||
if (state.documents?.length) types.push('dsi')
|
|
||||||
if (state.consents?.length) types.push('einwilligung')
|
|
||||||
if (state.cookieBanner) types.push('einwilligung')
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Erstellt eine kurze Zusammenfassung eines Dokuments fuer Validierung.
|
|
||||||
*/
|
|
||||||
private summarizeDocument(
|
|
||||||
state: SDKState,
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
): string {
|
|
||||||
switch (documentType) {
|
|
||||||
case 'vvt':
|
|
||||||
return state.vvt?.length
|
|
||||||
? `${state.vvt.length} Verarbeitungstaetigkeiten erfasst`
|
|
||||||
: 'Keine VVT-Eintraege vorhanden'
|
|
||||||
case 'tom':
|
|
||||||
return state.toms?.length
|
|
||||||
? `${state.toms.length} TOM-Massnahmen definiert`
|
|
||||||
: 'Keine TOM-Massnahmen vorhanden'
|
|
||||||
case 'lf':
|
|
||||||
return state.retentionPolicies?.length
|
|
||||||
? `${state.retentionPolicies.length} Loeschfristen definiert`
|
|
||||||
: 'Keine Loeschfristen vorhanden'
|
|
||||||
case 'dsfa':
|
|
||||||
return state.dsfa
|
|
||||||
? 'DSFA vorhanden'
|
|
||||||
: 'Keine DSFA vorhanden'
|
|
||||||
default:
|
|
||||||
return `Dokument ${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Singleton-Instanz */
|
|
||||||
export const stateProjector = new StateProjector()
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
/**
|
|
||||||
* Drafting Engine - Type Definitions
|
|
||||||
*
|
|
||||||
* Typen fuer die 4 Agent-Rollen: Explain, Ask, Draft, Validate
|
|
||||||
* Die Drafting Engine erweitert den Compliance Advisor um aktive Dokumententwurfs-
|
|
||||||
* und Validierungsfaehigkeiten, stets unter Beachtung der deterministischen Scope-Engine.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ComplianceDepthLevel,
|
|
||||||
ComplianceScores,
|
|
||||||
ScopeDecision,
|
|
||||||
ScopeDocumentType,
|
|
||||||
ScopeGap,
|
|
||||||
RequiredDocument,
|
|
||||||
RiskFlag,
|
|
||||||
DocumentDepthRequirement,
|
|
||||||
ScopeProfilingQuestion,
|
|
||||||
} from '../compliance-scope-types'
|
|
||||||
import type { CompanyProfile } from '../types'
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Agent Mode
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/** Die 4 Agent-Rollen */
|
|
||||||
export type AgentMode = 'explain' | 'ask' | 'draft' | 'validate'
|
|
||||||
|
|
||||||
/** Confidence-Score fuer Intent-Erkennung */
|
|
||||||
export interface IntentClassification {
|
|
||||||
mode: AgentMode
|
|
||||||
confidence: number
|
|
||||||
matchedPatterns: string[]
|
|
||||||
/** Falls Draft oder Validate: erkannter Dokumenttyp */
|
|
||||||
detectedDocumentType?: ScopeDocumentType
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Draft Context (fuer Draft-Mode)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/** Projizierter State fuer Draft-Operationen (~1500 Tokens) */
|
|
||||||
export interface DraftContext {
|
|
||||||
/** Scope-Entscheidung (Level, Scores, Hard Triggers) */
|
|
||||||
decisions: {
|
|
||||||
level: ComplianceDepthLevel
|
|
||||||
scores: ComplianceScores
|
|
||||||
hardTriggers: Array<{ id: string; label: string; legalReference: string }>
|
|
||||||
requiredDocuments: Array<{
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
depth: string
|
|
||||||
detailItems: string[]
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
/** Firmenprofil-Auszug */
|
|
||||||
companyProfile: {
|
|
||||||
name: string
|
|
||||||
industry: string
|
|
||||||
employeeCount: number
|
|
||||||
businessModel: string
|
|
||||||
isPublicSector: boolean
|
|
||||||
dataProtectionOfficer?: { name: string; email: string }
|
|
||||||
}
|
|
||||||
/** Constraints aus der Scope-Engine */
|
|
||||||
constraints: {
|
|
||||||
depthRequirements: DocumentDepthRequirement
|
|
||||||
riskFlags: Array<{ severity: string; title: string; recommendation: string }>
|
|
||||||
boundaries: string[]
|
|
||||||
}
|
|
||||||
/** Optional: bestehende Dokumentdaten aus dem SDK-State */
|
|
||||||
existingDocumentData?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Gap Context (fuer Ask-Mode)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/** Projizierter State fuer Ask-Operationen (~600 Tokens) */
|
|
||||||
export interface GapContext {
|
|
||||||
/** Noch unbeantwortete Fragen aus dem Scope-Profiling */
|
|
||||||
unansweredQuestions: Array<{
|
|
||||||
id: string
|
|
||||||
question: string
|
|
||||||
type: string
|
|
||||||
blockId: string
|
|
||||||
}>
|
|
||||||
/** Identifizierte Luecken */
|
|
||||||
gaps: Array<{
|
|
||||||
id: string
|
|
||||||
severity: string
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
relatedDocuments: ScopeDocumentType[]
|
|
||||||
}>
|
|
||||||
/** Fehlende Pflichtdokumente */
|
|
||||||
missingDocuments: Array<{
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
label: string
|
|
||||||
depth: string
|
|
||||||
estimatedEffort: string
|
|
||||||
}>
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Validation Context (fuer Validate-Mode)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/** Projizierter State fuer Validate-Operationen (~2000 Tokens) */
|
|
||||||
export interface ValidationContext {
|
|
||||||
/** Zu validierende Dokumente */
|
|
||||||
documents: Array<{
|
|
||||||
type: ScopeDocumentType
|
|
||||||
/** Zusammenfassung/Auszug des Inhalts */
|
|
||||||
contentSummary: string
|
|
||||||
/** Strukturierte Daten falls vorhanden */
|
|
||||||
structuredData?: Record<string, unknown>
|
|
||||||
}>
|
|
||||||
/** Cross-Referenzen zwischen Dokumenten */
|
|
||||||
crossReferences: {
|
|
||||||
/** VVT Kategorien (Verarbeitungstaetigkeiten) */
|
|
||||||
vvtCategories: string[]
|
|
||||||
/** DSFA Risiken */
|
|
||||||
dsfaRisks: string[]
|
|
||||||
/** TOM Controls */
|
|
||||||
tomControls: string[]
|
|
||||||
/** Loeschfristen-Kategorien */
|
|
||||||
retentionCategories: string[]
|
|
||||||
}
|
|
||||||
/** Scope-Level fuer Tiefenpruefung */
|
|
||||||
scopeLevel: ComplianceDepthLevel
|
|
||||||
/** Relevante Depth-Requirements */
|
|
||||||
depthRequirements: Record<ScopeDocumentType, DocumentDepthRequirement>
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Validation Result
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export type ValidationSeverity = 'error' | 'warning' | 'suggestion'
|
|
||||||
|
|
||||||
export interface ValidationFinding {
|
|
||||||
id: string
|
|
||||||
severity: ValidationSeverity
|
|
||||||
category: 'scope_violation' | 'inconsistency' | 'missing_content' | 'depth_mismatch' | 'cross_reference'
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
/** Betroffenes Dokument */
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
/** Optional: Referenz zu anderem Dokument */
|
|
||||||
crossReferenceType?: ScopeDocumentType
|
|
||||||
/** Rechtsgrundlage falls relevant */
|
|
||||||
legalReference?: string
|
|
||||||
/** Vorschlag zur Behebung */
|
|
||||||
suggestion?: string
|
|
||||||
/** Kann automatisch uebernommen werden */
|
|
||||||
autoFixable?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidationResult {
|
|
||||||
passed: boolean
|
|
||||||
timestamp: string
|
|
||||||
scopeLevel: ComplianceDepthLevel
|
|
||||||
errors: ValidationFinding[]
|
|
||||||
warnings: ValidationFinding[]
|
|
||||||
suggestions: ValidationFinding[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Draft Session
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface DraftRevision {
|
|
||||||
id: string
|
|
||||||
content: string
|
|
||||||
sections: DraftSection[]
|
|
||||||
createdAt: string
|
|
||||||
instruction?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftSection {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
content: string
|
|
||||||
/** Mapping zum Dokumentschema (z.B. VVT-Feld) */
|
|
||||||
schemaField?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftSession {
|
|
||||||
id: string
|
|
||||||
mode: AgentMode
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
/** Aktueller Draft-Inhalt */
|
|
||||||
currentDraft: DraftRevision | null
|
|
||||||
/** Alle bisherigen Revisionen */
|
|
||||||
revisions: DraftRevision[]
|
|
||||||
/** Validierungszustand */
|
|
||||||
validationState: ValidationResult | null
|
|
||||||
/** Constraint-Check Ergebnis */
|
|
||||||
constraintCheck: ConstraintCheckResult | null
|
|
||||||
createdAt: string
|
|
||||||
updatedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Constraint Check (Hard Gate)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface ConstraintCheckResult {
|
|
||||||
/** Darf der Draft erstellt werden? */
|
|
||||||
allowed: boolean
|
|
||||||
/** Verletzungen die den Draft blockieren */
|
|
||||||
violations: string[]
|
|
||||||
/** Anpassungen die vorgenommen werden sollten */
|
|
||||||
adjustments: string[]
|
|
||||||
/** Gepruefte Regeln */
|
|
||||||
checkedRules: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Chat / API Types
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface DraftingChatMessage {
|
|
||||||
role: 'user' | 'assistant' | 'system'
|
|
||||||
content: string
|
|
||||||
/** Metadata fuer Agent-Nachrichten */
|
|
||||||
metadata?: {
|
|
||||||
mode: AgentMode
|
|
||||||
documentType?: ScopeDocumentType
|
|
||||||
hasDraft?: boolean
|
|
||||||
hasValidation?: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftingChatRequest {
|
|
||||||
message: string
|
|
||||||
history: DraftingChatMessage[]
|
|
||||||
sdkStateProjection: DraftContext | GapContext | ValidationContext
|
|
||||||
mode?: AgentMode
|
|
||||||
documentType?: ScopeDocumentType
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftRequest {
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
draftContext: DraftContext
|
|
||||||
instructions?: string
|
|
||||||
existingDraft?: DraftRevision
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftResponse {
|
|
||||||
draft: DraftRevision
|
|
||||||
constraintCheck: ConstraintCheckResult
|
|
||||||
tokensUsed: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ValidateRequest {
|
|
||||||
documentType: ScopeDocumentType
|
|
||||||
draftContent: string
|
|
||||||
validationContext: ValidationContext
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Feature Flag
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
export interface DraftingEngineConfig {
|
|
||||||
/** Feature-Flag: Drafting Engine aktiviert */
|
|
||||||
enableDraftingEngine: boolean
|
|
||||||
/** Verfuegbare Modi (fuer schrittweises Rollout) */
|
|
||||||
enabledModes: AgentMode[]
|
|
||||||
/** Max Token-Budget fuer State-Projection */
|
|
||||||
maxProjectionTokens: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_DRAFTING_ENGINE_CONFIG: DraftingEngineConfig = {
|
|
||||||
enableDraftingEngine: false,
|
|
||||||
enabledModes: ['explain'],
|
|
||||||
maxProjectionTokens: 4096,
|
|
||||||
}
|
|
||||||
@@ -1,343 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* useDraftingEngine - React Hook fuer die Drafting Engine
|
|
||||||
*
|
|
||||||
* Managed: currentMode, activeDocumentType, draftSessions, validationState
|
|
||||||
* Handled: State-Projection, API-Calls, Streaming
|
|
||||||
* Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft()
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useState, useCallback, useRef } from 'react'
|
|
||||||
import { useSDK } from '../context'
|
|
||||||
import { stateProjector } from './state-projector'
|
|
||||||
import { intentClassifier } from './intent-classifier'
|
|
||||||
import { constraintEnforcer } from './constraint-enforcer'
|
|
||||||
import type {
|
|
||||||
AgentMode,
|
|
||||||
DraftSession,
|
|
||||||
DraftRevision,
|
|
||||||
DraftingChatMessage,
|
|
||||||
ValidationResult,
|
|
||||||
ConstraintCheckResult,
|
|
||||||
DraftContext,
|
|
||||||
GapContext,
|
|
||||||
ValidationContext,
|
|
||||||
} from './types'
|
|
||||||
import type { ScopeDocumentType } from '../compliance-scope-types'
|
|
||||||
|
|
||||||
export interface DraftingEngineState {
|
|
||||||
currentMode: AgentMode
|
|
||||||
activeDocumentType: ScopeDocumentType | null
|
|
||||||
messages: DraftingChatMessage[]
|
|
||||||
isTyping: boolean
|
|
||||||
currentDraft: DraftRevision | null
|
|
||||||
validationResult: ValidationResult | null
|
|
||||||
constraintCheck: ConstraintCheckResult | null
|
|
||||||
error: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DraftingEngineActions {
|
|
||||||
setMode: (mode: AgentMode) => void
|
|
||||||
setDocumentType: (type: ScopeDocumentType) => void
|
|
||||||
sendMessage: (content: string) => Promise<void>
|
|
||||||
requestDraft: (instructions?: string) => Promise<void>
|
|
||||||
validateDraft: () => Promise<void>
|
|
||||||
acceptDraft: () => void
|
|
||||||
stopGeneration: () => void
|
|
||||||
clearMessages: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
|
|
||||||
const { state, dispatch } = useSDK()
|
|
||||||
const abortControllerRef = useRef<AbortController | null>(null)
|
|
||||||
|
|
||||||
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
|
|
||||||
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
|
|
||||||
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
|
|
||||||
const [isTyping, setIsTyping] = useState(false)
|
|
||||||
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
|
|
||||||
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
|
|
||||||
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
|
|
||||||
const [error, setError] = useState<string | null>(null)
|
|
||||||
|
|
||||||
// Get state projection based on mode
|
|
||||||
const getProjection = useCallback(() => {
|
|
||||||
switch (currentMode) {
|
|
||||||
case 'draft':
|
|
||||||
return activeDocumentType
|
|
||||||
? stateProjector.projectForDraft(state, activeDocumentType)
|
|
||||||
: null
|
|
||||||
case 'ask':
|
|
||||||
return stateProjector.projectForAsk(state)
|
|
||||||
case 'validate':
|
|
||||||
return activeDocumentType
|
|
||||||
? stateProjector.projectForValidate(state, [activeDocumentType])
|
|
||||||
: stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf'])
|
|
||||||
default:
|
|
||||||
return activeDocumentType
|
|
||||||
? stateProjector.projectForDraft(state, activeDocumentType)
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
}, [state, currentMode, activeDocumentType])
|
|
||||||
|
|
||||||
const setMode = useCallback((mode: AgentMode) => {
|
|
||||||
setCurrentMode(mode)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const setDocumentType = useCallback((type: ScopeDocumentType) => {
|
|
||||||
setActiveDocumentType(type)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const sendMessage = useCallback(async (content: string) => {
|
|
||||||
if (!content.trim() || isTyping) return
|
|
||||||
setError(null)
|
|
||||||
|
|
||||||
// Auto-detect mode if needed
|
|
||||||
const classification = intentClassifier.classify(content)
|
|
||||||
if (classification.confidence > 0.7 && classification.mode !== currentMode) {
|
|
||||||
setCurrentMode(classification.mode)
|
|
||||||
}
|
|
||||||
if (classification.detectedDocumentType && !activeDocumentType) {
|
|
||||||
setActiveDocumentType(classification.detectedDocumentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
const userMessage: DraftingChatMessage = {
|
|
||||||
role: 'user',
|
|
||||||
content: content.trim(),
|
|
||||||
}
|
|
||||||
setMessages(prev => [...prev, userMessage])
|
|
||||||
setIsTyping(true)
|
|
||||||
|
|
||||||
abortControllerRef.current = new AbortController()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const projection = getProjection()
|
|
||||||
const response = await fetch('/api/sdk/drafting-engine/chat', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
message: content.trim(),
|
|
||||||
history: messages.map(m => ({ role: m.role, content: m.content })),
|
|
||||||
sdkStateProjection: projection,
|
|
||||||
mode: currentMode,
|
|
||||||
documentType: activeDocumentType,
|
|
||||||
}),
|
|
||||||
signal: abortControllerRef.current.signal,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }))
|
|
||||||
throw new Error(errorData.error || `Server-Fehler (${response.status})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentMessageId = `msg-${Date.now()}-agent`
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
role: 'assistant',
|
|
||||||
content: '',
|
|
||||||
metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined },
|
|
||||||
}])
|
|
||||||
|
|
||||||
// Stream response
|
|
||||||
const reader = response.body!.getReader()
|
|
||||||
const decoder = new TextDecoder()
|
|
||||||
let accumulated = ''
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read()
|
|
||||||
if (done) break
|
|
||||||
accumulated += decoder.decode(value, { stream: true })
|
|
||||||
const text = accumulated
|
|
||||||
setMessages(prev =>
|
|
||||||
prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsTyping(false)
|
|
||||||
} catch (err) {
|
|
||||||
if ((err as Error).name === 'AbortError') {
|
|
||||||
setIsTyping(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setError((err as Error).message)
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
role: 'assistant',
|
|
||||||
content: `Fehler: ${(err as Error).message}`,
|
|
||||||
}])
|
|
||||||
setIsTyping(false)
|
|
||||||
}
|
|
||||||
}, [isTyping, messages, currentMode, activeDocumentType, getProjection])
|
|
||||||
|
|
||||||
const requestDraft = useCallback(async (instructions?: string) => {
|
|
||||||
if (!activeDocumentType) {
|
|
||||||
setError('Bitte waehlen Sie zuerst einen Dokumenttyp.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setError(null)
|
|
||||||
setIsTyping(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const draftContext = stateProjector.projectForDraft(state, activeDocumentType)
|
|
||||||
|
|
||||||
const response = await fetch('/api/sdk/drafting-engine/draft', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
documentType: activeDocumentType,
|
|
||||||
draftContext,
|
|
||||||
instructions,
|
|
||||||
existingDraft: currentDraft,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(result.error || 'Draft-Generierung fehlgeschlagen')
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentDraft(result.draft)
|
|
||||||
setConstraintCheck(result.constraintCheck)
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
role: 'assistant',
|
|
||||||
content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`,
|
|
||||||
metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true },
|
|
||||||
}])
|
|
||||||
|
|
||||||
setIsTyping(false)
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message)
|
|
||||||
setIsTyping(false)
|
|
||||||
}
|
|
||||||
}, [activeDocumentType, state, currentDraft])
|
|
||||||
|
|
||||||
const validateDraft = useCallback(async () => {
|
|
||||||
setError(null)
|
|
||||||
setIsTyping(true)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const docTypes: ScopeDocumentType[] = activeDocumentType
|
|
||||||
? [activeDocumentType]
|
|
||||||
: ['vvt', 'tom', 'lf']
|
|
||||||
const validationContext = stateProjector.projectForValidate(state, docTypes)
|
|
||||||
|
|
||||||
const response = await fetch('/api/sdk/drafting-engine/validate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
documentType: activeDocumentType || 'vvt',
|
|
||||||
draftContent: currentDraft?.content || '',
|
|
||||||
validationContext,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(result.error || 'Validierung fehlgeschlagen')
|
|
||||||
}
|
|
||||||
|
|
||||||
setValidationResult(result)
|
|
||||||
|
|
||||||
const summary = result.passed
|
|
||||||
? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.`
|
|
||||||
: `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.`
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
role: 'assistant',
|
|
||||||
content: summary,
|
|
||||||
metadata: { mode: 'validate', hasValidation: true },
|
|
||||||
}])
|
|
||||||
|
|
||||||
setIsTyping(false)
|
|
||||||
} catch (err) {
|
|
||||||
setError((err as Error).message)
|
|
||||||
setIsTyping(false)
|
|
||||||
}
|
|
||||||
}, [activeDocumentType, state, currentDraft])
|
|
||||||
|
|
||||||
const acceptDraft = useCallback(() => {
|
|
||||||
if (!currentDraft || !activeDocumentType) return
|
|
||||||
|
|
||||||
// Dispatch the draft data into SDK state
|
|
||||||
switch (activeDocumentType) {
|
|
||||||
case 'vvt':
|
|
||||||
dispatch({
|
|
||||||
type: 'ADD_PROCESSING_ACTIVITY',
|
|
||||||
payload: {
|
|
||||||
id: `draft-vvt-${Date.now()}`,
|
|
||||||
name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag',
|
|
||||||
...Object.fromEntries(
|
|
||||||
currentDraft.sections
|
|
||||||
.filter(s => s.schemaField)
|
|
||||||
.map(s => [s.schemaField!, s.content])
|
|
||||||
),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'tom':
|
|
||||||
dispatch({
|
|
||||||
type: 'ADD_TOM',
|
|
||||||
payload: {
|
|
||||||
id: `draft-tom-${Date.now()}`,
|
|
||||||
name: 'TOM-Entwurf',
|
|
||||||
...Object.fromEntries(
|
|
||||||
currentDraft.sections
|
|
||||||
.filter(s => s.schemaField)
|
|
||||||
.map(s => [s.schemaField!, s.content])
|
|
||||||
),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
dispatch({
|
|
||||||
type: 'ADD_DOCUMENT',
|
|
||||||
payload: {
|
|
||||||
id: `draft-${activeDocumentType}-${Date.now()}`,
|
|
||||||
type: activeDocumentType,
|
|
||||||
content: currentDraft.content,
|
|
||||||
sections: currentDraft.sections,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setMessages(prev => [...prev, {
|
|
||||||
role: 'assistant',
|
|
||||||
content: `Draft wurde in den SDK-State uebernommen.`,
|
|
||||||
}])
|
|
||||||
setCurrentDraft(null)
|
|
||||||
}, [currentDraft, activeDocumentType, dispatch])
|
|
||||||
|
|
||||||
const stopGeneration = useCallback(() => {
|
|
||||||
abortControllerRef.current?.abort()
|
|
||||||
setIsTyping(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const clearMessages = useCallback(() => {
|
|
||||||
setMessages([])
|
|
||||||
setCurrentDraft(null)
|
|
||||||
setValidationResult(null)
|
|
||||||
setConstraintCheck(null)
|
|
||||||
setError(null)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentMode,
|
|
||||||
activeDocumentType,
|
|
||||||
messages,
|
|
||||||
isTyping,
|
|
||||||
currentDraft,
|
|
||||||
validationResult,
|
|
||||||
constraintCheck,
|
|
||||||
error,
|
|
||||||
setMode,
|
|
||||||
setDocumentType,
|
|
||||||
sendMessage,
|
|
||||||
requestDraft,
|
|
||||||
validateDraft,
|
|
||||||
acceptDraft,
|
|
||||||
stopGeneration,
|
|
||||||
clearMessages,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,881 +0,0 @@
|
|||||||
/**
|
|
||||||
* DSR API Client
|
|
||||||
*
|
|
||||||
* API client for Data Subject Request management
|
|
||||||
* Connects to the Go Consent Service backend
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DSRRequest,
|
|
||||||
DSRListResponse,
|
|
||||||
DSRFilters,
|
|
||||||
DSRCreateRequest,
|
|
||||||
DSRUpdateRequest,
|
|
||||||
DSRVerifyIdentityRequest,
|
|
||||||
DSRCompleteRequest,
|
|
||||||
DSRRejectRequest,
|
|
||||||
DSRExtendDeadlineRequest,
|
|
||||||
DSRSendCommunicationRequest,
|
|
||||||
DSRCommunication,
|
|
||||||
DSRAuditEntry,
|
|
||||||
DSRStatistics,
|
|
||||||
DSRDataExport,
|
|
||||||
DSRErasureChecklist
|
|
||||||
} from './types'
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// CONFIGURATION
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
const DSR_API_BASE = process.env.NEXT_PUBLIC_CONSENT_SERVICE_URL || 'http://localhost:8081'
|
|
||||||
const API_TIMEOUT = 30000 // 30 seconds
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// HELPER FUNCTIONS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
function getTenantId(): string {
|
|
||||||
// In a real app, this would come from auth context or localStorage
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
return localStorage.getItem('tenantId') || 'default-tenant'
|
|
||||||
}
|
|
||||||
return 'default-tenant'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuthHeaders(): HeadersInit {
|
|
||||||
const headers: HeadersInit = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-ID': getTenantId()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add auth token if available
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const token = localStorage.getItem('authToken')
|
|
||||||
if (token) {
|
|
||||||
headers['Authorization'] = `Bearer ${token}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchWithTimeout<T>(
|
|
||||||
url: string,
|
|
||||||
options: RequestInit = {},
|
|
||||||
timeout: number = API_TIMEOUT
|
|
||||||
): Promise<T> {
|
|
||||||
const controller = new AbortController()
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
signal: controller.signal,
|
|
||||||
headers: {
|
|
||||||
...getAuthHeaders(),
|
|
||||||
...options.headers
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.text()
|
|
||||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
|
|
||||||
try {
|
|
||||||
const errorJson = JSON.parse(errorBody)
|
|
||||||
errorMessage = errorJson.error || errorJson.message || errorMessage
|
|
||||||
} catch {
|
|
||||||
// Keep the HTTP status message
|
|
||||||
}
|
|
||||||
throw new Error(errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle empty responses
|
|
||||||
const contentType = response.headers.get('content-type')
|
|
||||||
if (contentType && contentType.includes('application/json')) {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
return {} as T
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// DSR LIST & CRUD
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all DSR requests with optional filters
|
|
||||||
*/
|
|
||||||
export async function fetchDSRList(filters?: DSRFilters): Promise<DSRListResponse> {
|
|
||||||
const params = new URLSearchParams()
|
|
||||||
|
|
||||||
if (filters) {
|
|
||||||
if (filters.status) {
|
|
||||||
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
|
|
||||||
statuses.forEach(s => params.append('status', s))
|
|
||||||
}
|
|
||||||
if (filters.type) {
|
|
||||||
const types = Array.isArray(filters.type) ? filters.type : [filters.type]
|
|
||||||
types.forEach(t => params.append('type', t))
|
|
||||||
}
|
|
||||||
if (filters.priority) params.set('priority', filters.priority)
|
|
||||||
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
|
|
||||||
if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue))
|
|
||||||
if (filters.search) params.set('search', filters.search)
|
|
||||||
if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
|
|
||||||
if (filters.dateTo) params.set('dateTo', filters.dateTo)
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryString = params.toString()
|
|
||||||
const url = `${DSR_API_BASE}/api/v1/admin/dsr${queryString ? `?${queryString}` : ''}`
|
|
||||||
|
|
||||||
return fetchWithTimeout<DSRListResponse>(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a single DSR request by ID
|
|
||||||
*/
|
|
||||||
export async function fetchDSR(id: string): Promise<DSRRequest> {
|
|
||||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new DSR request
|
|
||||||
*/
|
|
||||||
export async function createDSR(request: DSRCreateRequest): Promise<DSRRequest> {
|
|
||||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(request)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a DSR request
|
|
||||||
*/
|
|
||||||
export async function updateDSR(id: string, update: DSRUpdateRequest): Promise<DSRRequest> {
|
|
||||||
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(update)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a DSR request (soft delete - marks as cancelled)
|
|
||||||
*/
|
|
||||||
export async function deleteDSR(id: string): Promise<void> {
|
|
||||||
await fetchWithTimeout<void>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// DSR WORKFLOW ACTIONS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify the identity of the requester
|
|
||||||
*/
|
|
||||||
export async function verifyIdentity(
|
|
||||||
dsrId: string,
|
|
||||||
verification: DSRVerifyIdentityRequest
|
|
||||||
): Promise<DSRRequest> {
|
|
||||||
return fetchWithTimeout<DSRRequest>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/verify-identity`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(verification)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete a DSR request
|
|
||||||
*/
|
|
||||||
export async function completeDSR(
|
|
||||||
dsrId: string,
|
|
||||||
completion?: DSRCompleteRequest
|
|
||||||
): Promise<DSRRequest> {
|
|
||||||
return fetchWithTimeout<DSRRequest>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/complete`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(completion || {})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reject a DSR request
|
|
||||||
*/
|
|
||||||
export async function rejectDSR(
|
|
||||||
dsrId: string,
|
|
||||||
rejection: DSRRejectRequest
|
|
||||||
): Promise<DSRRequest> {
|
|
||||||
return fetchWithTimeout<DSRRequest>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/reject`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(rejection)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extend the deadline for a DSR request
|
|
||||||
*/
|
|
||||||
export async function extendDeadline(
|
|
||||||
dsrId: string,
|
|
||||||
extension: DSRExtendDeadlineRequest
|
|
||||||
): Promise<DSRRequest> {
|
|
||||||
return fetchWithTimeout<DSRRequest>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/extend`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(extension)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign a DSR request to a user
|
|
||||||
*/
|
|
||||||
export async function assignDSR(
|
|
||||||
dsrId: string,
|
|
||||||
assignedTo: string
|
|
||||||
): Promise<DSRRequest> {
|
|
||||||
return fetchWithTimeout<DSRRequest>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/assign`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ assignedTo })
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// COMMUNICATION
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all communications for a DSR request
|
|
||||||
*/
|
|
||||||
export async function getCommunications(dsrId: string): Promise<DSRCommunication[]> {
|
|
||||||
return fetchWithTimeout<DSRCommunication[]>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/communications`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a communication (email, letter, internal note)
|
|
||||||
*/
|
|
||||||
export async function sendCommunication(
|
|
||||||
dsrId: string,
|
|
||||||
communication: DSRSendCommunicationRequest
|
|
||||||
): Promise<DSRCommunication> {
|
|
||||||
return fetchWithTimeout<DSRCommunication>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/send-communication`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(communication)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// AUDIT LOG
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get audit log entries for a DSR request
|
|
||||||
*/
|
|
||||||
export async function getAuditLog(dsrId: string): Promise<DSRAuditEntry[]> {
|
|
||||||
return fetchWithTimeout<DSRAuditEntry[]>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/audit`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// STATISTICS
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get DSR statistics
|
|
||||||
*/
|
|
||||||
export async function getDSRStatistics(): Promise<DSRStatistics> {
|
|
||||||
return fetchWithTimeout<DSRStatistics>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/statistics`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// DATA EXPORT (Art. 15, 20)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate data export for Art. 15 (access) or Art. 20 (portability)
|
|
||||||
*/
|
|
||||||
export async function generateDataExport(
|
|
||||||
dsrId: string,
|
|
||||||
format: 'json' | 'csv' | 'xml' | 'pdf' = 'json'
|
|
||||||
): Promise<DSRDataExport> {
|
|
||||||
return fetchWithTimeout<DSRDataExport>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ format })
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download generated data export
|
|
||||||
*/
|
|
||||||
export async function downloadDataExport(dsrId: string): Promise<Blob> {
|
|
||||||
const response = await fetch(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export/download`,
|
|
||||||
{
|
|
||||||
headers: getAuthHeaders()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Download failed: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.blob()
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// ERASURE CHECKLIST (Art. 17)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the erasure checklist for an Art. 17 request
|
|
||||||
*/
|
|
||||||
export async function getErasureChecklist(dsrId: string): Promise<DSRErasureChecklist> {
|
|
||||||
return fetchWithTimeout<DSRErasureChecklist>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the erasure checklist
|
|
||||||
*/
|
|
||||||
export async function updateErasureChecklist(
|
|
||||||
dsrId: string,
|
|
||||||
checklist: DSRErasureChecklist
|
|
||||||
): Promise<DSRErasureChecklist> {
|
|
||||||
return fetchWithTimeout<DSRErasureChecklist>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`,
|
|
||||||
{
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(checklist)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// EMAIL TEMPLATES
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get available email templates
|
|
||||||
*/
|
|
||||||
export async function getEmailTemplates(): Promise<{ id: string; name: string; stage: string }[]> {
|
|
||||||
return fetchWithTimeout<{ id: string; name: string; stage: string }[]>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preview an email with variables filled in
|
|
||||||
*/
|
|
||||||
export async function previewEmail(
|
|
||||||
templateId: string,
|
|
||||||
dsrId: string
|
|
||||||
): Promise<{ subject: string; body: string }> {
|
|
||||||
return fetchWithTimeout<{ subject: string; body: string }>(
|
|
||||||
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates/${templateId}/preview`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ dsrId })
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// SDK API FUNCTIONS (via Next.js proxy to ai-compliance-sdk)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
interface BackendDSR {
|
|
||||||
id: string
|
|
||||||
tenant_id: string
|
|
||||||
namespace_id?: string
|
|
||||||
request_type: string
|
|
||||||
status: string
|
|
||||||
subject_name: string
|
|
||||||
subject_email: string
|
|
||||||
subject_identifier?: string
|
|
||||||
request_description: string
|
|
||||||
request_channel: string
|
|
||||||
received_at: string
|
|
||||||
verified_at?: string
|
|
||||||
verification_method?: string
|
|
||||||
deadline_at: string
|
|
||||||
extended_deadline_at?: string
|
|
||||||
extension_reason?: string
|
|
||||||
completed_at?: string
|
|
||||||
response_sent: boolean
|
|
||||||
response_sent_at?: string
|
|
||||||
response_method?: string
|
|
||||||
rejection_reason?: string
|
|
||||||
notes?: string
|
|
||||||
affected_systems?: string[]
|
|
||||||
assigned_to?: string
|
|
||||||
created_at: string
|
|
||||||
updated_at: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapBackendStatus(status: string): import('./types').DSRStatus {
|
|
||||||
const mapping: Record<string, import('./types').DSRStatus> = {
|
|
||||||
'received': 'intake',
|
|
||||||
'verified': 'identity_verification',
|
|
||||||
'in_progress': 'processing',
|
|
||||||
'completed': 'completed',
|
|
||||||
'rejected': 'rejected',
|
|
||||||
'extended': 'processing',
|
|
||||||
}
|
|
||||||
return mapping[status] || 'intake'
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapBackendChannel(channel: string): import('./types').DSRSource {
|
|
||||||
const mapping: Record<string, import('./types').DSRSource> = {
|
|
||||||
'email': 'email',
|
|
||||||
'form': 'web_form',
|
|
||||||
'phone': 'phone',
|
|
||||||
'letter': 'letter',
|
|
||||||
}
|
|
||||||
return mapping[channel] || 'other'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform flat backend DSR to nested SDK DSRRequest format
|
|
||||||
*/
|
|
||||||
export function transformBackendDSR(b: BackendDSR): DSRRequest {
|
|
||||||
const deadlineAt = b.extended_deadline_at || b.deadline_at
|
|
||||||
const receivedDate = new Date(b.received_at)
|
|
||||||
const defaultDeadlineDays = 30
|
|
||||||
const originalDeadline = b.deadline_at || new Date(receivedDate.getTime() + defaultDeadlineDays * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: b.id,
|
|
||||||
referenceNumber: `DSR-${new Date(b.created_at).getFullYear()}-${b.id.slice(0, 6).toUpperCase()}`,
|
|
||||||
type: b.request_type as DSRRequest['type'],
|
|
||||||
status: mapBackendStatus(b.status),
|
|
||||||
priority: 'normal',
|
|
||||||
requester: {
|
|
||||||
name: b.subject_name,
|
|
||||||
email: b.subject_email,
|
|
||||||
customerId: b.subject_identifier,
|
|
||||||
},
|
|
||||||
source: mapBackendChannel(b.request_channel),
|
|
||||||
requestText: b.request_description,
|
|
||||||
receivedAt: b.received_at,
|
|
||||||
deadline: {
|
|
||||||
originalDeadline,
|
|
||||||
currentDeadline: deadlineAt,
|
|
||||||
extended: !!b.extended_deadline_at,
|
|
||||||
extensionReason: b.extension_reason,
|
|
||||||
},
|
|
||||||
completedAt: b.completed_at,
|
|
||||||
identityVerification: {
|
|
||||||
verified: !!b.verified_at,
|
|
||||||
verifiedAt: b.verified_at,
|
|
||||||
method: b.verification_method as any,
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: b.assigned_to || null,
|
|
||||||
},
|
|
||||||
notes: b.notes,
|
|
||||||
createdAt: b.created_at,
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: b.updated_at,
|
|
||||||
tenantId: b.tenant_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSdkHeaders(): HeadersInit {
|
|
||||||
if (typeof window === 'undefined') return {}
|
|
||||||
return {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
|
||||||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch DSR list from SDK backend via proxy
|
|
||||||
*/
|
|
||||||
export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> {
|
|
||||||
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP ${res.status}`)
|
|
||||||
}
|
|
||||||
const data = await res.json()
|
|
||||||
const backendDSRs: BackendDSR[] = data.dsrs || []
|
|
||||||
const requests = backendDSRs.map(transformBackendDSR)
|
|
||||||
|
|
||||||
// Calculate statistics locally
|
|
||||||
const now = new Date()
|
|
||||||
const statistics: DSRStatistics = {
|
|
||||||
total: requests.length,
|
|
||||||
byStatus: {
|
|
||||||
intake: requests.filter(r => r.status === 'intake').length,
|
|
||||||
identity_verification: requests.filter(r => r.status === 'identity_verification').length,
|
|
||||||
processing: requests.filter(r => r.status === 'processing').length,
|
|
||||||
completed: requests.filter(r => r.status === 'completed').length,
|
|
||||||
rejected: requests.filter(r => r.status === 'rejected').length,
|
|
||||||
cancelled: requests.filter(r => r.status === 'cancelled').length,
|
|
||||||
},
|
|
||||||
byType: {
|
|
||||||
access: requests.filter(r => r.type === 'access').length,
|
|
||||||
rectification: requests.filter(r => r.type === 'rectification').length,
|
|
||||||
erasure: requests.filter(r => r.type === 'erasure').length,
|
|
||||||
restriction: requests.filter(r => r.type === 'restriction').length,
|
|
||||||
portability: requests.filter(r => r.type === 'portability').length,
|
|
||||||
objection: requests.filter(r => r.type === 'objection').length,
|
|
||||||
},
|
|
||||||
overdue: requests.filter(r => {
|
|
||||||
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false
|
|
||||||
return new Date(r.deadline.currentDeadline) < now
|
|
||||||
}).length,
|
|
||||||
dueThisWeek: requests.filter(r => {
|
|
||||||
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false
|
|
||||||
const deadline = new Date(r.deadline.currentDeadline)
|
|
||||||
const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
|
|
||||||
return deadline >= now && deadline <= weekFromNow
|
|
||||||
}).length,
|
|
||||||
averageProcessingDays: 0,
|
|
||||||
completedThisMonth: requests.filter(r => {
|
|
||||||
if (r.status !== 'completed' || !r.completedAt) return false
|
|
||||||
const completed = new Date(r.completedAt)
|
|
||||||
return completed.getMonth() === now.getMonth() && completed.getFullYear() === now.getFullYear()
|
|
||||||
}).length,
|
|
||||||
}
|
|
||||||
|
|
||||||
return { requests, statistics }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new DSR via SDK backend
|
|
||||||
*/
|
|
||||||
export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
|
|
||||||
const body = {
|
|
||||||
request_type: request.type,
|
|
||||||
subject_name: request.requester.name,
|
|
||||||
subject_email: request.requester.email,
|
|
||||||
subject_identifier: request.requester.customerId || '',
|
|
||||||
request_description: request.requestText || '',
|
|
||||||
request_channel: request.source === 'web_form' ? 'form' : request.source,
|
|
||||||
notes: '',
|
|
||||||
}
|
|
||||||
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP ${res.status}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a single DSR by ID from SDK backend
|
|
||||||
*/
|
|
||||||
export async function fetchSDKDSR(id: string): Promise<DSRRequest | null> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, {
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const data = await res.json()
|
|
||||||
if (!data || !data.id) return null
|
|
||||||
return transformBackendDSR(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update DSR status via SDK backend
|
|
||||||
*/
|
|
||||||
export async function updateSDKDSRStatus(id: string, status: string): Promise<void> {
|
|
||||||
const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: getSdkHeaders(),
|
|
||||||
body: JSON.stringify({ status }),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error(`HTTP ${res.status}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MOCK DATA FUNCTIONS (kept as fallback)
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export function createMockDSRList(): DSRRequest[] {
|
|
||||||
const now = new Date()
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'dsr-001',
|
|
||||||
referenceNumber: 'DSR-2025-000001',
|
|
||||||
type: 'access',
|
|
||||||
status: 'intake',
|
|
||||||
priority: 'high',
|
|
||||||
requester: {
|
|
||||||
name: 'Max Mustermann',
|
|
||||||
email: 'max.mustermann@example.de'
|
|
||||||
},
|
|
||||||
source: 'web_form',
|
|
||||||
sourceDetails: 'Kontaktformular auf breakpilot.de',
|
|
||||||
receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
identityVerification: {
|
|
||||||
verified: false
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: null
|
|
||||||
},
|
|
||||||
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-002',
|
|
||||||
referenceNumber: 'DSR-2025-000002',
|
|
||||||
type: 'erasure',
|
|
||||||
status: 'identity_verification',
|
|
||||||
priority: 'high',
|
|
||||||
requester: {
|
|
||||||
name: 'Anna Schmidt',
|
|
||||||
email: 'anna.schmidt@example.de',
|
|
||||||
phone: '+49 170 1234567'
|
|
||||||
},
|
|
||||||
source: 'email',
|
|
||||||
requestText: 'Ich moechte, dass alle meine Daten geloescht werden.',
|
|
||||||
receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
identityVerification: {
|
|
||||||
verified: false
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'DSB Mueller',
|
|
||||||
assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-003',
|
|
||||||
referenceNumber: 'DSR-2025-000003',
|
|
||||||
type: 'rectification',
|
|
||||||
status: 'processing',
|
|
||||||
priority: 'normal',
|
|
||||||
requester: {
|
|
||||||
name: 'Peter Meier',
|
|
||||||
email: 'peter.meier@example.de'
|
|
||||||
},
|
|
||||||
source: 'email',
|
|
||||||
requestText: 'Meine Adresse ist falsch gespeichert.',
|
|
||||||
receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
identityVerification: {
|
|
||||||
verified: true,
|
|
||||||
method: 'existing_account',
|
|
||||||
verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
verifiedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'DSB Mueller',
|
|
||||||
assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
rectificationDetails: {
|
|
||||||
fieldsToCorrect: [
|
|
||||||
{
|
|
||||||
field: 'Adresse',
|
|
||||||
currentValue: 'Musterstr. 1, 12345 Berlin',
|
|
||||||
requestedValue: 'Musterstr. 10, 12345 Berlin',
|
|
||||||
corrected: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-004',
|
|
||||||
referenceNumber: 'DSR-2025-000004',
|
|
||||||
type: 'portability',
|
|
||||||
status: 'processing',
|
|
||||||
priority: 'normal',
|
|
||||||
requester: {
|
|
||||||
name: 'Lisa Weber',
|
|
||||||
email: 'lisa.weber@example.de'
|
|
||||||
},
|
|
||||||
source: 'web_form',
|
|
||||||
receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
identityVerification: {
|
|
||||||
verified: true,
|
|
||||||
method: 'id_document',
|
|
||||||
verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
verifiedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'IT Team',
|
|
||||||
assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
notes: 'JSON-Export wird vorbereitet',
|
|
||||||
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-005',
|
|
||||||
referenceNumber: 'DSR-2025-000005',
|
|
||||||
type: 'objection',
|
|
||||||
status: 'rejected',
|
|
||||||
priority: 'low',
|
|
||||||
requester: {
|
|
||||||
name: 'Thomas Klein',
|
|
||||||
email: 'thomas.klein@example.de'
|
|
||||||
},
|
|
||||||
source: 'letter',
|
|
||||||
requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.',
|
|
||||||
receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
identityVerification: {
|
|
||||||
verified: true,
|
|
||||||
method: 'postal',
|
|
||||||
verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
verifiedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'Rechtsabteilung',
|
|
||||||
assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
objectionDetails: {
|
|
||||||
processingPurpose: 'Marketing',
|
|
||||||
legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))',
|
|
||||||
objectionGrounds: 'Keine konkreten Gruende genannt',
|
|
||||||
decision: 'rejected',
|
|
||||||
decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen',
|
|
||||||
decisionBy: 'Rechtsabteilung',
|
|
||||||
decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende',
|
|
||||||
createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'dsr-006',
|
|
||||||
referenceNumber: 'DSR-2025-000006',
|
|
||||||
type: 'access',
|
|
||||||
status: 'completed',
|
|
||||||
priority: 'normal',
|
|
||||||
requester: {
|
|
||||||
name: 'Sarah Braun',
|
|
||||||
email: 'sarah.braun@example.de'
|
|
||||||
},
|
|
||||||
source: 'email',
|
|
||||||
receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
deadline: {
|
|
||||||
originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
extended: false
|
|
||||||
},
|
|
||||||
completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
identityVerification: {
|
|
||||||
verified: true,
|
|
||||||
method: 'id_document',
|
|
||||||
verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
verifiedBy: 'DSB Mueller'
|
|
||||||
},
|
|
||||||
assignment: {
|
|
||||||
assignedTo: 'DSB Mueller',
|
|
||||||
assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString()
|
|
||||||
},
|
|
||||||
dataExport: {
|
|
||||||
format: 'pdf',
|
|
||||||
generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
generatedBy: 'DSB Mueller',
|
|
||||||
fileName: 'datenauskunft_sarah_braun.pdf',
|
|
||||||
fileSize: 245000,
|
|
||||||
includesThirdPartyData: false
|
|
||||||
},
|
|
||||||
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
createdBy: 'system',
|
|
||||||
updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
tenantId: 'default-tenant'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createMockStatistics(): DSRStatistics {
|
|
||||||
return {
|
|
||||||
total: 6,
|
|
||||||
byStatus: {
|
|
||||||
intake: 1,
|
|
||||||
identity_verification: 1,
|
|
||||||
processing: 2,
|
|
||||||
completed: 1,
|
|
||||||
rejected: 1,
|
|
||||||
cancelled: 0
|
|
||||||
},
|
|
||||||
byType: {
|
|
||||||
access: 2,
|
|
||||||
rectification: 1,
|
|
||||||
erasure: 1,
|
|
||||||
restriction: 0,
|
|
||||||
portability: 1,
|
|
||||||
objection: 1
|
|
||||||
},
|
|
||||||
overdue: 0,
|
|
||||||
dueThisWeek: 2,
|
|
||||||
averageProcessingDays: 18,
|
|
||||||
completedThisMonth: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user