feat(auth): add GDPR self-service endpoints for user data

Add /api/v1/me/data endpoints for users to view, export, and delete
their own data without admin privileges (GDPR compliance).

Backend:
- New MeModule with MeController and MeService
- GET /api/v1/me/data - view own data summary
- GET /api/v1/me/data/export - download as JSON
- DELETE /api/v1/me/data - delete all own data

Frontend:
- New /settings/my-data page with full data overview
- Export button for JSON download
- DeleteConfirmationModal with email verification
- Link from settings page to my-data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-12 13:20:08 +01:00
parent 759b227355
commit 9881e84ee3
9 changed files with 928 additions and 13 deletions

View file

@ -0,0 +1,113 @@
/**
* My Data API Service
*
* Self-service GDPR endpoints for users to view, export, and delete their own data.
*/
import { browser } from '$app/environment';
import { createApiClient, type ApiResult } from '../base-client';
import { authStore } from '$lib/stores/auth.svelte';
// Re-export types from admin (same structure for user data)
export type {
UserDataSummary,
DeleteUserDataResponse,
ProjectDataSummary,
EntityCount,
UserInfo,
AuthDataSummary,
CreditsDataSummary,
} from './admin';
/**
* User data export with metadata
*/
export interface UserDataExport {
exportedAt: string;
exportVersion: string;
data: import('./admin').UserDataSummary;
}
// Get Auth API URL dynamically at runtime
function getAuthApiUrl(): string {
if (browser && typeof window !== 'undefined') {
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
if (injectedUrl) {
return `${injectedUrl}/api/v1`;
}
}
return 'http://localhost:3001/api/v1';
}
// Lazy-initialized client
let _client: ReturnType<typeof createApiClient> | null = null;
function getClient() {
if (!_client) {
_client = createApiClient(getAuthApiUrl());
}
return _client;
}
/**
* My Data service for self-service data management
*/
export const myDataService = {
/**
* Get the authenticated user's data summary
*/
async getMyData(): Promise<ApiResult<import('./admin').UserDataSummary>> {
return getClient().get<import('./admin').UserDataSummary>('/me/data');
},
/**
* Export user data as JSON file download
* Returns the full export object with metadata
*/
async exportMyData(): Promise<ApiResult<UserDataExport>> {
return getClient().get<UserDataExport>('/me/data/export');
},
/**
* Trigger browser download of user data
*/
async downloadMyData(): Promise<void> {
const baseUrl = getAuthApiUrl();
const token = await authStore.getAccessToken();
// Use fetch with blob response for file download
const response = await fetch(`${baseUrl}/me/data/export`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(`Export failed: ${response.status}`);
}
const blob = await response.blob();
const filename =
response.headers.get('Content-Disposition')?.match(/filename="(.+)"/)?.[1] ||
`meine-daten-${new Date().toISOString().split('T')[0]}.json`;
// Create download link
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
/**
* Delete all user data (GDPR right to be forgotten)
*/
async deleteMyData(): Promise<ApiResult<import('./admin').DeleteUserDataResponse>> {
return getClient().delete<import('./admin').DeleteUserDataResponse>('/me/data');
},
};

View file

@ -0,0 +1,254 @@
<script lang="ts">
import type { DeleteUserDataResponse } from '$lib/api/services/admin';
interface Props {
show: boolean;
userEmail: string;
deleting: boolean;
deleteResult: DeleteUserDataResponse | null;
deleteError: string | null;
onConfirm: () => void;
onClose: () => void;
}
let { show, userEmail, deleting, deleteResult, deleteError, onConfirm, onClose }: Props =
$props();
let confirmEmail = $state('');
function handleClose() {
confirmEmail = '';
onClose();
}
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget && !deleting && !deleteResult) {
handleClose();
}
}
// Reset confirmEmail when modal opens
$effect(() => {
if (!show) {
confirmEmail = '';
}
});
</script>
{#if show}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
onclick={handleBackdropClick}
>
<div class="bg-card rounded-xl shadow-xl max-w-md w-full" role="dialog" aria-modal="true">
{#if deleteResult}
<!-- Success State -->
<div class="p-6">
<div class="flex items-center gap-3 mb-4">
<div
class="h-10 w-10 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center"
>
<svg
class="h-5 w-5 text-green-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h3 class="text-lg font-semibold">Daten geloscht</h3>
</div>
<p class="text-sm text-muted-foreground mb-4">
Dein Konto und alle damit verbundenen Daten wurden geloscht. Insgesamt wurden
<strong>{deleteResult.totalDeleted}</strong> Eintrage entfernt.
</p>
<div class="space-y-2 mb-6 text-sm">
{#each deleteResult.deletedFromProjects as project}
<div class="flex items-center justify-between">
<span class="text-muted-foreground">{project.projectName}</span>
{#if project.success}
<span class="text-green-600">{project.deletedCount || 0} geloscht</span>
{:else}
<span class="text-yellow-600">Nicht erreichbar</span>
{/if}
</div>
{/each}
<div class="pt-2 border-t mt-2">
<div class="flex items-center justify-between">
<span class="text-muted-foreground">Sessions</span>
<span>{deleteResult.deletedFromAuth.sessions}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-muted-foreground">Verknupfte Accounts</span>
<span>{deleteResult.deletedFromAuth.accounts}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-muted-foreground">Credit-Transaktionen</span>
<span>{deleteResult.deletedFromAuth.credits}</span>
</div>
</div>
</div>
<p class="text-sm text-muted-foreground mb-4">
Du wirst automatisch ausgeloggt und zur Startseite weitergeleitet.
</p>
<button
onclick={handleClose}
class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
Schliessen
</button>
</div>
{:else}
<!-- Confirmation State -->
<div class="p-6">
<div class="flex items-center gap-3 mb-4">
<div
class="h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center"
>
<svg
class="h-5 w-5 text-red-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<h3 class="text-lg font-semibold text-red-600">Alle Daten loschen?</h3>
</div>
<div
class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4"
>
<p class="text-sm text-red-700 dark:text-red-300 font-medium mb-2">
Diese Aktion ist unwiderruflich!
</p>
<p class="text-sm text-red-600 dark:text-red-400">
Dein Konto und alle deine Daten werden dauerhaft geloscht. Du wirst ausgeloggt und
kannst dich nicht mehr anmelden.
</p>
</div>
<p class="text-sm text-muted-foreground mb-4">Folgende Daten werden geloscht:</p>
<ul class="text-sm text-muted-foreground mb-6 space-y-2">
<li class="flex items-center gap-2">
<svg class="h-4 w-4 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
<span>Alle Projektdaten (Chats, Todos, Termine, Kontakte, etc.)</span>
</li>
<li class="flex items-center gap-2">
<svg class="h-4 w-4 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
<span>Alle Sessions und Anmeldedaten</span>
</li>
<li class="flex items-center gap-2">
<svg class="h-4 w-4 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
<span>Credits und Transaktionshistorie</span>
</li>
<li class="flex items-center gap-2">
<svg class="h-4 w-4 text-red-500" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd"
/>
</svg>
<span>Dein Nutzerkonto</span>
</li>
</ul>
<div class="mb-4">
<label for="delete-confirm-email" class="block text-sm font-medium mb-2">
Gib zur Bestatigung deine Email-Adresse ein:
</label>
<input
id="delete-confirm-email"
type="email"
placeholder={userEmail}
bind:value={confirmEmail}
disabled={deleting}
class="w-full px-3 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-red-500/50 disabled:opacity-50"
/>
</div>
{#if deleteError}
<div
class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"
>
<p class="text-sm text-red-600 dark:text-red-400">{deleteError}</p>
</div>
{/if}
<div class="flex gap-3">
<button
onclick={handleClose}
class="flex-1 px-4 py-2 border rounded-lg hover:bg-muted transition-colors disabled:opacity-50"
disabled={deleting}
>
Abbrechen
</button>
<button
onclick={onConfirm}
disabled={deleting || confirmEmail !== userEmail}
class="flex-1 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
>
{#if deleting}
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span>Wird geloscht...</span>
{:else}
Endgultig loschen
{/if}
</button>
</div>
</div>
{/if}
</div>
</div>
{/if}

View file

@ -265,39 +265,69 @@
</div>
</Card>
<!-- Danger Zone -->
<!-- My Data & Danger Zone -->
<Card>
<div class="p-6 border-red-200 dark:border-red-800">
<div class="p-6">
<div class="flex items-center gap-3 mb-6">
<div
class="flex h-10 w-10 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400"
class="flex h-10 w-10 items-center justify-center rounded-full bg-purple-100 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<div>
<h2 class="text-lg font-semibold text-red-600 dark:text-red-400">Gefahrenzone</h2>
<p class="text-sm text-muted-foreground">Irreversible Aktionen</p>
<h2 class="text-lg font-semibold">Meine Daten (DSGVO)</h2>
<p class="text-sm text-muted-foreground">Datenschutz und Datenexport</p>
</div>
</div>
<div class="rounded-lg border border-red-200 dark:border-red-800 p-4">
<div class="flex items-center justify-between">
<div class="space-y-4">
<div class="flex items-center justify-between py-3 border-b border-border">
<div>
<p class="font-medium text-red-600 dark:text-red-400">Konto löschen</p>
<p class="font-medium">Daten ansehen & exportieren</p>
<p class="text-sm text-muted-foreground">
Das Löschen deines Kontos kann nicht rückgängig gemacht werden.
Sieh alle deine gespeicherten Daten ein und exportiere sie als JSON
</p>
</div>
<Button variant="destructive" disabled class="bg-red-600 hover:bg-red-700 text-white">
Konto löschen
</Button>
<a
href="/settings/my-data"
class="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
>
Meine Daten
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
</a>
</div>
<div
class="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/10 p-4"
>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-red-600 dark:text-red-400">Konto loschen</p>
<p class="text-sm text-muted-foreground">
Das Loschen deines Kontos kann nicht ruckgangig gemacht werden.
</p>
</div>
<a
href="/settings/my-data"
class="inline-flex items-center gap-2 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 transition-colors"
>
Verwalten
</a>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,391 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { Card, PageHeader } from '@manacore/shared-ui';
import StatCard from '$lib/components/admin/StatCard.svelte';
import ProjectDataCard from '$lib/components/admin/ProjectDataCard.svelte';
import DeleteConfirmationModal from '$lib/components/my-data/DeleteConfirmationModal.svelte';
import { myDataService, type UserDataSummary } from '$lib/api/services/my-data';
import type { DeleteUserDataResponse } from '$lib/api/services/admin';
import { authStore } from '$lib/stores/auth.svelte';
let userData = $state<UserDataSummary | null>(null);
let loading = $state(true);
let error = $state<string | null>(null);
let exporting = $state(false);
// Delete dialog state
let showDeleteDialog = $state(false);
let deleting = $state(false);
let deleteResult = $state<DeleteUserDataResponse | null>(null);
let deleteError = $state<string | null>(null);
async function loadMyData() {
loading = true;
error = null;
const result = await myDataService.getMyData();
if (result.error) {
error = result.error;
userData = null;
} else {
userData = result.data;
}
loading = false;
}
async function handleExport() {
exporting = true;
try {
await myDataService.downloadMyData();
} catch (e) {
console.error('Export failed:', e);
// Could show toast here
} finally {
exporting = false;
}
}
async function handleDelete() {
deleting = true;
deleteError = null;
const result = await myDataService.deleteMyData();
if (result.error) {
deleteError = result.error;
} else {
deleteResult = result.data;
}
deleting = false;
}
async function handleDeleteModalClose() {
if (deleteResult) {
// After successful deletion, sign out and redirect
await authStore.signOut();
goto('/');
} else {
showDeleteDialog = false;
deleteError = null;
}
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
onMount(() => {
loadMyData();
});
</script>
<svelte:head>
<title>Meine Daten - Einstellungen</title>
</svelte:head>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-4">
<a
href="/settings"
class="p-2 rounded-lg hover:bg-muted transition-colors"
aria-label="Zuruck zu Einstellungen"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 19l-7-7 7-7"
/>
</svg>
</a>
<div>
<h1 class="text-2xl font-bold">Meine Daten</h1>
<p class="text-muted-foreground">
Ubersicht uber alle deine gespeicherten Daten (GDPR/DSGVO)
</p>
</div>
</div>
{#if userData}
<button
onclick={handleExport}
disabled={exporting}
class="flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 transition-colors"
>
{#if exporting}
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{:else}
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
{/if}
<span>{exporting ? 'Exportiere...' : 'Daten exportieren'}</span>
</button>
{/if}
</div>
{#if loading}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{#each Array(4) as _}
<div class="rounded-lg border bg-card p-6 shadow-sm animate-pulse">
<div class="h-4 bg-muted rounded w-20 mb-2"></div>
<div class="h-8 bg-muted rounded w-16"></div>
</div>
{/each}
</div>
{:else if error}
<div
class="rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-900/20 p-6 text-center"
>
<p class="text-red-600 dark:text-red-400 mb-4">{error}</p>
<button
onclick={loadMyData}
class="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90"
>
Erneut versuchen
</button>
</div>
{:else if userData}
<!-- User Info Card -->
<Card>
<div class="p-6">
<div class="flex items-start gap-4">
<div class="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center">
<span class="text-2xl font-bold text-primary">
{(userData.user.name || userData.user.email)[0].toUpperCase()}
</span>
</div>
<div class="flex-1">
<h2 class="text-xl font-semibold">{userData.user.name || 'Kein Name'}</h2>
<p class="text-muted-foreground">{userData.user.email}</p>
<div class="flex items-center gap-4 mt-2">
<span
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium
{userData.user.role === 'admin'
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300'}"
>
{userData.user.role}
</span>
{#if userData.user.emailVerified}
<span class="text-xs text-green-600 flex items-center gap-1">
<svg class="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
/>
</svg>
Email verifiziert
</span>
{:else}
<span class="text-xs text-yellow-600 flex items-center gap-1">
<svg class="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
<path
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"
/>
</svg>
Email nicht verifiziert
</span>
{/if}
</div>
<p class="text-xs text-muted-foreground mt-2">
Registriert am {formatDate(userData.user.createdAt)}
</p>
</div>
</div>
</div>
</Card>
<!-- Stats Overview -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard title="Gesamt-Entitaten" value={userData.totals.totalEntities} icon="chart" />
<StatCard
title="Projekte mit Daten"
value="{userData.totals.projectsWithData} / {userData.projects.length}"
icon="activity"
/>
<StatCard title="Credits" value={userData.credits.balance} icon="chart" />
<StatCard title="Sessions" value={userData.auth.sessionsCount} icon="users" />
</div>
<!-- Auth & Credits Details -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Auth Data -->
<Card>
<div class="p-6">
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg
class="h-5 w-5 text-blue-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
Authentifizierung
</h3>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Aktive Sessions</span>
<span class="font-mono">{userData.auth.sessionsCount}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Verknupfte Accounts</span>
<span class="font-mono">{userData.auth.accountsCount}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">2FA aktiviert</span>
<span class={userData.auth.has2FA ? 'text-green-500' : 'text-muted-foreground'}>
{userData.auth.has2FA ? 'Ja' : 'Nein'}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Letzter Login</span>
<span class="text-sm">
{userData.auth.lastLoginAt ? formatDate(userData.auth.lastLoginAt) : '-'}
</span>
</div>
</div>
</div>
</Card>
<!-- Credits Data -->
<Card>
<div class="p-6">
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
<svg
class="h-5 w-5 text-yellow-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
Credits
</h3>
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Aktueller Stand</span>
<span class="font-mono font-bold text-lg">{userData.credits.balance}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Gesamt verdient</span>
<span class="font-mono text-green-600">+{userData.credits.totalEarned}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Gesamt ausgegeben</span>
<span class="font-mono text-red-500">-{userData.credits.totalSpent}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-muted-foreground">Transaktionen</span>
<span class="font-mono">{userData.credits.transactionsCount}</span>
</div>
</div>
</div>
</Card>
</div>
<!-- Project Data -->
<div>
<h3 class="text-lg font-semibold mb-4">Projektdaten</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each userData.projects as project}
<ProjectDataCard {project} />
{/each}
</div>
</div>
<!-- Danger Zone -->
<Card>
<div class="p-6 border-t-4 border-red-500">
<div class="flex items-center gap-3 mb-4">
<div
class="h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30 flex items-center justify-center"
>
<svg class="h-5 w-5 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-red-600">Gefahrenzone</h3>
<p class="text-sm text-muted-foreground">Diese Aktionen sind unwiderruflich</p>
</div>
</div>
<div
class="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/10 p-4"
>
<div class="flex items-center justify-between">
<div>
<p class="font-medium text-red-700 dark:text-red-400">Alle meine Daten loschen</p>
<p class="text-sm text-muted-foreground mt-1">
Loscht dein Konto und alle damit verbundenen Daten dauerhaft aus allen Projekten.
Diese Aktion kann nicht ruckgangig gemacht werden.
</p>
</div>
<button
onclick={() => (showDeleteDialog = true)}
class="ml-4 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors whitespace-nowrap"
>
Daten loschen
</button>
</div>
</div>
</div>
</Card>
{/if}
</div>
<!-- Delete Confirmation Modal -->
<DeleteConfirmationModal
show={showDeleteDialog}
userEmail={userData?.user.email || ''}
{deleting}
{deleteResult}
{deleteError}
onConfirm={handleDelete}
onClose={handleDeleteModalClose}
/>