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')
|
||||
}
|
||||
Reference in New Issue
Block a user