ui-nyla/src/pages/GDPR.tsx
2026-04-11 19:44:52 +02:00

339 lines
12 KiB
TypeScript

/**
* GDPR Page
*
* Provides access to user data rights (export, portability, deletion).
*/
import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { FaDownload, FaFileExport, FaShieldAlt, FaSpinner, FaTrash } from 'react-icons/fa';
import api from '../api';
import { clearUserDataCache } from '../utils/userCache';
import styles from './GDPR.module.css';
import { useLanguage } from '../providers/language/LanguageContext';
type ConsentInfo = {
dataCollected?: Record<string, string>;
dataProcessing?: Record<string, string>;
userRights?: Record<string, string>;
contact?: Record<string, string>;
};
type ActionMessage = {
type: 'success' | 'error';
text: string;
};
const downloadJson = (data: unknown, fileName: string, mimeType = 'application/json') => {
const fileBlob = new Blob([JSON.stringify(data, null, 2)], { type: mimeType });
const fileUrl = URL.createObjectURL(fileBlob);
const link = document.createElement('a');
link.href = fileUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(fileUrl);
};
export const GDPRPage: React.FC = () => {
const { t } = useLanguage();
const contactEmail = 'p.motsch@poweron.swiss';
const [consentInfo, setConsentInfo] = useState<ConsentInfo | null>(null);
const [isLoadingConsent, setIsLoadingConsent] = useState(true);
const [consentError, setConsentError] = useState<string | null>(null);
const [isExporting, setIsExporting] = useState(false);
const [isPortabilityExporting, setIsPortabilityExporting] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [actionMessage, setActionMessage] = useState<ActionMessage | null>(null);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [deleteConfirmText, setDeleteConfirmText] = useState('');
const [isDeleted, setIsDeleted] = useState(false);
const isActionLocked = isDeleting || isDeleted;
useEffect(() => {
let isActive = true;
const loadConsentInfo = async () => {
setIsLoadingConsent(true);
setConsentError(null);
try {
const response = await api.get('/api/user/me/consent-info');
if (isActive) {
setConsentInfo(response.data as ConsentInfo);
}
} catch (error: any) {
console.error('Failed to load GDPR consent info:', error);
if (isActive) {
setConsentError(t('Einwilligungsinformationen konnten nicht geladen werden.'));
}
} finally {
if (isActive) {
setIsLoadingConsent(false);
}
}
};
loadConsentInfo();
return () => {
isActive = false;
};
}, [t]);
const handleDataExport = async () => {
if (isActionLocked) return;
setIsExporting(true);
setActionMessage(null);
try {
const response = await api.get('/api/user/me/data-export');
downloadJson(response.data, 'gdpr-data-export.json');
setActionMessage({ type: 'success', text: t('Datenexport heruntergeladen.') });
} catch (error: any) {
console.error('GDPR export failed:', error);
setActionMessage({ type: 'error', text: t('Datenexport fehlgeschlagen. Bitte erneut versuchen.') });
} finally {
setIsExporting(false);
}
};
const handlePortabilityExport = async () => {
if (isActionLocked) return;
setIsPortabilityExporting(true);
setActionMessage(null);
try {
const response = await api.get('/api/user/me/data-portability', {
headers: { Accept: 'application/ld+json' }
});
downloadJson(response.data, 'gdpr-data-portability.json', 'application/ld+json');
setActionMessage({ type: 'success', text: t('Portabler Export heruntergeladen.') });
} catch (error: any) {
console.error('GDPR portability export failed:', error);
setActionMessage({ type: 'error', text: t('Portabler Export fehlgeschlagen. Bitte erneut versuchen.') });
} finally {
setIsPortabilityExporting(false);
}
};
const handleDeleteAccount = async () => {
setActionMessage(null);
if (deleteConfirmText !== 'LOESCHEN') {
setActionMessage({ type: 'error', text: t('Bitte geben Sie LOESCHEN ein, um die Löschung zu bestätigen.') });
return;
}
setIsDeleting(true);
try {
await api.delete('/api/user/me/', { params: { confirmDeletion: true } });
localStorage.removeItem('authToken');
sessionStorage.removeItem('auth_authority');
clearUserDataCache();
setIsDeleted(true);
setActionMessage({ type: 'success', text: t('Konto gelöscht. Weiterleitung zur Anmeldung…') });
window.location.replace('/login');
} catch (error: any) {
console.error('GDPR deletion failed:', error);
setActionMessage({ type: 'error', text: t('Kontolöschung fehlgeschlagen. Bitte erneut versuchen.') });
} finally {
setIsDeleting(false);
}
};
return (
<div className={styles.gdpr}>
<header className={styles.header}>
<div>
<h1 className={styles.title}>
<FaShieldAlt className={styles.titleIcon} />
{t('DSGVO / Datenschutz')}
</h1>
<p className={styles.subtitle}>
{t('Verwalten Sie Ihre personenbezogenen Datenexporte und Kontolöschung.')}
</p>
</div>
<Link to="/settings" className={styles.backLink}>
{t('Zurück zu Einstellungen')}
</Link>
</header>
<main className={styles.content}>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('Ihre Datenrechte')}</h2>
<div className={styles.actions}>
<div className={styles.actionCard}>
<h3>{t('Zugriff (Artikel 15)')}</h3>
<p>{t('Einen vollständigen Export herunterladen von')}</p>
<button
className={styles.primaryButton}
onClick={handleDataExport}
disabled={isExporting || isActionLocked}
>
{isExporting ? (
<span className={styles.buttonSpinner}>
<FaSpinner />
{t('Export wird erstellt…')}
</span>
) : (
<>
<FaDownload />
{t('Daten exportieren')}
</>
)}
</button>
</div>
<div className={styles.actionCard}>
<h3>{t('Datenübertragbarkeit (Artikel 20)')}</h3>
<p>{t('Einen maschinenlesbaren JSON-LD-Export herunterladen')}</p>
<button
className={styles.secondaryButton}
onClick={handlePortabilityExport}
disabled={isPortabilityExporting || isActionLocked}
>
{isPortabilityExporting ? (
<span className={styles.buttonSpinner}>
<FaSpinner />
{t('Export wird erstellt…')}
</span>
) : (
<>
<FaFileExport />
{t('Portabler Datenexport')}
</>
)}
</button>
</div>
<div className={styles.actionCard}>
<h3>{t('Löschung (Artikel 17)')}</h3>
<p>{t('Ihr Konto dauerhaft löschen und')}</p>
{!showDeleteConfirm && (
<button
className={styles.dangerButton}
onClick={() => setShowDeleteConfirm(true)}
disabled={isActionLocked}
>
<FaTrash />
{t('Löschung starten')}
</button>
)}
{showDeleteConfirm && (
<div className={styles.deleteConfirm}>
<p className={styles.deleteWarning}>
{t('Diese Aktion ist unwiderruflich. Geben Sie {word} ein, um zu bestätigen.', {
word: 'LOESCHEN',
})}
</p>
<input
className={styles.deleteInput}
value={deleteConfirmText}
onChange={(event) => setDeleteConfirmText(event.target.value)}
placeholder="LOESCHEN"
disabled={isDeleting}
/>
<div className={styles.deleteActions}>
<button
className={styles.secondaryButton}
onClick={() => {
setShowDeleteConfirm(false);
setDeleteConfirmText('');
}}
disabled={isDeleting}
>
{t('Abbrechen')}
</button>
<button
className={styles.dangerButton}
onClick={handleDeleteAccount}
disabled={isDeleting || deleteConfirmText !== 'LOESCHEN'}
>
{isDeleting ? (
<span className={styles.buttonSpinner}>
<FaSpinner />
{t('Wird gelöscht…')}
</span>
) : (
<>
<FaTrash />
{t('Löschung bestätigen')}
</>
)}
</button>
</div>
</div>
)}
</div>
</div>
{actionMessage && (
<div
className={`${styles.message} ${
actionMessage.type === 'success' ? styles.successMessage : styles.errorMessage
}`}
>
{actionMessage.text}
</div>
)}
</section>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('Verarbeitungsinformationen')}</h2>
{isLoadingConsent && <p className={styles.mutedText}>{t('Lade Einwilligungsinformationen')}</p>}
{consentError && <p className={styles.errorText}>{consentError}</p>}
{!isLoadingConsent && !consentError && consentInfo && (
<div className={styles.infoGrid}>
<div className={styles.infoBlock}>
<h3>{t('Gesammelte Daten')}</h3>
<ul>
{Object.entries(consentInfo.dataCollected || {}).map(([key, value]) => (
<li key={key}>
<strong>{key}:</strong> {value}
</li>
))}
</ul>
</div>
<div className={styles.infoBlock}>
<h3>{t('Verarbeitung')}</h3>
<ul>
{Object.entries(consentInfo.dataProcessing || {}).map(([key, value]) => (
<li key={key}>
<strong>{key}:</strong> {value}
</li>
))}
</ul>
</div>
<div className={styles.infoBlock}>
<h3>{t('Ihre Rechte')}</h3>
<ul>
{Object.entries(consentInfo.userRights || {}).map(([key, value]) => (
<li key={key}>
<strong>{key}:</strong> {value}
</li>
))}
</ul>
</div>
<div className={styles.infoBlock}>
<h3>{t('Kontakt')}</h3>
<ul>
{Object.entries({
...(consentInfo.contact || {}),
email: contactEmail,
}).map(([key, value]) => (
<li key={key}>
<strong>{key}:</strong> {value}
</li>
))}
</ul>
</div>
</div>
)}
</section>
</main>
</div>
);
};
export default GDPRPage;