Impressum, Datenschutzerklärung und DSGVO-Löschfunktion

- /impressum (§ 5 DDG) und /datenschutz als öffentliche Seiten im
  Brand-Look; Betreiber-Angaben (Name, Anschrift, E-Mail, Zusatz)
  liegen als Settings und sind bewusst Owner-only — Team-Mitglieder
  mit settings-Scope kommen an die private Anschrift nicht heran
- Die Datenschutzerklärung beschreibt die tatsächliche Verarbeitung
  dieser Instanz: Server-Logs, Session-Cookies (einwilligungsfrei nach
  § 25 Abs. 2 TDDDG, kein Tracking), Discord-Login, Level, Wünsche,
  Geburtstage, Playtester/Alpha-Keys, Bewerbungen, Tickets/Modmail,
  Erinnerungen, Moderation und Audit-Log
- DSGVO Art. 17: DELETE /api/profile löscht XP, Geburtstag,
  Playtester-Eintrag, Abstimmungen, Erinnerungen, Rollen-Sicherung,
  Giveaway-Teilnahmen und gibt den Alpha-Key wieder frei; Button im
  Profil unter „Meine Daten", Löschung landet im Audit-Log
- Footer verlinkt beide Rechtstexte, neues GET /api/legal (öffentlich)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 13:19:33 +02:00
co-authored by Claude Fable 5
parent 676fc45bb1
commit 8853bce2ec
10 changed files with 464 additions and 4 deletions
+23
View File
@@ -493,6 +493,29 @@ export const setBirthday = (userId, username, day, month) => upsertBirthday.run(
export const deleteBirthday = (userId) => deleteBirthdayStmt.run(userId).changes > 0;
export const birthdaysToday = (day, month) => birthdaysTodayStmt.all(day, month);
/**
* DSGVO Art. 17: alle Selbstauskunfts-Daten eines Users löschen.
* Bewusst NICHT gelöscht: Verwarnungen (berechtigtes Interesse an Moderation)
* und Inhalte, die im Discord stehen (Devlogs, Galerie) — die gehören dort gelöscht.
* @returns {Record<string, number>} gelöschte Zeilen pro Bereich
*/
export function deleteUserData(userId) {
const del = (sql) => db.prepare(sql).run(userId).changes;
const result = {
level: del('DELETE FROM levels WHERE user_id = ?'),
geburtstag: del('DELETE FROM birthdays WHERE user_id = ?'),
playtester: del('DELETE FROM playtesters WHERE user_id = ?'),
votes: del('DELETE FROM wish_votes WHERE user_id = ?'),
erinnerungen: del('DELETE FROM reminders WHERE user_id = ?'),
rollen_backup: del('DELETE FROM sticky_roles WHERE user_id = ?'),
giveaways: del('DELETE FROM giveaway_entries WHERE user_id = ?'),
alpha_key: db.prepare(
`UPDATE alpha_keys SET assigned_to = NULL, assigned_at = NULL WHERE assigned_to = ?`
).run(userId).changes,
};
return result;
}
// Audit-Log: wer (Owner/Team) hat wann was im Webinterface geändert
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (
+10
View File
@@ -161,6 +161,16 @@ export function levelRewards() {
.sort((a, b) => a.level - b.level);
}
/** Betreiber-Angaben fürs Impressum (§ 5 DDG) — leer = Seite zeigt Hinweis */
export function legalInfo() {
return {
name: getSetting('legal_name') ?? '',
address: getSetting('legal_address') ?? '',
email: getSetting('legal_email') ?? '',
extra: getSetting('legal_extra') ?? '',
};
}
/** Member-Gate: Web-Login nur für Discord-Member (Default: an, wenn Guild gesetzt) */
export function memberGateEnabled() {
return getSetting('member_gate_enabled') !== '0';
+29 -2
View File
@@ -14,7 +14,7 @@ import {
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
saveTemplate, listTemplates, deleteTemplate,
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
logAudit, listAudit, playerHistory,
logAudit, listAudit, playerHistory, deleteUserData,
} from '../db.js';
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
@@ -23,7 +23,7 @@ import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-men
import { computeNextRun } from './scheduled-posts.js';
import { config } from '../config.js';
import { removeDevlog } from '../bot/devlog-archive.js';
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken, votingChannelId, discordGuildId, discordInviteUrl } from '../runtime-settings.js';
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken, votingChannelId, discordGuildId, discordInviteUrl, legalInfo } from '../runtime-settings.js';
import { xpForLevel } from '../bot/levels.js';
import { getMilestones } from '../gitea-api.js';
import { getSessionUser, isAdmin, isGuildMember } from './auth.js';
@@ -353,6 +353,10 @@ ${rssItems}
discord_invite_url: getSetting('discord_invite_url') ?? '',
birthday_channel_id: getSetting('birthday_channel_id') ?? '',
birthday_role_id: getSetting('birthday_role_id') ?? '',
legal_name: getSetting('legal_name') ?? '',
legal_address: getSetting('legal_address') ?? '',
legal_email: getSetting('legal_email') ?? '',
legal_extra: getSetting('legal_extra') ?? '',
};
}
@@ -390,6 +394,7 @@ ${rssItems}
for (const key of [
'gitea_api_token', 'twitch_client_id', 'twitch_client_secret',
'discord_guild_id', 'bot_name', 'bot_description', 'public_url', 'gitea_url',
'legal_name', 'legal_address', 'legal_email', 'legal_extra',
]) delete body[key];
}
@@ -545,6 +550,10 @@ ${rssItems}
setSetting(key, String(body[key]).trim());
}
}
// Impressums-Angaben (Owner-only, oben bereits gefiltert)
for (const key of ['legal_name', 'legal_address', 'legal_email', 'legal_extra']) {
if (body[key] !== undefined) setSetting(key, String(body[key]).trim().slice(0, 500));
}
// URLs: müssen mit http(s) beginnen
for (const key of ['public_url', 'gitea_url']) {
if (body[key] === undefined) continue;
@@ -1111,6 +1120,24 @@ ${rssItems}
app.get('/api/invite', async () => ({ invite: discordInviteUrl() }));
// Betreiber-Angaben für Impressum/Datenschutz — öffentlich (müssen es sein)
app.get('/api/legal', async () => ({
legal: legalInfo(),
brand: brandName(),
publicUrl: publicUrl(),
guildConfigured: Boolean(discordGuildId()),
}));
// DSGVO Art. 17: eigene Daten löschen
app.delete('/api/profile', async (request, reply) => {
if (await requireMember(request, reply)) return;
const user = getSessionUser(request);
const deleted = deleteUserData(user.id);
request.log.info(`Datenlöschung für ${user.id}: ${JSON.stringify(deleted)}`);
logAudit(user, 'daten gelöscht (dsgvo)', user.username);
return { ok: true, deleted };
});
app.get('/api/profile', async (request, reply) => {
if (await requireMember(request, reply)) return;
const user = getSessionUser(request);