mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:21:09 +02:00
✨ 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:
parent
7d450aa2a8
commit
ab15c2367b
4 changed files with 306 additions and 9 deletions
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue