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:
@@ -65,7 +65,8 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St
|
|||||||
| `/events` | öffentlich | Discord-Events (Playtests, Streams …) |
|
| `/events` | öffentlich | Discord-Events (Playtests, Streams …) |
|
||||||
| `/changelog` | öffentlich | Alle Releases mit Notes, Tag- und Pre-Release-Chips |
|
| `/changelog` | öffentlich | Alle Releases mit Notes, Tag- und Pre-Release-Chips |
|
||||||
| `/feed.xml` | öffentlich | RSS-Feed der Devlogs |
|
| `/feed.xml` | öffentlich | RSS-Feed der Devlogs |
|
||||||
| `/profil` | Member | Eigenes Profil: Rang, XP, Rollen (togglebar), Alpha-Key |
|
| `/profil` | Member | Eigenes Profil: Rang, XP, Rollen (togglebar), Alpha-Key, DSGVO-Löschung |
|
||||||
|
| `/impressum` · `/datenschutz` | öffentlich | Rechtstexte — Betreiber-Angaben aus dem Setup (Owner-only) |
|
||||||
| `/` | öffentlich | Startseite: Hero, Live-Zahlen (Member/Server/Spieler), Bereichs-Kacheln, neuestes Devlog |
|
| `/` | öffentlich | Startseite: Hero, Live-Zahlen (Member/Server/Spieler), Bereichs-Kacheln, neuestes Devlog |
|
||||||
| `/server` | öffentlich | Live-Status aller Game-Server: Logo, Spieler-Balken, 24h-Verlauf, Uptime & Peak, Copy-Adresse |
|
| `/server` | öffentlich | Live-Status aller Game-Server: Logo, Spieler-Balken, 24h-Verlauf, Uptime & Peak, Copy-Adresse |
|
||||||
| `/commits` | nur Admin | Archivierte Commits aller Repos (SHA → Gitea-Link) |
|
| `/commits` | nur Admin | Archivierte Commits aller Repos (SHA → Gitea-Link) |
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import Profil from './pages/Profil.jsx';
|
|||||||
import Server from './pages/Server.jsx';
|
import Server from './pages/Server.jsx';
|
||||||
import Home from './pages/Home.jsx';
|
import Home from './pages/Home.jsx';
|
||||||
import DevlogDetail from './pages/DevlogDetail.jsx';
|
import DevlogDetail from './pages/DevlogDetail.jsx';
|
||||||
|
import Impressum from './pages/Impressum.jsx';
|
||||||
|
import Datenschutz from './pages/Datenschutz.jsx';
|
||||||
|
|
||||||
const COMMUNITY_LINKS = [
|
const COMMUNITY_LINKS = [
|
||||||
['/galerie', 'Galerie'],
|
['/galerie', 'Galerie'],
|
||||||
@@ -196,6 +198,8 @@ export default function App() {
|
|||||||
<Route path="/events" element={<Events />} />
|
<Route path="/events" element={<Events />} />
|
||||||
<Route path="/changelog" element={<Changelog />} />
|
<Route path="/changelog" element={<Changelog />} />
|
||||||
<Route path="/profil" element={<Profil me={me} />} />
|
<Route path="/profil" element={<Profil me={me} />} />
|
||||||
|
<Route path="/impressum" element={<Impressum />} />
|
||||||
|
<Route path="/datenschutz" element={<Datenschutz />} />
|
||||||
<Route path="/commits" element={<Commits me={me} />} />
|
<Route path="/commits" element={<Commits me={me} />} />
|
||||||
<Route path="/settings" element={<Settings me={me} />} />
|
<Route path="/settings" element={<Settings me={me} />} />
|
||||||
<Route path="*" element={<Navigate to="/devlogs" replace />} />
|
<Route path="*" element={<Navigate to="/devlogs" replace />} />
|
||||||
@@ -208,6 +212,9 @@ export default function App() {
|
|||||||
<a href="https://git.d4rkst3r.de/D4rkst3r" target="_blank" rel="noreferrer">
|
<a href="https://git.d4rkst3r.de/D4rkst3r" target="_blank" rel="noreferrer">
|
||||||
git.d4rkst3r.de
|
git.d4rkst3r.de
|
||||||
</a>
|
</a>
|
||||||
|
<span className="footer-sep">//</span>
|
||||||
|
<NavLink to="/impressum">Impressum</NavLink>
|
||||||
|
<NavLink to="/datenschutz">Datenschutz</NavLink>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
// Datenschutzerklärung — beschreibt die tatsächliche Verarbeitung dieser Instanz
|
||||||
|
// (Discord-Login, Community-Funktionen, Moderation). Betreiber-Angaben aus den Settings.
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { apiGet } from '../api.js';
|
||||||
|
|
||||||
|
export default function Datenschutz() {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/api/legal').then(setData).catch(() => setData({ legal: {} }));
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const legal = data?.legal ?? {};
|
||||||
|
const brand = data?.brand ?? 'Wir';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="page-header">
|
||||||
|
<div className="page-bg-text">LEGAL</div>
|
||||||
|
<div className="page-header-grid" aria-hidden="true" />
|
||||||
|
<div className="page-header-content">
|
||||||
|
<div className="page-tag">// DSGVO</div>
|
||||||
|
<h1 className="page-title">Datenschutz</h1>
|
||||||
|
<p className="page-subtitle">
|
||||||
|
Welche Daten hier verarbeitet werden, warum — und wie du sie wieder loswirst.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="content legal-text">
|
||||||
|
<h2 className="settings-title">// 1. Verantwortlicher</h2>
|
||||||
|
<p>
|
||||||
|
Verantwortlich für die Datenverarbeitung auf dieser Webseite im Sinne der
|
||||||
|
Datenschutz-Grundverordnung (DSGVO) ist:<br /><br />
|
||||||
|
{legal.name || '—'}<br />
|
||||||
|
{(legal.address || '').split('\n').map((line, i) => (
|
||||||
|
<span key={i}>{line}<br /></span>
|
||||||
|
))}
|
||||||
|
{legal.email && <>E-Mail: <a href={`mailto:${legal.email}`}>{legal.email}</a></>}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 2. Aufruf der Webseite (Server-Logs)</h2>
|
||||||
|
<p>
|
||||||
|
Beim Aufruf dieser Seite verarbeitet der Server technische Zugriffsdaten:
|
||||||
|
IP-Adresse, Zeitpunkt der Anfrage, aufgerufene Adresse, HTTP-Statuscode und
|
||||||
|
die Browserkennung. Diese Daten sind technisch erforderlich, um die Seite
|
||||||
|
auszuliefern und Störungen sowie Angriffe erkennen zu können. Rechtsgrundlage
|
||||||
|
ist Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse am sicheren und
|
||||||
|
stabilen Betrieb). Die Logdaten werden nicht mit anderen Datenquellen
|
||||||
|
zusammengeführt und nach kurzer Zeit im Rahmen der üblichen Rotation gelöscht.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 3. Cookies</h2>
|
||||||
|
<p>
|
||||||
|
Diese Seite verwendet ausschließlich technisch notwendige Cookies. Konkret
|
||||||
|
wird nach einem Login ein signiertes Sitzungs-Cookie gesetzt, damit du
|
||||||
|
eingeloggt bleibst; ein weiteres kurzlebiges Cookie sichert den Login-Vorgang
|
||||||
|
gegen Missbrauch ab (CSRF-Schutz). Diese Cookies sind für den von dir
|
||||||
|
gewünschten Dienst unbedingt erforderlich und daher nach § 25 Abs. 2 Nr. 2
|
||||||
|
TDDDG einwilligungsfrei. Es findet <strong>kein Tracking</strong>, keine
|
||||||
|
Reichweitenmessung und keine Werbung statt. Die verwendeten Schriftarten
|
||||||
|
werden lokal von diesem Server ausgeliefert — es entsteht dabei keine
|
||||||
|
Verbindung zu Dritten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 4. Login mit Discord</h2>
|
||||||
|
<p>
|
||||||
|
Für den Mitglieder-Bereich kannst du dich freiwillig mit deinem
|
||||||
|
Discord-Konto anmelden (OAuth2, Berechtigung <code>identify</code>). Dabei
|
||||||
|
erhalten wir von Discord deine Benutzer-ID, deinen Benutzernamen und dein
|
||||||
|
Profilbild. Diese Angaben werden in dem oben genannten Sitzungs-Cookie
|
||||||
|
gespeichert; ein Zugriffstoken speichern wir nicht. Zusätzlich prüfen wir
|
||||||
|
beim Login, ob du Mitglied unseres Discord-Servers bist, da der Zugang
|
||||||
|
Mitgliedern vorbehalten ist. Rechtsgrundlage ist Art. 6 Abs. 1 lit. a DSGVO
|
||||||
|
(Einwilligung durch die Anmeldung) bzw. lit. b DSGVO für die Bereitstellung
|
||||||
|
des Mitglieder-Bereichs. Du kannst dich jederzeit über den Logout abmelden;
|
||||||
|
das Cookie wird dann gelöscht.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Anbieter ist Discord Netherlands BV bzw. Discord Inc., 444 De Haro Street,
|
||||||
|
San Francisco, CA 94107, USA. Beim Login und beim Anzeigen von Profilbildern
|
||||||
|
(<code>cdn.discordapp.com</code>) werden Daten an Discord übertragen und
|
||||||
|
gegebenenfalls in den USA verarbeitet. Details findest du in der
|
||||||
|
Datenschutzerklärung von Discord.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 5. Community-Funktionen</h2>
|
||||||
|
<p>
|
||||||
|
{brand} betreibt einen eigenen Discord-Bot, der zur Verwaltung der Community
|
||||||
|
Daten speichert. Verarbeitet werden — je nachdem, welche Funktionen du nutzt:
|
||||||
|
</p>
|
||||||
|
<ul className="legal-list">
|
||||||
|
<li>
|
||||||
|
<strong>Level-System:</strong> Benutzer-ID, Anzeigename und gesammelte
|
||||||
|
Erfahrungspunkte, um eine Bestenliste anzuzeigen.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Feature-Wünsche & Abstimmungen:</strong> dein Anzeigename und der
|
||||||
|
Text deines Vorschlags, bei Abstimmungen deine Benutzer-ID (damit jede
|
||||||
|
Person nur einmal abstimmt).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Geburtstage:</strong> Tag und Monat deines Geburtstags — nur wenn du
|
||||||
|
sie selbst per Befehl einträgst, jederzeit selbst wieder entfernbar.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Playtester & Zugangsschlüssel:</strong> Benutzer-ID, Anzeigename und
|
||||||
|
Zeitpunkt der Anmeldung sowie ein dir zugeteilter Alpha-Key.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Bewerbungen:</strong> deine Antworten auf die Formularfragen sowie das
|
||||||
|
Ergebnis der Prüfung.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Support & Kontakt:</strong> Inhalte von Tickets und Direktnachrichten
|
||||||
|
an den Bot, damit das Team antworten kann; beim Schließen eines Tickets
|
||||||
|
wird auf Wunsch ein Gesprächsverlauf erstellt.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Erinnerungen:</strong> Text und Zeitpunkt, bis die Erinnerung
|
||||||
|
zugestellt wurde.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Veröffentlichte Beiträge:</strong> Entwicklungsberichte und
|
||||||
|
Community-Screenshots werden mit Anzeigenamen archiviert und auf dieser
|
||||||
|
Webseite dargestellt.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Rechtsgrundlage ist Art. 6 Abs. 1 lit. a DSGVO (Einwilligung) für alle
|
||||||
|
freiwilligen Angaben und Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse
|
||||||
|
am Betrieb einer funktionierenden Community) für den technischen Betrieb.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 6. Moderation und Protokolle</h2>
|
||||||
|
<p>
|
||||||
|
Zur Durchsetzung der Serverregeln werden Verwarnungen mit Benutzer-ID, Grund
|
||||||
|
und Zeitpunkt gespeichert. Änderungen im Administrationsbereich der Webseite
|
||||||
|
werden in einem Protokoll festgehalten (wer wann welche Einstellung geändert
|
||||||
|
hat), um Missbrauch nachvollziehen zu können. Rechtsgrundlage ist Art. 6
|
||||||
|
Abs. 1 lit. f DSGVO.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 7. Speicherdauer</h2>
|
||||||
|
<p>
|
||||||
|
Wir speichern personenbezogene Daten so lange, wie du Mitglied der Community
|
||||||
|
bist oder die jeweilige Funktion nutzt. Verlässt du den Discord-Server, kannst
|
||||||
|
du die Löschung deiner Daten verlangen (siehe Punkt 9). Sitzungs-Cookies
|
||||||
|
laufen spätestens nach sieben Tagen ab. Von der Datenbank werden verschlüsselte
|
||||||
|
Sicherungskopien erstellt, die nach spätestens 14 Tagen automatisch gelöscht
|
||||||
|
werden.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 8. Weitergabe an Dritte</h2>
|
||||||
|
<p>
|
||||||
|
Eine Weitergabe deiner Daten zu Werbezwecken findet nicht statt. Ein Verkauf
|
||||||
|
von Daten erfolgt nicht. Übermittlungen erfolgen nur an Discord, soweit dies
|
||||||
|
für die Nutzung des Discord-Servers und des Logins technisch notwendig ist.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 9. Deine Rechte</h2>
|
||||||
|
<p>
|
||||||
|
Du hast jederzeit das Recht auf Auskunft über die zu deiner Person
|
||||||
|
gespeicherten Daten (Art. 15 DSGVO), auf Berichtigung (Art. 16), auf Löschung
|
||||||
|
(Art. 17), auf Einschränkung der Verarbeitung (Art. 18), auf
|
||||||
|
Datenübertragbarkeit (Art. 20) sowie auf Widerspruch gegen die Verarbeitung
|
||||||
|
(Art. 21). Eine erteilte Einwilligung kannst du jederzeit mit Wirkung für die
|
||||||
|
Zukunft widerrufen.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Selbst löschen:</strong> Eingeloggte Mitglieder können ihre gespeicherten
|
||||||
|
Daten direkt auf der Profilseite löschen — dort findest du unten den Bereich
|
||||||
|
„Meine Daten löschen". Beiträge, die im Discord stehen (etwa Screenshots oder
|
||||||
|
Nachrichten), löschst du bitte dort; melde dich sonst gerne bei uns.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Für Anfragen genügt eine formlose Nachricht an
|
||||||
|
{' '}{legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : 'die im Impressum genannte Adresse'}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 10. Beschwerderecht</h2>
|
||||||
|
<p>
|
||||||
|
Wenn du der Ansicht bist, dass die Verarbeitung deiner Daten gegen die DSGVO
|
||||||
|
verstößt, kannst du dich bei einer Datenschutz-Aufsichtsbehörde beschweren —
|
||||||
|
etwa bei der für deinen Wohnsitz zuständigen Landesdatenschutzbehörde.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// 11. Änderungen</h2>
|
||||||
|
<p>
|
||||||
|
Wir passen diese Datenschutzerklärung an, wenn sich die angebotenen Funktionen
|
||||||
|
ändern. Es gilt jeweils die hier veröffentlichte Fassung.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Impressum nach § 5 DDG — die Betreiber-Angaben kommen aus den Settings
|
||||||
|
// (System-Tab, nur Owner), damit hier nichts hartkodiert im Repo landet.
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { apiGet } from '../api.js';
|
||||||
|
|
||||||
|
export default function Impressum() {
|
||||||
|
const [data, setData] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
apiGet('/api/legal').then(setData).catch(() => setData({ legal: {} }));
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const legal = data?.legal ?? {};
|
||||||
|
const incomplete = !legal.name || !legal.address || !legal.email;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="page-header">
|
||||||
|
<div className="page-bg-text">LEGAL</div>
|
||||||
|
<div className="page-header-grid" aria-hidden="true" />
|
||||||
|
<div className="page-header-content">
|
||||||
|
<div className="page-tag">// Pflichtangaben</div>
|
||||||
|
<h1 className="page-title">Impressum</h1>
|
||||||
|
<p className="page-subtitle">Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG).</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="content legal-text">
|
||||||
|
{incomplete && (
|
||||||
|
<p className="notice">
|
||||||
|
⚠️ Die Impressums-Angaben sind noch nicht vollständig hinterlegt
|
||||||
|
(Setup → System → Impressum).
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h2 className="settings-title">// Diensteanbieter</h2>
|
||||||
|
<p>
|
||||||
|
{legal.name || '—'}<br />
|
||||||
|
{(legal.address || '').split('\n').map((line, i) => (
|
||||||
|
<span key={i}>{line}<br /></span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// Kontakt</h2>
|
||||||
|
<p>
|
||||||
|
E-Mail: {legal.email ? <a href={`mailto:${legal.email}`}>{legal.email}</a> : '—'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{legal.extra && (
|
||||||
|
<>
|
||||||
|
<h2 className="settings-title">// Weitere Angaben</h2>
|
||||||
|
<p>{legal.extra.split('\n').map((line, i) => <span key={i}>{line}<br /></span>)}</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h2 className="settings-title">// Verantwortlich für den Inhalt</h2>
|
||||||
|
<p>
|
||||||
|
Verantwortlich für journalistisch-redaktionelle Inhalte nach § 18 Abs. 2 MStV:
|
||||||
|
{' '}{legal.name || '—'}{legal.address ? `, ${legal.address.replace(/\n/g, ', ')}` : ''}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// Haftung für Inhalte</h2>
|
||||||
|
<p>
|
||||||
|
Als Diensteanbieter sind wir für eigene Inhalte auf diesen Seiten nach den
|
||||||
|
allgemeinen Gesetzen verantwortlich. Wir sind jedoch nicht verpflichtet,
|
||||||
|
übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach
|
||||||
|
Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
|
||||||
|
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen
|
||||||
|
nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche
|
||||||
|
Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten
|
||||||
|
Rechtsverletzung möglich. Bei Bekanntwerden entsprechender Rechtsverletzungen
|
||||||
|
entfernen wir diese Inhalte umgehend.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// Haftung für Links</h2>
|
||||||
|
<p>
|
||||||
|
Unser Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte
|
||||||
|
wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch
|
||||||
|
keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der
|
||||||
|
jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Bei Bekanntwerden
|
||||||
|
von Rechtsverletzungen entfernen wir derartige Links umgehend.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="settings-title">// Urheberrecht</h2>
|
||||||
|
<p>
|
||||||
|
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
|
||||||
|
unterliegen dem deutschen Urheberrecht. Beiträge Dritter — insbesondere
|
||||||
|
Screenshots aus der Community-Galerie — sind als solche gekennzeichnet.
|
||||||
|
Sollten Sie auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um
|
||||||
|
einen entsprechenden Hinweis; bei Bekanntwerden entfernen wir die Inhalte
|
||||||
|
umgehend.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
// Member-Profil: Rang & XP, Discord-Rollen, Alpha-Key, Playtester-Status
|
// Member-Profil: Rang & XP, Discord-Rollen, Alpha-Key, Playtester-Status
|
||||||
// + Rollen-Selfservice (Rollen-Menüs direkt im Browser togglen).
|
// + Rollen-Selfservice (Rollen-Menüs direkt im Browser togglen).
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { apiGet, apiPost } from '../api.js';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { apiGet, apiPost, apiDelete } from '../api.js';
|
||||||
|
|
||||||
function fmtDate(iso) {
|
function fmtDate(iso) {
|
||||||
if (!iso) return '—';
|
if (!iso) return '—';
|
||||||
@@ -15,6 +16,7 @@ export default function Profil({ me }) {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [busy, setBusy] = useState('');
|
const [busy, setBusy] = useState('');
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [wipeMsg, setWipeMsg] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
apiGet('/api/profile').then(setProfile).catch((e) => setError(e?.message ?? 'Fehler'));
|
apiGet('/api/profile').then(setProfile).catch((e) => setError(e?.message ?? 'Fehler'));
|
||||||
@@ -40,6 +42,23 @@ export default function Profil({ me }) {
|
|||||||
setBusy('');
|
setBusy('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function wipeData() {
|
||||||
|
if (!window.confirm(
|
||||||
|
'Wirklich alle gespeicherten Daten löschen?\n\n' +
|
||||||
|
'Betrifft: XP/Level, Geburtstag, Playtester-Eintrag, Abstimmungen, ' +
|
||||||
|
'Erinnerungen, Rollen-Sicherung und deinen Alpha-Key.\n\n' +
|
||||||
|
'Das lässt sich nicht rückgängig machen.'
|
||||||
|
)) return;
|
||||||
|
try {
|
||||||
|
const res = await apiDelete('/api/profile');
|
||||||
|
const total = Object.values(res.deleted).reduce((a, b) => a + b, 0);
|
||||||
|
setWipeMsg(`✅ ${total} Einträge gelöscht.`);
|
||||||
|
apiGet('/api/profile').then(setProfile).catch(() => {});
|
||||||
|
} catch {
|
||||||
|
setWipeMsg('⚠️ Löschen fehlgeschlagen — melde dich bitte beim Team.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function copyKey() {
|
async function copyKey() {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(profile.alphaKey);
|
await navigator.clipboard.writeText(profile.alphaKey);
|
||||||
@@ -156,6 +175,22 @@ export default function Profil({ me }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* DSGVO Art. 17: eigene Daten löschen */}
|
||||||
|
<div className="roadmap-group danger-zone">
|
||||||
|
<h2 className="settings-title">// Meine Daten</h2>
|
||||||
|
<p className="field-hint">
|
||||||
|
Du kannst jederzeit alle Daten löschen, die hier zu dir gespeichert
|
||||||
|
sind: XP/Level, Geburtstag, Playtester-Eintrag, Abstimmungen,
|
||||||
|
Erinnerungen, Rollen-Sicherung und einen zugeteilten Alpha-Key.
|
||||||
|
Was du im Discord gepostet hast, löschst du bitte dort. Details im{' '}
|
||||||
|
<Link to="/datenschutz" style={{ textDecoration: 'underline' }}>Datenschutz</Link>.
|
||||||
|
</p>
|
||||||
|
<div className="field-row" style={{ marginTop: '.8rem' }}>
|
||||||
|
<button className="btn btn-danger" onClick={wipeData}>Meine Daten löschen</button>
|
||||||
|
</div>
|
||||||
|
{wipeMsg && <p className="notice" style={{ padding: '.6rem 0 0' }}>{wipeMsg}</p>}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1219,6 +1219,31 @@ export default function Settings({ me }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div className="settings-section">
|
||||||
|
<h2 className="settings-title">// Impressum & Datenschutz</h2>
|
||||||
|
<p className="field-hint" style={{ marginBottom: '.9rem' }}>
|
||||||
|
Erscheint auf <code>/impressum</code> und <code>/datenschutz</code>. Für § 5 DDG
|
||||||
|
ist eine ladungsfähige Anschrift nötig (kein Postfach) — nur der Owner sieht
|
||||||
|
und ändert diese Felder.
|
||||||
|
</p>
|
||||||
|
<div className="field">
|
||||||
|
<label>Name / Verantwortlicher</label>
|
||||||
|
<input type="text" placeholder="Vorname Nachname" value={form.legal_name ?? ''} onChange={(e) => setForm({ ...form, legal_name: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label>Anschrift <span className="field-hint">mehrzeilig: Straße, PLZ Ort, Land</span></label>
|
||||||
|
<textarea rows={3} placeholder={'Musterstraße 1\n12345 Musterstadt\nDeutschland'} value={form.legal_address ?? ''} onChange={(e) => setForm({ ...form, legal_address: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label>Kontakt-E-Mail <span className="field-hint">Pflichtangabe — muss erreichbar sein</span></label>
|
||||||
|
<input type="email" placeholder="kontakt@d4rkst3r.de" value={form.legal_email ?? ''} onChange={(e) => setForm({ ...form, legal_email: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label>Weitere Angaben <span className="field-hint">optional: USt-ID, Telefon, Handelsregister …</span></label>
|
||||||
|
<textarea rows={2} placeholder="USt-IdNr.: DE123456789" value={form.legal_extra ?? ''} onChange={(e) => setForm({ ...form, legal_extra: e.target.value })} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<h2 className="settings-title">// Web-Login (Member)</h2>
|
<h2 className="settings-title">// Web-Login (Member)</h2>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
|
|||||||
@@ -1473,3 +1473,41 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.card-permalink:hover { opacity: 1; }
|
.card-permalink:hover { opacity: 1; }
|
||||||
|
|
||||||
|
/* ── RECHTSTEXTE (Impressum / Datenschutz) ─────────── */
|
||||||
|
.legal-text { max-width: 780px; }
|
||||||
|
.legal-text .settings-title { margin-top: 2.2rem; }
|
||||||
|
.legal-text .settings-title:first-of-type { margin-top: 0; }
|
||||||
|
.legal-text p {
|
||||||
|
color: var(--muted); font-weight: 300; line-height: 1.75;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.legal-text strong { color: var(--text); font-weight: 600; }
|
||||||
|
.legal-text a { color: var(--neon); border-bottom: 1px solid rgba(245, 197, 24, .3); }
|
||||||
|
.legal-text a:hover { border-bottom-color: var(--neon); }
|
||||||
|
.legal-text code {
|
||||||
|
font-family: var(--mono); font-size: .85em;
|
||||||
|
background: var(--bg3); border: 1px solid var(--border);
|
||||||
|
border-radius: 4px; padding: .05rem .35rem; color: var(--text);
|
||||||
|
}
|
||||||
|
.legal-list { list-style: none; margin: 0 0 1rem; padding: 0; }
|
||||||
|
.legal-list li {
|
||||||
|
position: relative; padding-left: 1.4rem; margin-bottom: .7rem;
|
||||||
|
color: var(--muted); font-weight: 300; line-height: 1.7;
|
||||||
|
}
|
||||||
|
.legal-list li::before {
|
||||||
|
content: '▸'; position: absolute; left: 0; color: var(--neon2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lösch-Bereich im Profil */
|
||||||
|
.danger-zone {
|
||||||
|
border: 1px solid rgba(242, 63, 67, .25);
|
||||||
|
border-radius: 12px; padding: 1.2rem 1.4rem;
|
||||||
|
background: rgba(242, 63, 67, .03);
|
||||||
|
}
|
||||||
|
.danger-zone .settings-title { color: #f23f43; }
|
||||||
|
.danger-zone .settings-title::after {
|
||||||
|
background: linear-gradient(90deg, rgba(242, 63, 67, .35), transparent);
|
||||||
|
}
|
||||||
|
.btn-danger { border-color: rgba(242, 63, 67, .5); color: #f23f43; }
|
||||||
|
.btn-danger:hover { border-color: #f23f43; background: rgba(242, 63, 67, .1); }
|
||||||
|
|||||||
@@ -493,6 +493,29 @@ export const setBirthday = (userId, username, day, month) => upsertBirthday.run(
|
|||||||
export const deleteBirthday = (userId) => deleteBirthdayStmt.run(userId).changes > 0;
|
export const deleteBirthday = (userId) => deleteBirthdayStmt.run(userId).changes > 0;
|
||||||
export const birthdaysToday = (day, month) => birthdaysTodayStmt.all(day, month);
|
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
|
// Audit-Log: wer (Owner/Team) hat wann was im Webinterface geändert
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS audit_log (
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
|
|||||||
@@ -161,6 +161,16 @@ export function levelRewards() {
|
|||||||
.sort((a, b) => a.level - b.level);
|
.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) */
|
/** Member-Gate: Web-Login nur für Discord-Member (Default: an, wenn Guild gesetzt) */
|
||||||
export function memberGateEnabled() {
|
export function memberGateEnabled() {
|
||||||
return getSetting('member_gate_enabled') !== '0';
|
return getSetting('member_gate_enabled') !== '0';
|
||||||
|
|||||||
+29
-2
@@ -14,7 +14,7 @@ import {
|
|||||||
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
||||||
saveTemplate, listTemplates, deleteTemplate,
|
saveTemplate, listTemplates, deleteTemplate,
|
||||||
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
getLevelRow, isPlaytester, saveWish, toggleWishVote, myWishVotes,
|
||||||
logAudit, listAudit, playerHistory,
|
logAudit, listAudit, playerHistory, deleteUserData,
|
||||||
} from '../db.js';
|
} from '../db.js';
|
||||||
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
import { lastResults, GAME_ICONS } from '../bot/server-monitor.js';
|
||||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.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 { computeNextRun } from './scheduled-posts.js';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { removeDevlog } from '../bot/devlog-archive.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 { xpForLevel } from '../bot/levels.js';
|
||||||
import { getMilestones } from '../gitea-api.js';
|
import { getMilestones } from '../gitea-api.js';
|
||||||
import { getSessionUser, isAdmin, isGuildMember } from './auth.js';
|
import { getSessionUser, isAdmin, isGuildMember } from './auth.js';
|
||||||
@@ -353,6 +353,10 @@ ${rssItems}
|
|||||||
discord_invite_url: getSetting('discord_invite_url') ?? '',
|
discord_invite_url: getSetting('discord_invite_url') ?? '',
|
||||||
birthday_channel_id: getSetting('birthday_channel_id') ?? '',
|
birthday_channel_id: getSetting('birthday_channel_id') ?? '',
|
||||||
birthday_role_id: getSetting('birthday_role_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 [
|
for (const key of [
|
||||||
'gitea_api_token', 'twitch_client_id', 'twitch_client_secret',
|
'gitea_api_token', 'twitch_client_id', 'twitch_client_secret',
|
||||||
'discord_guild_id', 'bot_name', 'bot_description', 'public_url', 'gitea_url',
|
'discord_guild_id', 'bot_name', 'bot_description', 'public_url', 'gitea_url',
|
||||||
|
'legal_name', 'legal_address', 'legal_email', 'legal_extra',
|
||||||
]) delete body[key];
|
]) delete body[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,6 +550,10 @@ ${rssItems}
|
|||||||
setSetting(key, String(body[key]).trim());
|
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
|
// URLs: müssen mit http(s) beginnen
|
||||||
for (const key of ['public_url', 'gitea_url']) {
|
for (const key of ['public_url', 'gitea_url']) {
|
||||||
if (body[key] === undefined) continue;
|
if (body[key] === undefined) continue;
|
||||||
@@ -1111,6 +1120,24 @@ ${rssItems}
|
|||||||
|
|
||||||
app.get('/api/invite', async () => ({ invite: discordInviteUrl() }));
|
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) => {
|
app.get('/api/profile', async (request, reply) => {
|
||||||
if (await requireMember(request, reply)) return;
|
if (await requireMember(request, reply)) return;
|
||||||
const user = getSessionUser(request);
|
const user = getSessionUser(request);
|
||||||
|
|||||||
Reference in New Issue
Block a user