From 05bc1e254365dfcfc7f335562d84d6d824410995 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Thu, 27 Aug 2026 01:49:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(compliance):=20G1-b=20frontend=20DSAR=20st?= =?UTF-8?q?atus=20UI=20=E2=80=94=204th=20subtab=20in=20ComplianceTab:=20ty?= =?UTF-8?q?pe=20selection=20(Art.15/17/16),=20person=20picker,=20direct=20?= =?UTF-8?q?GDPR=20export=20download,=20two-step=20deletion=20confirmation;?= =?UTF-8?q?=20uses=20existing=20system-settings=20DSAR=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/compliance.ts | 38 ++++++++ frontend/src/i18n/locales/de.json | 18 ++++ frontend/src/pages/ComplianceTab.tsx | 141 ++++++++++++++++++++++++++- 3 files changed, 196 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/compliance.ts b/frontend/src/api/compliance.ts index 973cf24..059c1a0 100644 --- a/frontend/src/api/compliance.ts +++ b/frontend/src/api/compliance.ts @@ -145,3 +145,41 @@ export async function updateRetentionPolicy( { days } ); } + +// ─── DSAR (GDPR Art. 15/17/20) ─── + +export type DsarType = 'access' | 'deletion' | 'rectification'; + +export interface DsarRequestResponse { + job_id: string; + status: string; + type: DsarType; + user_id: string; +} + +/** Queue a DSAR job for a user. Admin only. */ +export async function submitDsarRequest( + userId: string, + type: DsarType +): Promise { + return apiPost(`/system-settings/dsar/${userId}`, { + type, + }); +} + +/** Stream the full GDPR data export for a user and trigger a browser download. */ +export async function downloadDsgvoExport(userId: string, userName?: string): Promise { + const response = await apiGet(`/system-settings/dsgvo-export/${userId}`, { + responseType: 'blob', + }); + const blob = new Blob([response], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + const safeName = (userName ?? userId).replace(/[^a-z0-9_-]/gi, '_'); + link.download = `dsgvo_export_${safeName}.json`; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); +} diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index d084745..89c9408 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -2051,5 +2051,23 @@ }, "index": { "seitewirdgeladen": "Seite wird geladen" + }, + "compliance": { + "dsar": { + "tab": "DSGVO-Anfragen", + "description": "DSGVO-Anfrage für eine Person auslösen: Auskunft (Art. 15), Löschung (Art. 17) oder Berichtigung (Art. 16). Der Vorgang wird als Hintergrund-Job durch den Worker ausgeführt.", + "person": "Person", + "selectPerson": "-- Bitte wählen --", + "requestType": "Antragsart", + "typeAccess": "Auskunft (Art. 15)", + "typeDeletion": "Löschung (Art. 17)", + "typeRectification": "Berichtigung (Art. 16)", + "downloadExport": "Datenexport herunterladen", + "submit": "Anfrage stellen", + "confirmDeletion": "Wirklich löschen? Unwiderruflich!", + "chooseFirst": "Bitte zuerst eine Person wählen.", + "queued": "Anfrage eingereicht — Job", + "statusQueued": "(in Warteschlange). Die Bearbeitung erfolgt im Hintergrund." + } } } diff --git a/frontend/src/pages/ComplianceTab.tsx b/frontend/src/pages/ComplianceTab.tsx index 1731c6c..30a3152 100644 --- a/frontend/src/pages/ComplianceTab.tsx +++ b/frontend/src/pages/ComplianceTab.tsx @@ -13,9 +13,14 @@ import { type ComplianceIncident, type RetentionPolicyEntry, type IncidentCreate, + submitDsarRequest, + downloadDsgvoExport, + type DsarRequestResponse, + type DsarType, } from '../api/compliance'; +import { useUsers } from '../api/users'; -type SubTab = 'registry' | 'incidents' | 'retention'; +type SubTab = 'registry' | 'incidents' | 'retention' | 'dsar'; export function ComplianceTab() { const { t } = useTranslation(); @@ -25,6 +30,7 @@ export function ComplianceTab() { { key: 'registry', label: t('compliance.aiRegistry', 'AI-Register') }, { key: 'incidents', label: t('compliance.incidents', 'Vorfälle') }, { key: 'retention', label: t('compliance.retention', 'Aufbewahrung') }, + { key: 'dsar', label: t('compliance.dsar.tab', 'DSGVO-Anfragen') }, ]; return ( @@ -53,6 +59,7 @@ export function ComplianceTab() { {subTab === 'registry' && } {subTab === 'incidents' && } {subTab === 'retention' && } + {subTab === 'dsar' && } ); } @@ -461,3 +468,135 @@ function RetentionPanel() { ); } + +// ─── DSAR Panel (GDPR Art. 15/17/20) ─── + +const DSAR_TYPES: { value: DsarType; labelKey: string; fallback: string }[] = [ + { value: 'access', labelKey: 'compliance.dsar.typeAccess', fallback: 'Auskunft (Art. 15)' }, + { value: 'deletion', labelKey: 'compliance.dsar.typeDeletion', fallback: 'Löschung (Art. 17)' }, + { value: 'rectification', labelKey: 'compliance.dsar.typeRectification', fallback: 'Berichtigung (Art. 16)' }, +]; + +function DsarPanel() { + const { t } = useTranslation(); + const [userId, setUserId] = useState(''); + const [dsarType, setDsarType] = useState('access'); + const [confirmDelete, setConfirmDelete] = useState(false); + const [lastJob, setLastJob] = useState(null); + const [exportError, setExportError] = useState(false); + + const { data: usersData, isLoading: usersLoading } = useUsers(1, 200); + + const dsarMutation = useMutation({ + mutationFn: () => submitDsarRequest(userId, dsarType), + onSuccess: (resp) => { + setLastJob(resp); + setConfirmDelete(false); + }, + }); + + const selectedUser = usersData?.items.find((u) => u.id === userId); + + const handleExportClick = async () => { + setExportError(false); + try { + await downloadDsgvoExport(userId, selectedUser?.name ?? undefined); + } catch { + setExportError(true); + } + }; + + const handleSubmit = () => { + if (!userId) return; + if (dsarType === 'deletion' && !confirmDelete) { + setConfirmDelete(true); + return; + } + dsarMutation.mutate(); + }; + + return ( +
+

{t('compliance.dsar.description', 'DSGVO-Anfrage für eine Person auslösen: Auskunft (Art. 15), Löschung (Art. 17) oder Berichtigung (Art. 16). Der Vorgang wird als Hintergrund-Job durch den Worker ausgeführt.')}

+ +
+ + +
+ +
+ {t('compliance.dsar.requestType', 'Antragsart')} +
+ {DSAR_TYPES.map(({ value, labelKey, fallback }) => ( + + ))} +
+
+ +
+ {dsarType === 'access' && ( + + )} + + {!userId && ( + {t('compliance.dsar.chooseFirst', 'Bitte zuerst eine Person wählen.')} + )} +
+ + {dsarMutation.isPending &&

{t('common.saving', 'Wird gesendet...')}

} + {dsarMutation.isError &&

{t('common.errorOccurred', 'Fehler beim Senden')}

} + {exportError &&

{t('common.errorOccurred', 'Fehler beim Erstellen des Exports')}

} + + {lastJob && ( +
+ {t('compliance.dsar.queued', 'Anfrage eingereicht — Job')}{' '} + {lastJob.job_id}{' '} + {t('compliance.dsar.statusQueued', '(in Warteschlange). Die Bearbeitung erfolgt im Hintergrund.')} +
+ )} +
+ ); +}