frontend_nyla/src/pages/GDPR.tsx
2026-04-09 00:11:35 +02:00

337 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('Consent information could not be loaded.');
}
} finally {
if (isActive) {
setIsLoadingConsent(false);
}
}
};
loadConsentInfo();
return () => {
isActive = false;
};
}, []);
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: 'Data export downloaded.' });
} catch (error: any) {
console.error('GDPR export failed:', error);
setActionMessage({ type: 'error', text: 'Data export failed. Please try again.' });
} 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: 'Portable export downloaded.' });
} catch (error: any) {
console.error('GDPR portability export failed:', error);
setActionMessage({ type: 'error', text: 'Portable export failed. Please try again.' });
} finally {
setIsPortabilityExporting(false);
}
};
const handleDeleteAccount = async () => {
setActionMessage(null);
if (deleteConfirmText !== 'LOESCHEN') {
setActionMessage({ type: 'error', text: 'Please type LOESCHEN to confirm deletion.' });
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: 'Account deleted. Redirecting to login...' });
window.location.replace('/login');
} catch (error: any) {
console.error('GDPR deletion failed:', error);
setActionMessage({ type: 'error', text: 'Account deletion failed. Please try again.' });
} finally {
setIsDeleting(false);
}
};
return (
<div className={styles.gdpr}>
<header className={styles.header}>
<div>
<h1 className={styles.title}>
<FaShieldAlt className={styles.titleIcon} />
GDPR / Privacy
</h1>
<p className={styles.subtitle}>
Manage your personal data exports and account deletion.
</p>
</div>
<Link to="/settings" className={styles.backLink}>
Back to Settings
</Link>
</header>
<main className={styles.content}>
<section className={styles.section}>
<h2 className={styles.sectionTitle}>{t('gDPR.yourDataRights')}</h2>
<div className={styles.actions}>
<div className={styles.actionCard}>
<h3>{t('gDPR.accessArticle15')}</h3>
<p>{t('gDPR.downloadAFullExportOf')}</p>
<button
className={styles.primaryButton}
onClick={handleDataExport}
disabled={isExporting || isActionLocked}
>
{isExporting ? (
<span className={styles.buttonSpinner}>
<FaSpinner />
Exporting...
</span>
) : (
<>
<FaDownload />
Export data
</>
)}
</button>
</div>
<div className={styles.actionCard}>
<h3>{t('gDPR.portabilityArticle20')}</h3>
<p>{t('gDPR.downloadAMachinereadableJsonldExport')}</p>
<button
className={styles.secondaryButton}
onClick={handlePortabilityExport}
disabled={isPortabilityExporting || isActionLocked}
>
{isPortabilityExporting ? (
<span className={styles.buttonSpinner}>
<FaSpinner />
Exporting...
</span>
) : (
<>
<FaFileExport />
Export portable data
</>
)}
</button>
</div>
<div className={styles.actionCard}>
<h3>{t('gDPR.erasureArticle17')}</h3>
<p>{t('gDPR.permanentlyDeleteYourAccountAnd')}</p>
{!showDeleteConfirm && (
<button
className={styles.dangerButton}
onClick={() => setShowDeleteConfirm(true)}
disabled={isActionLocked}
>
<FaTrash />
Start deletion
</button>
)}
{showDeleteConfirm && (
<div className={styles.deleteConfirm}>
<p className={styles.deleteWarning}>
This action is irreversible. Type <strong>LOESCHEN</strong> to confirm.
</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}
>
Cancel
</button>
<button
className={styles.dangerButton}
onClick={handleDeleteAccount}
disabled={isDeleting || deleteConfirmText !== 'LOESCHEN'}
>
{isDeleting ? (
<span className={styles.buttonSpinner}>
<FaSpinner />
Deleting...
</span>
) : (
<>
<FaTrash />
Confirm deletion
</>
)}
</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('gDPR.processingInformation')}</h2>
{isLoadingConsent && <p className={styles.mutedText}>{t('gDPR.loadingConsentInfo')}</p>}
{consentError && <p className={styles.errorText}>{consentError}</p>}
{!isLoadingConsent && !consentError && consentInfo && (
<div className={styles.infoGrid}>
<div className={styles.infoBlock}>
<h3>{t('gDPR.dataCollected')}</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('gDPR.processing')}</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('gDPR.yourRights')}</h3>
<ul>
{Object.entries(consentInfo.userRights || {}).map(([key, value]) => (
<li key={key}>
<strong>{key}:</strong> {value}
</li>
))}
</ul>
</div>
<div className={styles.infoBlock}>
<h3>Contact</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;