/** * DSR API Client * * API client for Data Subject Request management * Connects to the Go Consent Service backend */ import { DSRRequest, DSRListResponse, DSRFilters, DSRCreateRequest, DSRUpdateRequest, DSRVerifyIdentityRequest, DSRCompleteRequest, DSRRejectRequest, DSRExtendDeadlineRequest, DSRSendCommunicationRequest, DSRCommunication, DSRAuditEntry, DSRStatistics, DSRDataExport, DSRErasureChecklist } from './types' // ============================================================================= // CONFIGURATION // ============================================================================= const DSR_API_BASE = process.env.NEXT_PUBLIC_CONSENT_SERVICE_URL || 'http://localhost:8081' const API_TIMEOUT = 30000 // 30 seconds // ============================================================================= // HELPER FUNCTIONS // ============================================================================= function getTenantId(): string { // In a real app, this would come from auth context or localStorage if (typeof window !== 'undefined') { return localStorage.getItem('tenantId') || 'default-tenant' } return 'default-tenant' } function getAuthHeaders(): HeadersInit { const headers: HeadersInit = { 'Content-Type': 'application/json', 'X-Tenant-ID': getTenantId() } // Add auth token if available if (typeof window !== 'undefined') { const token = localStorage.getItem('authToken') if (token) { headers['Authorization'] = `Bearer ${token}` } } return headers } async function fetchWithTimeout( url: string, options: RequestInit = {}, timeout: number = API_TIMEOUT ): Promise { const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), timeout) try { const response = await fetch(url, { ...options, signal: controller.signal, headers: { ...getAuthHeaders(), ...options.headers } }) if (!response.ok) { const errorBody = await response.text() let errorMessage = `HTTP ${response.status}: ${response.statusText}` try { const errorJson = JSON.parse(errorBody) errorMessage = errorJson.error || errorJson.message || errorMessage } catch { // Keep the HTTP status message } throw new Error(errorMessage) } // Handle empty responses const contentType = response.headers.get('content-type') if (contentType && contentType.includes('application/json')) { return response.json() } return {} as T } finally { clearTimeout(timeoutId) } } // ============================================================================= // DSR LIST & CRUD // ============================================================================= /** * Fetch all DSR requests with optional filters */ export async function fetchDSRList(filters?: DSRFilters): Promise { const params = new URLSearchParams() if (filters) { if (filters.status) { const statuses = Array.isArray(filters.status) ? filters.status : [filters.status] statuses.forEach(s => params.append('status', s)) } if (filters.type) { const types = Array.isArray(filters.type) ? filters.type : [filters.type] types.forEach(t => params.append('type', t)) } if (filters.priority) params.set('priority', filters.priority) if (filters.assignedTo) params.set('assignedTo', filters.assignedTo) if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue)) if (filters.search) params.set('search', filters.search) if (filters.dateFrom) params.set('dateFrom', filters.dateFrom) if (filters.dateTo) params.set('dateTo', filters.dateTo) } const queryString = params.toString() const url = `${DSR_API_BASE}/api/v1/admin/dsr${queryString ? `?${queryString}` : ''}` return fetchWithTimeout(url) } /** * Fetch a single DSR request by ID */ export async function fetchDSR(id: string): Promise { return fetchWithTimeout(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`) } /** * Create a new DSR request */ export async function createDSR(request: DSRCreateRequest): Promise { return fetchWithTimeout(`${DSR_API_BASE}/api/v1/admin/dsr`, { method: 'POST', body: JSON.stringify(request) }) } /** * Update a DSR request */ export async function updateDSR(id: string, update: DSRUpdateRequest): Promise { return fetchWithTimeout(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, { method: 'PUT', body: JSON.stringify(update) }) } /** * Delete a DSR request (soft delete - marks as cancelled) */ export async function deleteDSR(id: string): Promise { await fetchWithTimeout(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, { method: 'DELETE' }) } // ============================================================================= // DSR WORKFLOW ACTIONS // ============================================================================= /** * Verify the identity of the requester */ export async function verifyIdentity( dsrId: string, verification: DSRVerifyIdentityRequest ): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/verify-identity`, { method: 'POST', body: JSON.stringify(verification) } ) } /** * Complete a DSR request */ export async function completeDSR( dsrId: string, completion?: DSRCompleteRequest ): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/complete`, { method: 'POST', body: JSON.stringify(completion || {}) } ) } /** * Reject a DSR request */ export async function rejectDSR( dsrId: string, rejection: DSRRejectRequest ): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/reject`, { method: 'POST', body: JSON.stringify(rejection) } ) } /** * Extend the deadline for a DSR request */ export async function extendDeadline( dsrId: string, extension: DSRExtendDeadlineRequest ): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/extend`, { method: 'POST', body: JSON.stringify(extension) } ) } /** * Assign a DSR request to a user */ export async function assignDSR( dsrId: string, assignedTo: string ): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/assign`, { method: 'POST', body: JSON.stringify({ assignedTo }) } ) } // ============================================================================= // COMMUNICATION // ============================================================================= /** * Get all communications for a DSR request */ export async function getCommunications(dsrId: string): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/communications` ) } /** * Send a communication (email, letter, internal note) */ export async function sendCommunication( dsrId: string, communication: DSRSendCommunicationRequest ): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/send-communication`, { method: 'POST', body: JSON.stringify(communication) } ) } // ============================================================================= // AUDIT LOG // ============================================================================= /** * Get audit log entries for a DSR request */ export async function getAuditLog(dsrId: string): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/audit` ) } // ============================================================================= // STATISTICS // ============================================================================= /** * Get DSR statistics */ export async function getDSRStatistics(): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/statistics` ) } // ============================================================================= // DATA EXPORT (Art. 15, 20) // ============================================================================= /** * Generate data export for Art. 15 (access) or Art. 20 (portability) */ export async function generateDataExport( dsrId: string, format: 'json' | 'csv' | 'xml' | 'pdf' = 'json' ): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export`, { method: 'POST', body: JSON.stringify({ format }) } ) } /** * Download generated data export */ export async function downloadDataExport(dsrId: string): Promise { const response = await fetch( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export/download`, { headers: getAuthHeaders() } ) if (!response.ok) { throw new Error(`Download failed: ${response.statusText}`) } return response.blob() } // ============================================================================= // ERASURE CHECKLIST (Art. 17) // ============================================================================= /** * Get the erasure checklist for an Art. 17 request */ export async function getErasureChecklist(dsrId: string): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist` ) } /** * Update the erasure checklist */ export async function updateErasureChecklist( dsrId: string, checklist: DSRErasureChecklist ): Promise { return fetchWithTimeout( `${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`, { method: 'PUT', body: JSON.stringify(checklist) } ) } // ============================================================================= // EMAIL TEMPLATES // ============================================================================= /** * Get available email templates */ export async function getEmailTemplates(): Promise<{ id: string; name: string; stage: string }[]> { return fetchWithTimeout<{ id: string; name: string; stage: string }[]>( `${DSR_API_BASE}/api/v1/admin/dsr/email-templates` ) } /** * Preview an email with variables filled in */ export async function previewEmail( templateId: string, dsrId: string ): Promise<{ subject: string; body: string }> { return fetchWithTimeout<{ subject: string; body: string }>( `${DSR_API_BASE}/api/v1/admin/dsr/email-templates/${templateId}/preview`, { method: 'POST', body: JSON.stringify({ dsrId }) } ) } // ============================================================================= // SDK API FUNCTIONS (via Next.js proxy to ai-compliance-sdk) // ============================================================================= interface BackendDSR { id: string tenant_id: string namespace_id?: string request_type: string status: string subject_name: string subject_email: string subject_identifier?: string request_description: string request_channel: string received_at: string verified_at?: string verification_method?: string deadline_at: string extended_deadline_at?: string extension_reason?: string completed_at?: string response_sent: boolean response_sent_at?: string response_method?: string rejection_reason?: string notes?: string affected_systems?: string[] assigned_to?: string created_at: string updated_at: string } function mapBackendStatus(status: string): import('./types').DSRStatus { const mapping: Record = { 'received': 'intake', 'verified': 'identity_verification', 'in_progress': 'processing', 'completed': 'completed', 'rejected': 'rejected', 'extended': 'processing', } return mapping[status] || 'intake' } function mapBackendChannel(channel: string): import('./types').DSRSource { const mapping: Record = { 'email': 'email', 'form': 'web_form', 'phone': 'phone', 'letter': 'letter', } return mapping[channel] || 'other' } /** * Transform flat backend DSR to nested SDK DSRRequest format */ export function transformBackendDSR(b: BackendDSR): DSRRequest { const deadlineAt = b.extended_deadline_at || b.deadline_at const receivedDate = new Date(b.received_at) const defaultDeadlineDays = 30 const originalDeadline = b.deadline_at || new Date(receivedDate.getTime() + defaultDeadlineDays * 24 * 60 * 60 * 1000).toISOString() return { id: b.id, referenceNumber: `DSR-${new Date(b.created_at).getFullYear()}-${b.id.slice(0, 6).toUpperCase()}`, type: b.request_type as DSRRequest['type'], status: mapBackendStatus(b.status), priority: 'normal', requester: { name: b.subject_name, email: b.subject_email, customerId: b.subject_identifier, }, source: mapBackendChannel(b.request_channel), requestText: b.request_description, receivedAt: b.received_at, deadline: { originalDeadline, currentDeadline: deadlineAt, extended: !!b.extended_deadline_at, extensionReason: b.extension_reason, }, completedAt: b.completed_at, identityVerification: { verified: !!b.verified_at, verifiedAt: b.verified_at, method: b.verification_method as any, }, assignment: { assignedTo: b.assigned_to || null, }, notes: b.notes, createdAt: b.created_at, createdBy: 'system', updatedAt: b.updated_at, tenantId: b.tenant_id, } } function getSdkHeaders(): HeadersInit { if (typeof window === 'undefined') return {} return { 'Content-Type': 'application/json', 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } } /** * Fetch DSR list from SDK backend via proxy */ export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> { const res = await fetch('/api/sdk/v1/dsgvo/dsr', { headers: getSdkHeaders(), }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } const data = await res.json() const backendDSRs: BackendDSR[] = data.dsrs || [] const requests = backendDSRs.map(transformBackendDSR) // Calculate statistics locally const now = new Date() const statistics: DSRStatistics = { total: requests.length, byStatus: { intake: requests.filter(r => r.status === 'intake').length, identity_verification: requests.filter(r => r.status === 'identity_verification').length, processing: requests.filter(r => r.status === 'processing').length, completed: requests.filter(r => r.status === 'completed').length, rejected: requests.filter(r => r.status === 'rejected').length, cancelled: requests.filter(r => r.status === 'cancelled').length, }, byType: { access: requests.filter(r => r.type === 'access').length, rectification: requests.filter(r => r.type === 'rectification').length, erasure: requests.filter(r => r.type === 'erasure').length, restriction: requests.filter(r => r.type === 'restriction').length, portability: requests.filter(r => r.type === 'portability').length, objection: requests.filter(r => r.type === 'objection').length, }, overdue: requests.filter(r => { if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false return new Date(r.deadline.currentDeadline) < now }).length, dueThisWeek: requests.filter(r => { if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false const deadline = new Date(r.deadline.currentDeadline) const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000) return deadline >= now && deadline <= weekFromNow }).length, averageProcessingDays: 0, completedThisMonth: requests.filter(r => { if (r.status !== 'completed' || !r.completedAt) return false const completed = new Date(r.completedAt) return completed.getMonth() === now.getMonth() && completed.getFullYear() === now.getFullYear() }).length, } return { requests, statistics } } /** * Create a new DSR via SDK backend */ export async function createSDKDSR(request: DSRCreateRequest): Promise { const body = { request_type: request.type, subject_name: request.requester.name, subject_email: request.requester.email, subject_identifier: request.requester.customerId || '', request_description: request.requestText || '', request_channel: request.source === 'web_form' ? 'form' : request.source, notes: '', } const res = await fetch('/api/sdk/v1/dsgvo/dsr', { method: 'POST', headers: getSdkHeaders(), body: JSON.stringify(body), }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } } /** * Fetch a single DSR by ID from SDK backend */ export async function fetchSDKDSR(id: string): Promise { const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, { headers: getSdkHeaders(), }) if (!res.ok) { return null } const data = await res.json() if (!data || !data.id) return null return transformBackendDSR(data) } /** * Update DSR status via SDK backend */ export async function updateSDKDSRStatus(id: string, status: string): Promise { const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, { method: 'PUT', headers: getSdkHeaders(), body: JSON.stringify({ status }), }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } } // ============================================================================= // MOCK DATA FUNCTIONS (kept as fallback) // ============================================================================= export function createMockDSRList(): DSRRequest[] { const now = new Date() return [ { id: 'dsr-001', referenceNumber: 'DSR-2025-000001', type: 'access', status: 'intake', priority: 'high', requester: { name: 'Max Mustermann', email: 'max.mustermann@example.de' }, source: 'web_form', sourceDetails: 'Kontaktformular auf breakpilot.de', receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, identityVerification: { verified: false }, assignment: { assignedTo: null }, createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-002', referenceNumber: 'DSR-2025-000002', type: 'erasure', status: 'identity_verification', priority: 'high', requester: { name: 'Anna Schmidt', email: 'anna.schmidt@example.de', phone: '+49 170 1234567' }, source: 'email', requestText: 'Ich moechte, dass alle meine Daten geloescht werden.', receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, identityVerification: { verified: false }, assignment: { assignedTo: 'DSB Mueller', assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString() }, createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-003', referenceNumber: 'DSR-2025-000003', type: 'rectification', status: 'processing', priority: 'normal', requester: { name: 'Peter Meier', email: 'peter.meier@example.de' }, source: 'email', requestText: 'Meine Adresse ist falsch gespeichert.', receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, identityVerification: { verified: true, method: 'existing_account', verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(), verifiedBy: 'DSB Mueller' }, assignment: { assignedTo: 'DSB Mueller', assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString() }, rectificationDetails: { fieldsToCorrect: [ { field: 'Adresse', currentValue: 'Musterstr. 1, 12345 Berlin', requestedValue: 'Musterstr. 10, 12345 Berlin', corrected: false } ] }, createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-004', referenceNumber: 'DSR-2025-000004', type: 'portability', status: 'processing', priority: 'normal', requester: { name: 'Lisa Weber', email: 'lisa.weber@example.de' }, source: 'web_form', receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, identityVerification: { verified: true, method: 'id_document', verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(), verifiedBy: 'DSB Mueller' }, assignment: { assignedTo: 'IT Team', assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString() }, notes: 'JSON-Export wird vorbereitet', createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-005', referenceNumber: 'DSR-2025-000005', type: 'objection', status: 'rejected', priority: 'low', requester: { name: 'Thomas Klein', email: 'thomas.klein@example.de' }, source: 'letter', requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.', receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), identityVerification: { verified: true, method: 'postal', verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(), verifiedBy: 'DSB Mueller' }, assignment: { assignedTo: 'Rechtsabteilung', assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString() }, objectionDetails: { processingPurpose: 'Marketing', legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))', objectionGrounds: 'Keine konkreten Gruende genannt', decision: 'rejected', decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen', decisionBy: 'Rechtsabteilung', decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString() }, notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende', createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' }, { id: 'dsr-006', referenceNumber: 'DSR-2025-000006', type: 'access', status: 'completed', priority: 'normal', requester: { name: 'Sarah Braun', email: 'sarah.braun@example.de' }, source: 'email', receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), deadline: { originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(), extended: false }, completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), identityVerification: { verified: true, method: 'id_document', verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(), verifiedBy: 'DSB Mueller' }, assignment: { assignedTo: 'DSB Mueller', assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString() }, dataExport: { format: 'pdf', generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), generatedBy: 'DSB Mueller', fileName: 'datenauskunft_sarah_braun.pdf', fileSize: 245000, includesThirdPartyData: false }, createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(), createdBy: 'system', updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(), tenantId: 'default-tenant' } ] } export function createMockStatistics(): DSRStatistics { return { total: 6, byStatus: { intake: 1, identity_verification: 1, processing: 2, completed: 1, rejected: 1, cancelled: 0 }, byType: { access: 2, rectification: 1, erasure: 1, restriction: 0, portability: 1, objection: 1 }, overdue: 0, dueThisWeek: 2, averageProcessingDays: 18, completedThisMonth: 1 } }