mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
✨ 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:
parent
759b227355
commit
9881e84ee3
9 changed files with 928 additions and 13 deletions
113
apps/manacore/apps/web/src/lib/api/services/my-data.ts
Normal file
113
apps/manacore/apps/web/src/lib/api/services/my-data.ts
Normal 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');
|
||||
},
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
Loading…
Add table
Add a link
Reference in a new issue