feat(gdpr): add DSGVO improvements for self-service data page

- Add account deletion confirmation email
- Extend data export with sessions, security events, transactions
- Add DSGVO info banner with privacy policy link
- Add data retention periods section
- Add cookie info (no tracking cookies)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Till-JS 2026-02-13 13:43:23 +01:00
parent 7d450aa2a8
commit ab15c2367b
4 changed files with 306 additions and 9 deletions

View file

@ -338,6 +338,118 @@ export class UserDataService {
};
}
/**
* Get full export data including sessions, security events, and transactions
*/
async getFullExportData(userId: string) {
const summary = await this.getUserDataSummary(userId);
// Get additional details for export
const [sessions, securityEvents, transactions] = await Promise.all([
this.getSessionHistory(userId),
this.getSecurityEvents(userId),
this.getTransactionHistory(userId),
]);
return {
...summary,
exportedAt: new Date().toISOString(),
exportVersion: '2.0',
sessions: {
active: sessions.filter((s) => !s.revokedAt && new Date(s.expiresAt) > new Date()),
history: sessions,
},
securityEvents,
creditTransactions: transactions,
};
}
/**
* Get session history for a user
*/
private async getSessionHistory(userId: string) {
const db = this.getDatabase();
return db
.select({
id: schema.sessions.id,
createdAt: schema.sessions.createdAt,
expiresAt: schema.sessions.expiresAt,
lastActivityAt: schema.sessions.lastActivityAt,
ipAddress: schema.sessions.ipAddress,
userAgent: schema.sessions.userAgent,
deviceName: schema.sessions.deviceName,
revokedAt: schema.sessions.revokedAt,
})
.from(schema.sessions)
.where(eq(schema.sessions.userId, userId))
.orderBy(desc(schema.sessions.createdAt))
.limit(100);
}
/**
* Get security events for a user
*/
private async getSecurityEvents(userId: string) {
const db = this.getDatabase();
return db
.select({
id: schema.securityEvents.id,
eventType: schema.securityEvents.eventType,
ipAddress: schema.securityEvents.ipAddress,
userAgent: schema.securityEvents.userAgent,
metadata: schema.securityEvents.metadata,
createdAt: schema.securityEvents.createdAt,
})
.from(schema.securityEvents)
.where(eq(schema.securityEvents.userId, userId))
.orderBy(desc(schema.securityEvents.createdAt))
.limit(100);
}
/**
* Get transaction history for a user
*/
private async getTransactionHistory(userId: string) {
const db = this.getDatabase();
return db
.select({
id: schema.transactions.id,
type: schema.transactions.type,
status: schema.transactions.status,
amount: schema.transactions.amount,
balanceBefore: schema.transactions.balanceBefore,
balanceAfter: schema.transactions.balanceAfter,
appId: schema.transactions.appId,
description: schema.transactions.description,
createdAt: schema.transactions.createdAt,
completedAt: schema.transactions.completedAt,
})
.from(schema.transactions)
.where(eq(schema.transactions.userId, userId))
.orderBy(desc(schema.transactions.createdAt));
}
/**
* Get user data for email (before deletion)
*/
async getUserForEmail(userId: string) {
const db = this.getDatabase();
const user = await db
.select({
email: schema.users.email,
name: schema.users.name,
})
.from(schema.users)
.where(eq(schema.users.id, userId))
.limit(1);
return user[0] || null;
}
/**
* Query a single backend for user data
*/

View file

@ -224,6 +224,56 @@ export async function sendVerificationEmail(
});
}
/**
* Send account deletion confirmation email
*/
export async function sendAccountDeletionEmail(email: string, userName?: string): Promise<boolean> {
const name = userName || email.split('@')[0];
return sendEmail({
to: email,
subject: 'Dein ManaCore-Konto wurde gelöscht',
html: `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #2563eb; margin: 0;">ManaCore</h1>
</div>
<p>Hallo ${name},</p>
<p>dein ManaCore-Konto und alle damit verbundenen Daten wurden erfolgreich gelöscht.</p>
<div style="background-color: #f8f9fa; border-radius: 8px; padding: 20px; margin: 20px 0;">
<p style="margin: 0 0 10px 0; font-weight: 600;">Folgende Daten wurden entfernt:</p>
<ul style="margin: 0; padding-left: 20px; color: #666;">
<li>Benutzerprofil und Anmeldedaten</li>
<li>Alle Sessions und verknüpften Accounts</li>
<li>Credits und Transaktionshistorie</li>
<li>Daten in allen verbundenen Apps</li>
</ul>
</div>
<p style="color: #666;">Diese Aktion ist unwiderruflich. Falls du ManaCore erneut nutzen möchtest, kannst du jederzeit ein neues Konto erstellen.</p>
<p style="color: #666;">Bei Fragen erreichst du uns unter <a href="mailto:support@mana.how" style="color: #2563eb;">support@mana.how</a>.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="color: #999; font-size: 12px; text-align: center;">
Diese E-Mail wurde automatisch von ManaCore gesendet.
</p>
</body>
</html>
`,
});
}
/**
* Send welcome/verification email
*/

View file

@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { UserDataService } from '../admin/user-data.service';
import type { UserDataSummary, DeleteUserDataResponse } from '../admin/dto/user-data.dto';
import { sendAccountDeletionEmail } from '../email/email.service';
/**
* Self-service data management for authenticated users.
@ -22,24 +23,38 @@ export class MeService {
/**
* Export the authenticated user's data as a complete JSON object
* Includes sessions, security events, and credit transactions for GDPR compliance
*/
async exportMyData(userId: string): Promise<UserDataExport> {
async exportMyData(userId: string): Promise<FullUserDataExport> {
this.logger.log(`User ${userId} exporting own data`);
const summary = await this.userDataService.getUserDataSummary(userId);
return {
exportedAt: new Date().toISOString(),
exportVersion: '1.0',
data: summary,
};
return this.userDataService.getFullExportData(userId);
}
/**
* Delete all data for the authenticated user (GDPR right to be forgotten)
* Sends confirmation email after successful deletion
*/
async deleteMyData(userId: string): Promise<DeleteUserDataResponse> {
this.logger.log(`User ${userId} requesting deletion of own data`);
return this.userDataService.deleteUserData(userId);
// Get user data BEFORE deletion for sending confirmation email
const user = await this.userDataService.getUserForEmail(userId);
// Perform deletion
const result = await this.userDataService.deleteUserData(userId);
// Send confirmation email if deletion was successful
if (result.success && user?.email) {
try {
await sendAccountDeletionEmail(user.email, user.name || undefined);
this.logger.log(`Account deletion confirmation email sent to ${user.email}`);
} catch (error) {
// Log but don't fail the deletion if email fails
this.logger.error(`Failed to send deletion confirmation email to ${user.email}`, error);
}
}
return result;
}
}
@ -48,3 +63,47 @@ export interface UserDataExport {
exportVersion: string;
data: UserDataSummary;
}
export interface SessionExport {
id: string;
createdAt: Date;
expiresAt: Date;
lastActivityAt: Date | null;
ipAddress: string | null;
userAgent: string | null;
deviceName: string | null;
revokedAt: Date | null;
}
export interface SecurityEventExport {
id: string;
eventType: string;
ipAddress: string | null;
userAgent: string | null;
metadata: unknown;
createdAt: Date;
}
export interface TransactionExport {
id: string;
type: string;
status: string;
amount: number;
balanceBefore: number;
balanceAfter: number;
appId: string;
description: string;
createdAt: Date;
completedAt: Date | null;
}
export interface FullUserDataExport extends UserDataSummary {
exportedAt: string;
exportVersion: string;
sessions: {
active: SessionExport[];
history: SessionExport[];
};
securityEvents: SecurityEventExport[];
creditTransactions: TransactionExport[];
}