feat(dsr): Go DSR deprecated, Python Export-Endpoint, Frontend an Backend-APIs anbinden
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
- Go: DEPRECATED-Kommentare an allen DSR-Handlern und Routes - Python: GET /dsr/export?format=csv|json (Semikolon-CSV, 12 Spalten) - API-Client: 12 neue Funktionen (verify, assign, extend, complete, reject, communications, exception-checks, history) - Detail-Seite: Alle Actions verdrahtet (keine Coming-soon-Alerts mehr), Communications + Art.17(3)-Checks + Audit-Log live - Haupt-Seite: CSV-Export-Button im Header - Tests: 54/54 bestanden (4 neue Export-Tests) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,21 @@ import {
|
||||
DSRCommunication,
|
||||
DSRVerifyIdentityRequest
|
||||
} from '@/lib/sdk/dsr/types'
|
||||
import { fetchSDKDSR, updateSDKDSRStatus } from '@/lib/sdk/dsr/api'
|
||||
import {
|
||||
fetchSDKDSR,
|
||||
updateSDKDSRStatus,
|
||||
verifyDSRIdentity,
|
||||
assignDSR,
|
||||
extendDSRDeadline,
|
||||
completeDSR,
|
||||
rejectDSR,
|
||||
fetchDSRCommunications,
|
||||
sendDSRCommunication,
|
||||
fetchDSRExceptionChecks,
|
||||
initDSRExceptionChecks,
|
||||
updateDSRExceptionCheck,
|
||||
fetchDSRHistory,
|
||||
} from '@/lib/sdk/dsr/api'
|
||||
import {
|
||||
DSRWorkflowStepper,
|
||||
DSRIdentityModal,
|
||||
@@ -22,12 +36,6 @@ import {
|
||||
DSRDataExportComponent
|
||||
} from '@/components/sdk/dsr'
|
||||
|
||||
// =============================================================================
|
||||
// MOCK COMMUNICATIONS
|
||||
// =============================================================================
|
||||
|
||||
// TODO: Backend fehlt — Communications API noch nicht implementiert
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENTS
|
||||
// =============================================================================
|
||||
@@ -155,56 +163,38 @@ function ActionButtons({
|
||||
)
|
||||
}
|
||||
|
||||
function AuditLog({ request }: { request: DSRRequest }) {
|
||||
type AuditEvent = { action: string; timestamp: string; user: string }
|
||||
|
||||
const events: AuditEvent[] = [
|
||||
{ action: 'Erstellt', timestamp: request.createdAt, user: request.createdBy }
|
||||
]
|
||||
|
||||
if (request.assignment.assignedAt) {
|
||||
events.push({
|
||||
action: `Zugewiesen an ${request.assignment.assignedTo}`,
|
||||
timestamp: request.assignment.assignedAt,
|
||||
user: request.assignment.assignedBy || 'System'
|
||||
})
|
||||
}
|
||||
|
||||
if (request.identityVerification.verifiedAt) {
|
||||
events.push({
|
||||
action: 'Identitaet verifiziert',
|
||||
timestamp: request.identityVerification.verifiedAt,
|
||||
user: request.identityVerification.verifiedBy || 'System'
|
||||
})
|
||||
}
|
||||
|
||||
if (request.completedAt) {
|
||||
events.push({
|
||||
action: request.status === 'rejected' ? 'Abgelehnt' : 'Abgeschlossen',
|
||||
timestamp: request.completedAt,
|
||||
user: request.updatedBy || 'System'
|
||||
})
|
||||
}
|
||||
|
||||
function AuditLog({ history }: { history: any[] }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium text-gray-700">Aktivitaeten</h4>
|
||||
<div className="space-y-2">
|
||||
{events.map((event, idx) => (
|
||||
<div key={idx} className="flex items-start gap-2 text-xs">
|
||||
{history.length === 0 && (
|
||||
<div className="text-xs text-gray-400">Keine Eintraege</div>
|
||||
)}
|
||||
{history.map((entry, idx) => (
|
||||
<div key={entry.id || idx} className="flex items-start gap-2 text-xs">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-gray-300 mt-1.5 flex-shrink-0" />
|
||||
<div>
|
||||
<div className="text-gray-900">{event.action}</div>
|
||||
<div className="text-gray-900">
|
||||
{entry.previous_status
|
||||
? `${entry.previous_status} → ${entry.new_status}`
|
||||
: entry.new_status
|
||||
}
|
||||
{entry.comment && `: ${entry.comment}`}
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
{new Date(event.timestamp).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
{entry.created_at
|
||||
? new Date(entry.created_at).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
: ''
|
||||
}
|
||||
{' - '}
|
||||
{event.user}
|
||||
{entry.changed_by || 'System'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -225,10 +215,17 @@ export default function DSRDetailPage() {
|
||||
|
||||
const [request, setRequest] = useState<DSRRequest | null>(null)
|
||||
const [communications, setCommunications] = useState<DSRCommunication[]>([])
|
||||
const [history, setHistory] = useState<any[]>([])
|
||||
const [exceptionChecks, setExceptionChecks] = useState<any[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [showIdentityModal, setShowIdentityModal] = useState(false)
|
||||
const [activeContentTab, setActiveContentTab] = useState<'details' | 'communication' | 'type-specific'>('details')
|
||||
|
||||
const reloadRequest = async () => {
|
||||
const found = await fetchSDKDSR(requestId)
|
||||
if (found) setRequest(found)
|
||||
}
|
||||
|
||||
// Load data from SDK backend
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
@@ -237,8 +234,40 @@ export default function DSRDetailPage() {
|
||||
const found = await fetchSDKDSR(requestId)
|
||||
if (found) {
|
||||
setRequest(found)
|
||||
// TODO: Backend fehlt — Communications API noch nicht implementiert
|
||||
setCommunications([])
|
||||
// Load communications, history, and exception checks in parallel
|
||||
const [comms, hist] = await Promise.all([
|
||||
fetchDSRCommunications(requestId).catch(() => []),
|
||||
fetchDSRHistory(requestId).catch(() => []),
|
||||
])
|
||||
setCommunications(comms.map((c: any) => ({
|
||||
id: c.id,
|
||||
dsrId: c.dsr_id,
|
||||
type: c.communication_type,
|
||||
channel: c.channel,
|
||||
subject: c.subject,
|
||||
content: c.content,
|
||||
createdAt: c.created_at,
|
||||
createdBy: c.created_by,
|
||||
sentAt: c.sent_at,
|
||||
sentBy: c.sent_by,
|
||||
})))
|
||||
setHistory(hist)
|
||||
|
||||
// Load exception checks for erasure requests
|
||||
if (found.type === 'erasure') {
|
||||
try {
|
||||
const checks = await fetchDSRExceptionChecks(requestId)
|
||||
if (checks.length === 0) {
|
||||
// Auto-initialize if none exist
|
||||
const initialized = await initDSRExceptionChecks(requestId)
|
||||
setExceptionChecks(initialized)
|
||||
} else {
|
||||
setExceptionChecks(checks)
|
||||
}
|
||||
} catch {
|
||||
setExceptionChecks([])
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load DSR:', error)
|
||||
@@ -252,46 +281,109 @@ export default function DSRDetailPage() {
|
||||
const handleVerifyIdentity = async (verification: DSRVerifyIdentityRequest) => {
|
||||
if (!request) return
|
||||
try {
|
||||
await updateSDKDSRStatus(request.id, 'verified')
|
||||
setRequest({
|
||||
...request,
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: verification.method,
|
||||
verifiedAt: new Date().toISOString(),
|
||||
verifiedBy: 'Current User',
|
||||
notes: verification.notes
|
||||
},
|
||||
status: request.status === 'identity_verification' ? 'processing' : request.status
|
||||
const updated = await verifyDSRIdentity(request.id, {
|
||||
method: verification.method,
|
||||
notes: verification.notes,
|
||||
document_ref: verification.documentRef,
|
||||
})
|
||||
setRequest(updated)
|
||||
// Reload history
|
||||
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
|
||||
} catch (err) {
|
||||
console.error('Failed to verify identity:', err)
|
||||
// Still update locally as fallback
|
||||
setRequest({
|
||||
...request,
|
||||
identityVerification: {
|
||||
verified: true,
|
||||
method: verification.method,
|
||||
verifiedAt: new Date().toISOString(),
|
||||
verifiedBy: 'Current User',
|
||||
notes: verification.notes
|
||||
},
|
||||
status: request.status === 'identity_verification' ? 'processing' : request.status
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleAssign = async () => {
|
||||
if (!request) return
|
||||
const assignee = prompt('Zuweisen an (Name/ID):')
|
||||
if (!assignee) return
|
||||
try {
|
||||
const updated = await assignDSR(request.id, assignee)
|
||||
setRequest(updated)
|
||||
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
|
||||
} catch (err) {
|
||||
console.error('Failed to assign:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExtendDeadline = async () => {
|
||||
if (!request) return
|
||||
const reason = prompt('Grund fuer die Fristverlaengerung:')
|
||||
if (!reason) return
|
||||
const daysStr = prompt('Um wie viele Tage verlaengern? (Standard: 60)', '60')
|
||||
const days = parseInt(daysStr || '60', 10) || 60
|
||||
try {
|
||||
const updated = await extendDSRDeadline(request.id, reason, days)
|
||||
setRequest(updated)
|
||||
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
|
||||
} catch (err) {
|
||||
console.error('Failed to extend deadline:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (!request) return
|
||||
const summary = prompt('Zusammenfassung der Bearbeitung:')
|
||||
if (summary === null) return
|
||||
try {
|
||||
const updated = await completeDSR(request.id, summary || undefined)
|
||||
setRequest(updated)
|
||||
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
|
||||
} catch (err) {
|
||||
console.error('Failed to complete:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!request) return
|
||||
const reason = prompt('Ablehnungsgrund:')
|
||||
if (!reason) return
|
||||
const legalBasis = prompt('Rechtsgrundlage (optional):')
|
||||
try {
|
||||
const updated = await rejectDSR(request.id, reason, legalBasis || undefined)
|
||||
setRequest(updated)
|
||||
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
|
||||
} catch (err) {
|
||||
console.error('Failed to reject:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendCommunication = async (message: any) => {
|
||||
const newComm: DSRCommunication = {
|
||||
id: `comm-${Date.now()}`,
|
||||
dsrId: requestId,
|
||||
...message,
|
||||
createdAt: new Date().toISOString(),
|
||||
createdBy: 'Current User',
|
||||
sentAt: message.type === 'outgoing' ? new Date().toISOString() : undefined,
|
||||
sentBy: message.type === 'outgoing' ? 'Current User' : undefined
|
||||
if (!request) return
|
||||
try {
|
||||
const result = await sendDSRCommunication(requestId, {
|
||||
communication_type: message.type || 'outgoing',
|
||||
channel: message.channel || 'email',
|
||||
subject: message.subject,
|
||||
content: message.content,
|
||||
})
|
||||
// Map backend response to frontend format
|
||||
const newComm: DSRCommunication = {
|
||||
id: result.id,
|
||||
dsrId: result.dsr_id,
|
||||
type: result.communication_type,
|
||||
channel: result.channel,
|
||||
subject: result.subject,
|
||||
content: result.content,
|
||||
createdAt: result.created_at,
|
||||
createdBy: result.created_by || 'Current User',
|
||||
sentAt: result.sent_at,
|
||||
sentBy: result.sent_by,
|
||||
}
|
||||
setCommunications(prev => [newComm, ...prev])
|
||||
} catch (err) {
|
||||
console.error('Failed to send communication:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExceptionCheckChange = async (checkId: string, applies: boolean, notes?: string) => {
|
||||
try {
|
||||
const updated = await updateDSRExceptionCheck(requestId, checkId, { applies, notes })
|
||||
setExceptionChecks(prev => prev.map(c => c.id === checkId ? updated : c))
|
||||
} catch (err) {
|
||||
console.error('Failed to update exception check:', err)
|
||||
}
|
||||
setCommunications(prev => [newComm, ...prev])
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
@@ -528,10 +620,48 @@ export default function DSRDetailPage() {
|
||||
<div>
|
||||
{/* Art. 17 - Erasure */}
|
||||
{request.type === 'erasure' && (
|
||||
<DSRErasureChecklistComponent
|
||||
checklist={request.erasureChecklist}
|
||||
onChange={(checklist) => setRequest({ ...request, erasureChecklist: checklist })}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<DSRErasureChecklistComponent
|
||||
checklist={request.erasureChecklist}
|
||||
onChange={(checklist) => setRequest({ ...request, erasureChecklist: checklist })}
|
||||
/>
|
||||
|
||||
{/* Art. 17(3) Exception Checks from Backend */}
|
||||
{exceptionChecks.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">Art. 17(3) Ausnahmepruefung</h3>
|
||||
<p className="text-sm text-gray-500 mb-4">
|
||||
Pruefen Sie, ob eine der gesetzlichen Ausnahmen zur Loeschpflicht greift.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{exceptionChecks.map((check) => (
|
||||
<div key={check.id} className="bg-gray-50 rounded-xl p-4 border border-gray-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={check.applies || false}
|
||||
onChange={(e) => handleExceptionCheckChange(check.id, e.target.checked, check.notes)}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-900 text-sm">
|
||||
{check.article}: {check.label}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{check.description}</div>
|
||||
{check.checked_by && (
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Geprueft von {check.checked_by} am{' '}
|
||||
{check.checked_at ? new Date(check.checked_at).toLocaleDateString('de-DE') : '-'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Art. 15/20 - Data Export */}
|
||||
@@ -697,16 +827,16 @@ export default function DSRDetailPage() {
|
||||
<ActionButtons
|
||||
request={request}
|
||||
onVerifyIdentity={() => setShowIdentityModal(true)}
|
||||
onExtendDeadline={() => alert('Fristverlaengerung - Coming soon')}
|
||||
onComplete={() => alert('Abschliessen - Coming soon')}
|
||||
onReject={() => alert('Ablehnen - Coming soon')}
|
||||
onAssign={() => alert('Zuweisen - Coming soon')}
|
||||
onExtendDeadline={handleExtendDeadline}
|
||||
onComplete={handleComplete}
|
||||
onReject={handleReject}
|
||||
onAssign={handleAssign}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Audit Log Card */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<AuditLog request={request} />
|
||||
<AuditLog history={history} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -808,15 +808,31 @@ export default function DSRPage() {
|
||||
explanation={stepInfo.explanation}
|
||||
tips={stepInfo.tips}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Anfrage erfassen
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
const link = document.createElement('a')
|
||||
link.href = '/api/sdk/v1/compliance/dsr/export?format=csv'
|
||||
link.download = 'dsr_export.csv'
|
||||
link.click()
|
||||
}}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" 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>
|
||||
CSV Export
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
Anfrage erfassen
|
||||
</button>
|
||||
</div>
|
||||
</StepHeader>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
|
||||
@@ -245,6 +245,171 @@ export async function updateSDKDSRStatus(id: string, status: string): Promise<vo
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WORKFLOW ACTIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Verify identity of DSR requester
|
||||
*/
|
||||
export async function verifyDSRIdentity(id: string, data: { method: string; notes?: string; document_ref?: string }): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/verify-identity`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign DSR to a user
|
||||
*/
|
||||
export async function assignDSR(id: string, assigneeId: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/assign`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ assignee_id: assigneeId }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend DSR deadline (Art. 12 Abs. 3 DSGVO)
|
||||
*/
|
||||
export async function extendDSRDeadline(id: string, reason: string, days: number = 60): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/extend`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ reason, days }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a DSR
|
||||
*/
|
||||
export async function completeDSR(id: string, summary?: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/complete`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ summary }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject a DSR with legal basis
|
||||
*/
|
||||
export async function rejectDSR(id: string, reason: string, legalBasis?: string): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/reject`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ reason, legal_basis: legalBasis }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATIONS
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch communications for a DSR
|
||||
*/
|
||||
export async function fetchDSRCommunications(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communications`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a communication for a DSR
|
||||
*/
|
||||
export async function sendDSRCommunication(id: string, data: { communication_type?: string; channel?: string; subject?: string; content: string }): Promise<any> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/communicate`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify({ communication_type: 'outgoing', channel: 'email', ...data }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// EXCEPTION CHECKS (Art. 17)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch exception checks for an erasure DSR
|
||||
*/
|
||||
export async function fetchDSRExceptionChecks(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Art. 17(3) exception checks for an erasure DSR
|
||||
*/
|
||||
export async function initDSRExceptionChecks(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/exception-checks/init`, {
|
||||
method: 'POST',
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single exception check
|
||||
*/
|
||||
export async function updateDSRExceptionCheck(dsrId: string, checkId: string, data: { applies: boolean; notes?: string }): Promise<any> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${dsrId}/exception-checks/${checkId}`, {
|
||||
method: 'PUT',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HISTORY
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Fetch status change history for a DSR
|
||||
*/
|
||||
export async function fetchDSRHistory(id: string): Promise<any[]> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}/history`, {
|
||||
headers: getSdkHeaders(),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update DSR fields (priority, notes, etc.)
|
||||
*/
|
||||
export async function updateDSR(id: string, data: Record<string, any>): Promise<DSRRequest> {
|
||||
const res = await fetch(`/api/sdk/v1/compliance/dsr/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: getSdkHeaders(),
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
||||
return transformBackendDSR(await res.json())
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MOCK DATA FUNCTIONS (kept as fallback)
|
||||
// =============================================================================
|
||||
|
||||
@@ -273,6 +273,8 @@ func main() {
|
||||
}
|
||||
|
||||
// DSR - Data Subject Requests / Betroffenenrechte (Art. 15-22)
|
||||
// DEPRECATED: DSR is now managed by backend-compliance (Python).
|
||||
// Use: GET/POST/PUT /api/compliance/dsr/* on backend-compliance:8002
|
||||
dsr := dsgvoRoutes.Group("/dsr")
|
||||
{
|
||||
dsr.GET("", dsgvoHandlers.ListDSRs)
|
||||
@@ -304,7 +306,7 @@ func main() {
|
||||
{
|
||||
exports.GET("/vvt", dsgvoHandlers.ExportVVT) // DEPRECATED: use backend-compliance /vvt/export?format=csv
|
||||
exports.GET("/tom", dsgvoHandlers.ExportTOM) // DEPRECATED: use backend-compliance /tom/export?format=csv
|
||||
exports.GET("/dsr", dsgvoHandlers.ExportDSR)
|
||||
exports.GET("/dsr", dsgvoHandlers.ExportDSR) // DEPRECATED: use backend-compliance /dsr/export?format=csv
|
||||
exports.GET("/retention", dsgvoHandlers.ExportRetentionPolicies)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,9 +220,12 @@ func (h *DSGVOHandlers) CreateTOM(c *gin.Context) {
|
||||
|
||||
// ============================================================================
|
||||
// DSR - Data Subject Requests
|
||||
// DEPRECATED: DSR is now managed by backend-compliance (Python/FastAPI).
|
||||
// Use: /api/compliance/dsr/* endpoints on backend-compliance:8002
|
||||
// ============================================================================
|
||||
|
||||
// ListDSRs returns all DSRs for a tenant
|
||||
// DEPRECATED: Use backend-compliance GET /api/compliance/dsr
|
||||
func (h *DSGVOHandlers) ListDSRs(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
@@ -243,6 +246,7 @@ func (h *DSGVOHandlers) ListDSRs(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetDSR returns a DSR by ID
|
||||
// DEPRECATED: Use backend-compliance GET /api/compliance/dsr/{id}
|
||||
func (h *DSGVOHandlers) GetDSR(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -264,6 +268,7 @@ func (h *DSGVOHandlers) GetDSR(c *gin.Context) {
|
||||
}
|
||||
|
||||
// CreateDSR creates a new DSR
|
||||
// DEPRECATED: Use backend-compliance POST /api/compliance/dsr
|
||||
func (h *DSGVOHandlers) CreateDSR(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
userID := rbac.GetUserID(c)
|
||||
@@ -293,6 +298,7 @@ func (h *DSGVOHandlers) CreateDSR(c *gin.Context) {
|
||||
}
|
||||
|
||||
// UpdateDSR updates a DSR
|
||||
// DEPRECATED: Use backend-compliance PUT /api/compliance/dsr/{id}
|
||||
func (h *DSGVOHandlers) UpdateDSR(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -612,6 +618,7 @@ func (h *DSGVOHandlers) ExportTOM(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ExportDSR exports DSR overview as CSV/JSON
|
||||
// DEPRECATED: Use backend-compliance GET /api/compliance/dsr/export?format=csv|json
|
||||
func (h *DSGVOHandlers) ExportDSR(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
|
||||
@@ -4,11 +4,14 @@ DSR (Data Subject Request) Routes — Betroffenenanfragen nach DSGVO Art. 15-21.
|
||||
Native Python/FastAPI Implementierung, ersetzt Go consent-service Proxy.
|
||||
"""
|
||||
|
||||
import io
|
||||
import csv
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Header
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text, func, and_, or_, cast, String
|
||||
@@ -438,6 +441,61 @@ async def get_dsr_stats(
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Export
|
||||
# =============================================================================
|
||||
|
||||
@router.get("/export")
|
||||
async def export_dsrs(
|
||||
format: str = Query("csv", pattern="^(csv|json)$"),
|
||||
tenant_id: str = Depends(_get_tenant),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Exportiert alle DSRs als CSV oder JSON."""
|
||||
tid = uuid.UUID(tenant_id)
|
||||
dsrs = db.query(DSRRequestDB).filter(
|
||||
DSRRequestDB.tenant_id == tid,
|
||||
).order_by(DSRRequestDB.created_at.desc()).all()
|
||||
|
||||
if format == "json":
|
||||
return {
|
||||
"exported_at": datetime.utcnow().isoformat(),
|
||||
"total": len(dsrs),
|
||||
"requests": [_dsr_to_dict(d) for d in dsrs],
|
||||
}
|
||||
|
||||
# CSV export (semicolon-separated, matching Go format + extended fields)
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL)
|
||||
writer.writerow([
|
||||
"ID", "Referenznummer", "Typ", "Name", "E-Mail", "Status",
|
||||
"Prioritaet", "Eingegangen", "Frist", "Abgeschlossen", "Quelle", "Zugewiesen",
|
||||
])
|
||||
|
||||
for dsr in dsrs:
|
||||
writer.writerow([
|
||||
str(dsr.id),
|
||||
dsr.request_number or "",
|
||||
dsr.request_type or "",
|
||||
dsr.requester_name or "",
|
||||
dsr.requester_email or "",
|
||||
dsr.status or "",
|
||||
dsr.priority or "",
|
||||
dsr.received_at.strftime("%Y-%m-%d") if dsr.received_at else "",
|
||||
dsr.deadline_at.strftime("%Y-%m-%d") if dsr.deadline_at else "",
|
||||
dsr.completed_at.strftime("%Y-%m-%d") if dsr.completed_at else "",
|
||||
dsr.source or "",
|
||||
dsr.assigned_to or "",
|
||||
])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
output,
|
||||
media_type="text/csv; charset=utf-8",
|
||||
headers={"Content-Disposition": "attachment; filename=dsr_export.csv"},
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Deadline Processing (MUST be before /{dsr_id} to avoid path conflicts)
|
||||
# =============================================================================
|
||||
|
||||
@@ -697,3 +697,49 @@ class TestDSRTemplates:
|
||||
fake_id = str(uuid.uuid4())
|
||||
resp = client.get(f"/api/compliance/dsr/templates/{fake_id}/versions", headers=HEADERS)
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
class TestDSRExport:
|
||||
"""Tests for DSR export endpoint."""
|
||||
|
||||
def test_export_csv_empty(self):
|
||||
resp = client.get("/api/compliance/dsr/export?format=csv", headers=HEADERS)
|
||||
assert resp.status_code == 200
|
||||
assert "text/csv" in resp.headers.get("content-type", "")
|
||||
lines = resp.text.strip().split("\n")
|
||||
assert len(lines) == 1 # Header only
|
||||
assert "Referenznummer" in lines[0]
|
||||
assert "Zugewiesen" in lines[0]
|
||||
|
||||
def test_export_csv_with_data(self):
|
||||
# Create a DSR first
|
||||
body = {
|
||||
"request_type": "access",
|
||||
"requester_name": "Export Test",
|
||||
"requester_email": "export@example.de",
|
||||
"source": "email",
|
||||
}
|
||||
create_resp = client.post("/api/compliance/dsr", json=body, headers=HEADERS)
|
||||
assert create_resp.status_code == 200
|
||||
|
||||
resp = client.get("/api/compliance/dsr/export?format=csv", headers=HEADERS)
|
||||
assert resp.status_code == 200
|
||||
lines = resp.text.strip().split("\n")
|
||||
assert len(lines) >= 2 # Header + at least 1 data row
|
||||
# Check data row contains our test data
|
||||
assert "Export Test" in lines[1]
|
||||
assert "export@example.de" in lines[1]
|
||||
assert "access" in lines[1]
|
||||
|
||||
def test_export_json(self):
|
||||
resp = client.get("/api/compliance/dsr/export?format=json", headers=HEADERS)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert "exported_at" in data
|
||||
assert "total" in data
|
||||
assert "requests" in data
|
||||
assert isinstance(data["requests"], list)
|
||||
|
||||
def test_export_invalid_format(self):
|
||||
resp = client.get("/api/compliance/dsr/export?format=xml", headers=HEADERS)
|
||||
assert resp.status_code == 422 # Validation error
|
||||
|
||||
Reference in New Issue
Block a user