diff --git a/DSGVO_CHECKLIST.md b/DSGVO_CHECKLIST.md
new file mode 100644
index 0000000..3777763
--- /dev/null
+++ b/DSGVO_CHECKLIST.md
@@ -0,0 +1,342 @@
+# DSGVO-Konformitäts-Checkliste für Trainingstagebuch
+
+## Status: ⚠️ PRÜFUNG ERFORDERLICH
+
+Diese Checkliste dokumentiert den aktuellen Stand der DSGVO-Konformität der Anwendung.
+
+---
+
+## 1. Datenschutzerklärung ✅ / ⚠️
+
+### Status: ⚠️ Teilweise vorhanden, muss aktualisiert werden
+
+**Vorhanden:**
+- ✅ Datenschutzerklärung vorhanden (`/datenschutz`)
+- ✅ Impressum vorhanden (`/impressum`)
+- ✅ Verlinkung im Footer
+
+**Fehlend/Verbesserungsbedarf:**
+- ⚠️ MyTischtennis-Integration nicht erwähnt (Drittlandübermittlung?)
+- ⚠️ Logging von API-Requests nicht erwähnt
+- ⚠️ Verschlüsselung von Mitgliederdaten nicht erwähnt
+- ⚠️ Speicherdauer für Logs nicht konkretisiert
+- ⚠️ Keine Informationen zu automatischer Löschung
+
+---
+
+## 2. Einwilligungen ⚠️
+
+### Status: ⚠️ Teilweise vorhanden
+
+**Vorhanden:**
+- ✅ `picsInInternetAllowed` bei Mitgliedern (Einwilligung für Fotos im Internet)
+- ✅ MyTischtennis: `savePassword` und `autoUpdateRatings` (Einwilligungen)
+
+**Fehlend/Verbesserungsbedarf:**
+- ⚠️ Keine explizite Einwilligung bei Registrierung zur Datenschutzerklärung
+- ⚠️ Keine Einwilligung für Logging von API-Requests
+- ⚠️ Keine Einwilligung für Datenübertragung an MyTischtennis.de
+- ⚠️ Keine Möglichkeit, Einwilligungen zu widerrufen (außer manuell)
+
+---
+
+## 3. Löschrechte (Art. 17 DSGVO) ⚠️
+
+### Status: ⚠️ Teilweise implementiert
+
+**Vorhanden:**
+- ✅ DELETE-Endpunkte für viele Ressourcen (Member, Tournament, etc.)
+- ✅ MyTischtennis-Account kann gelöscht werden
+
+**Fehlend/Verbesserungsbedarf:**
+- ❌ **KRITISCH:** Kein Endpunkt zum vollständigen Löschen eines User-Accounts
+- ❌ **KRITISCH:** Keine automatische Löschung aller zugehörigen Daten (Cascade-Delete)
+- ❌ Keine Löschung von Logs nach Ablauf der Speicherdauer
+- ⚠️ Keine Anonymisierung statt Löschung (falls gesetzliche Aufbewahrungspflichten bestehen)
+- ⚠️ Keine Bestätigung vor Löschung kritischer Daten
+
+**Empfehlung:**
+- Implementiere `/api/user/delete` Endpunkt
+- Implementiere automatische Löschung aller zugehörigen Daten:
+ - UserClub-Einträge
+ - MyTischtennis-Account
+ - Alle Logs (nach Anonymisierung)
+ - Alle Mitglieder, die nur diesem User zugeordnet sind
+- Implementiere automatische Löschung von Logs nach 90 Tagen
+
+---
+
+## 4. Auskunftsrechte (Art. 15 DSGVO) ❌
+
+### Status: ❌ Nicht implementiert
+
+**Fehlend:**
+- ❌ **KRITISCH:** Kein Endpunkt zur Auskunft über gespeicherte Daten
+- ❌ Keine Übersicht über alle personenbezogenen Daten eines Users
+- ❌ Keine Übersicht über alle Mitgliederdaten
+- ❌ Keine Übersicht über Logs, die einen User betreffen
+
+**Empfehlung:**
+- Implementiere `/api/user/data-export` Endpunkt
+- Exportiere alle Daten in strukturiertem Format (JSON)
+- Inkludiere:
+ - User-Daten
+ - Vereinszugehörigkeiten
+ - Mitgliederdaten (falls User Zugriff hat)
+ - Logs
+ - MyTischtennis-Daten
+
+---
+
+## 5. Datenportabilität (Art. 20 DSGVO) ❌
+
+### Status: ❌ Nicht implementiert
+
+**Fehlend:**
+- ❌ **KRITISCH:** Kein Export in maschinenlesbarem Format
+- ❌ Keine JSON/XML-Export-Funktion
+- ⚠️ PDF-Export für Trainingstage vorhanden, aber nicht für alle Daten
+
+**Empfehlung:**
+- Implementiere `/api/user/data-export` mit JSON-Format
+- Implementiere Export für:
+ - Alle eigenen Daten
+ - Alle Mitgliederdaten (falls berechtigt)
+ - Alle Trainingsdaten
+ - Alle Turnierdaten
+
+---
+
+## 6. Verschlüsselung ✅ / ⚠️
+
+### Status: ✅ Gut implementiert
+
+**Vorhanden:**
+- ✅ AES-256-CBC Verschlüsselung für Mitgliederdaten:
+ - firstName, lastName
+ - birthDate
+ - phone, street, city, postalCode
+ - email
+ - notes (Participant)
+- ✅ Passwörter werden mit bcrypt gehasht
+- ✅ HTTPS für alle Verbindungen
+
+**Verbesserungsbedarf:**
+- ⚠️ Verschlüsselungsschlüssel sollte in separater, sicherer Konfiguration sein
+- ✅ **BEHOBEN:** MyTischtennis-Daten werden jetzt vollständig verschlüsselt (E-Mail, Zugriffstoken, Refresh-Token, Cookie, Benutzerdaten, Vereinsinformationen)
+- ⚠️ Keine Verschlüsselung für Logs (können personenbezogene Daten enthalten)
+
+---
+
+## 7. Logging ⚠️
+
+### Status: ⚠️ Verbesserungsbedarf
+
+**Vorhanden:**
+- ✅ Aktivitäts-Logging (`log` Tabelle) - protokolliert wichtige Aktionen
+- ✅ Server-Logs - Standard-Server-Logs für Fehlerbehebung
+- ✅ **ENTFERNT:** API-Logging für MyTischtennis-Requests wurde deaktiviert
+
+**Probleme:**
+- ✅ **BEHOBEN:** API-Logging für MyTischtennis-Requests wurde komplett entfernt (keine personenbezogenen Daten mehr in API-Logs)
+- ⚠️ Keine automatische Löschung von Aktivitätslogs (noch zu implementieren)
+- ✅ **BEHOBEN:** In Datenschutzerklärung dokumentiert, was geloggt wird
+
+**Empfehlung:**
+- ⚠️ Implementiere automatische Löschung von Aktivitätslogs nach angemessener Frist (noch ausstehend)
+
+---
+
+## 8. MyTischtennis-Integration ⚠️
+
+### Status: ⚠️ Verbesserungsbedarf
+
+**Vorhanden:**
+- ✅ Verschlüsselung von Passwörtern
+- ✅ Einwilligungen (`savePassword`, `autoUpdateRatings`)
+- ✅ DELETE-Endpunkt für Account
+
+**Probleme:**
+- ✅ **BEHOBEN:** Drittlandübermittlung in Datenschutzerklärung erwähnt
+- ⚠️ Keine explizite Einwilligung für Datenübertragung an MyTischtennis.de
+- ✅ **BEHOBEN:** Informationen über Datenschutz bei MyTischtennis.de in Datenschutzerklärung
+- ✅ **BEHOBEN:** Alle MyTischtennis-Daten werden jetzt verschlüsselt gespeichert
+
+**Empfehlung:**
+- Aktualisiere Datenschutzerklärung:
+ - Erwähne MyTischtennis-Integration
+ - Erkläre, welche Daten übertragen werden
+ - Verweise auf Datenschutzerklärung von MyTischtennis.de
+ - Erkläre Rechtsgrundlage (Einwilligung)
+- Implementiere explizite Einwilligung bei Einrichtung der Integration
+- Verschlüssele auch Zugriffstoken
+
+---
+
+## 9. Cookies & Local Storage ✅
+
+### Status: ✅ Konform
+
+**Vorhanden:**
+- ✅ Nur technisch notwendige Cookies/Storage:
+ - Session-Token (Session Storage)
+ - Username, Clubs, Permissions (Local Storage)
+- ✅ Keine Tracking-Cookies
+- ✅ Keine Werbe-Cookies
+- ✅ Dokumentiert in Datenschutzerklärung
+
+**Hinweis:**
+- Local Storage wird für persistente Daten verwendet (Clubs, Permissions)
+- Dies ist technisch notwendig und DSGVO-konform
+
+---
+
+## 10. Berechtigungssystem ✅
+
+### Status: ✅ Gut implementiert
+
+**Vorhanden:**
+- ✅ Rollenbasierte Zugriffe (Admin, Trainer, Mannschaftsführer, Mitglied)
+- ✅ Individuelle Berechtigungen pro Ressource
+- ✅ Transparente Zugriffskontrolle
+- ✅ Logging von Aktivitäten
+
+**Hinweis:**
+- Berechtigungssystem ist DSGVO-konform
+- Ermöglicht Datenminimierung (Zugriff nur auf notwendige Daten)
+
+---
+
+## 11. Datenminimierung ⚠️
+
+### Status: ⚠️ Teilweise konform
+
+**Vorhanden:**
+- ✅ Nur notwendige Daten werden gespeichert
+- ✅ Berechtigungssystem ermöglicht minimale Datenzugriffe
+
+**Verbesserungsbedarf:**
+- ⚠️ Logs enthalten möglicherweise zu viele Daten (Request/Response-Bodies)
+- ⚠️ Keine automatische Löschung alter Daten
+- ⚠️ Keine Option, Daten zu anonymisieren statt zu löschen
+
+---
+
+## 12. Technische und organisatorische Maßnahmen (TOM) ✅ / ⚠️
+
+### Status: ✅ Gut, aber verbesserungsbedürftig
+
+**Vorhanden:**
+- ✅ Verschlüsselung sensibler Daten
+- ✅ HTTPS für alle Verbindungen
+- ✅ Passwort-Hashing (bcrypt)
+- ✅ Authentifizierung und Autorisierung
+- ✅ Berechtigungssystem
+
+**Verbesserungsbedarf:**
+- ⚠️ Keine Dokumentation der TOM
+- ⚠️ Keine regelmäßigen Sicherheitsupdates dokumentiert
+- ⚠️ Keine Backup-Strategie dokumentiert
+- ⚠️ Keine Notfallpläne dokumentiert
+
+---
+
+## 13. Auftragsverarbeitung ⚠️
+
+### Status: ⚠️ Nicht dokumentiert
+
+**Fehlend:**
+- ⚠️ Keine Informationen über Hosting-Provider
+- ⚠️ Keine Informationen über Auftragsverarbeitungsverträge (AVV)
+- ⚠️ Keine Informationen über Subunternehmer
+
+**Empfehlung:**
+- Dokumentiere alle Auftragsverarbeiter (Hosting, etc.)
+- Erwähne in Datenschutzerklärung, dass AVV abgeschlossen wurden
+
+---
+
+## 14. Betroffenenrechte - Umsetzung ❌
+
+### Status: ❌ Nicht vollständig implementiert
+
+**Fehlend:**
+- ❌ **KRITISCH:** Kein Endpunkt für Auskunft (Art. 15)
+- ❌ **KRITISCH:** Kein Endpunkt für Löschung (Art. 17)
+- ❌ **KRITISCH:** Kein Endpunkt für Datenexport (Art. 20)
+- ❌ Kein Endpunkt für Berichtigung (Art. 16) - teilweise vorhanden über normale Edit-Endpunkte
+- ❌ Kein Endpunkt für Einschränkung (Art. 18)
+- ❌ Kein Endpunkt für Widerspruch (Art. 21)
+
+**Empfehlung:**
+- Implementiere zentrale Endpunkte für alle Betroffenenrechte:
+ - `GET /api/user/rights/information` - Auskunft
+ - `DELETE /api/user/rights/deletion` - Löschung
+ - `GET /api/user/rights/export` - Datenexport
+ - `PUT /api/user/rights/restriction` - Einschränkung
+ - `POST /api/user/rights/objection` - Widerspruch
+
+---
+
+## 15. Kontakt für Datenschutz ✅
+
+### Status: ✅ Vorhanden
+
+**Vorhanden:**
+- ✅ E-Mail-Adresse in Datenschutzerklärung: tsschulz@tsschulz.de
+- ✅ Vollständige Anschrift im Impressum
+
+---
+
+## Zusammenfassung
+
+### ✅ Gut implementiert:
+1. Verschlüsselung sensibler Daten
+2. HTTPS
+3. Berechtigungssystem
+4. Cookies/Local Storage (nur technisch notwendig)
+5. Datenschutzerklärung vorhanden
+
+### ⚠️ Verbesserungsbedarf:
+1. Datenschutzerklärung aktualisieren (MyTischtennis, Logging)
+2. Logging von personenbezogenen Daten reduzieren/anonymisieren
+3. Automatische Löschung von Logs implementieren
+4. MyTischtennis-Integration in Datenschutzerklärung erwähnen
+
+### ❌ Kritisch - Muss implementiert werden:
+1. **Löschrechte-API** (Art. 17 DSGVO)
+2. **Auskunftsrechte-API** (Art. 15 DSGVO)
+3. **Datenexport-API** (Art. 20 DSGVO)
+4. **Automatische Löschung von Logs** nach Retention-Periode
+
+---
+
+## Prioritäten
+
+### Sofort (vor Live-Betrieb):
+1. Datenschutzerklärung aktualisieren
+2. Löschrechte-API implementieren
+3. Auskunftsrechte-API implementieren
+4. Datenexport-API implementieren
+
+### Kurzfristig (innerhalb 1 Monat):
+1. Automatische Löschung von Logs implementieren
+2. Logging von personenbezogenen Daten reduzieren/anonymisieren
+3. MyTischtennis-Integration in Datenschutzerklärung dokumentieren
+
+### Mittelfristig (innerhalb 3 Monate):
+1. Einwilligungsmanagement implementieren
+2. TOM dokumentieren
+3. Auftragsverarbeitung dokumentieren
+
+---
+
+## Nächste Schritte
+
+1. ✅ Diese Checkliste erstellen
+2. ⏳ Datenschutzerklärung aktualisieren
+3. ⏳ Löschrechte-API implementieren
+4. ⏳ Auskunftsrechte-API implementieren
+5. ⏳ Datenexport-API implementieren
+6. ⏳ Logging verbessern
+
diff --git a/apache.conf.example b/apache.conf.example
index ca4527e..ba753ca 100644
--- a/apache.conf.example
+++ b/apache.conf.example
@@ -8,9 +8,29 @@
# sudo a2enmod headers
# sudo systemctl restart apache2
+# 301-Weiterleitung von www auf non-www (HTTP)
+
+ ServerName www.tt-tagebuch.de
+ Redirect permanent / https://tt-tagebuch.de/
+
+
+
+ ServerName tt-tagebuch.de
+ Redirect permanent / https://tt-tagebuch.de/
+
+
+# 301-Weiterleitung von www auf non-www (HTTPS)
+
+ ServerName www.tt-tagebuch.de
+ SSLEngine on
+ SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
+ SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
+ Include /etc/letsencrypt/options-ssl-apache.conf
+ Redirect permanent / https://tt-tagebuch.de/
+
+
ServerName tt-tagebuch.de
- ServerAlias www.tt-tagebuch.de
DocumentRoot /var/www/tt-tagebuch.de
diff --git a/backend/middleware/requestLoggingMiddleware.js b/backend/middleware/requestLoggingMiddleware.js
index 9a4dfaf..71364a8 100644
--- a/backend/middleware/requestLoggingMiddleware.js
+++ b/backend/middleware/requestLoggingMiddleware.js
@@ -1,87 +1,13 @@
-import ApiLog from '../models/ApiLog.js';
-
/**
* Middleware to log all API requests and responses
* Should be added early in the middleware chain, but after authentication
+ *
+ * HINWEIS: Logging wurde deaktiviert - keine API-Requests werden mehr geloggt
+ * (früher wurden nur MyTischtennis-Requests geloggt, dies wurde entfernt)
*/
export const requestLoggingMiddleware = async (req, res, next) => {
- const startTime = Date.now();
- const originalSend = res.send;
-
- // Get request body (but limit size for sensitive data)
- let requestBody = null;
- if (req.body && Object.keys(req.body).length > 0) {
- const bodyStr = JSON.stringify(req.body);
- // Truncate very long bodies
- requestBody = bodyStr.length > 10000 ? bodyStr.substring(0, 10000) + '... (truncated)' : bodyStr;
- }
-
- // Capture response
- let responseBody = null;
- res.send = function(data) {
- // Try to parse response as JSON
- try {
- const parsed = JSON.parse(data);
- const responseStr = JSON.stringify(parsed);
- // Truncate very long responses
- responseBody = responseStr.length > 10000 ? responseStr.substring(0, 10000) + '... (truncated)' : responseStr;
- } catch (e) {
- // Not JSON, just use raw data (truncated)
- responseBody = typeof data === 'string' ? data.substring(0, 1000) : String(data).substring(0, 1000);
- }
-
- // Restore original send
- res.send = originalSend;
- return res.send.apply(res, arguments);
- };
-
- // Log after response is sent
- res.on('finish', async () => {
- const executionTime = Date.now() - startTime;
- const ipAddress = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
- const path = req.path || req.url;
-
- // Nur myTischtennis-Requests loggen
- // Skip logging for non-data endpoints (Status-Checks, Health-Checks, etc.)
- // Exclude any endpoint containing 'status' or root paths
- if (
- path.includes('/status') ||
- path === '/' ||
- path === '/health' ||
- path.endsWith('/status') ||
- path.includes('/scheduler-status')
- ) {
- return;
- }
-
- // Nur myTischtennis-Endpunkte loggen (z.B. /api/mytischtennis/*)
- if (!path.includes('/mytischtennis')) {
- return;
- }
-
- // Get user ID if available (wird von authMiddleware gesetzt)
- const userId = req.user?.id || null;
-
- try {
- await ApiLog.create({
- userId,
- method: req.method,
- path: path,
- statusCode: res.statusCode,
- requestBody,
- responseBody,
- executionTime,
- errorMessage: res.statusCode >= 400 ? `HTTP ${res.statusCode}` : null,
- ipAddress,
- userAgent: req.headers['user-agent'],
- logType: 'api_request'
- });
- } catch (error) {
- // Don't let logging errors break the request
- console.error('Error logging API request:', error);
- }
- });
-
+ // Logging wurde deaktiviert - keine API-Requests werden mehr geloggt
+ // (früher wurden nur MyTischtennis-Requests geloggt, dies wurde entfernt)
next();
};
diff --git a/backend/models/MyTischtennis.js b/backend/models/MyTischtennis.js
index 433a325..557399b 100644
--- a/backend/models/MyTischtennis.js
+++ b/backend/models/MyTischtennis.js
@@ -22,6 +22,14 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
email: {
type: DataTypes.STRING,
allowNull: false,
+ set(value) {
+ const encryptedValue = encryptData(value);
+ this.setDataValue('email', encryptedValue);
+ },
+ get() {
+ const encryptedValue = this.getDataValue('email');
+ return decryptData(encryptedValue);
+ }
},
encryptedPassword: {
type: DataTypes.TEXT,
@@ -43,12 +51,38 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
accessToken: {
type: DataTypes.TEXT,
allowNull: true,
- field: 'access_token'
+ field: 'access_token',
+ set(value) {
+ if (value === null || value === undefined) {
+ this.setDataValue('accessToken', null);
+ } else {
+ const encryptedValue = encryptData(value);
+ this.setDataValue('accessToken', encryptedValue);
+ }
+ },
+ get() {
+ const encryptedValue = this.getDataValue('accessToken');
+ if (!encryptedValue) return null;
+ return decryptData(encryptedValue);
+ }
},
refreshToken: {
type: DataTypes.TEXT,
allowNull: true,
- field: 'refresh_token'
+ field: 'refresh_token',
+ set(value) {
+ if (value === null || value === undefined) {
+ this.setDataValue('refreshToken', null);
+ } else {
+ const encryptedValue = encryptData(value);
+ this.setDataValue('refreshToken', encryptedValue);
+ }
+ },
+ get() {
+ const encryptedValue = this.getDataValue('refreshToken');
+ if (!encryptedValue) return null;
+ return decryptData(encryptedValue);
+ }
},
expiresAt: {
type: DataTypes.BIGINT,
@@ -57,27 +91,99 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
},
cookie: {
type: DataTypes.TEXT,
- allowNull: true
+ allowNull: true,
+ set(value) {
+ if (value === null || value === undefined) {
+ this.setDataValue('cookie', null);
+ } else {
+ const encryptedValue = encryptData(value);
+ this.setDataValue('cookie', encryptedValue);
+ }
+ },
+ get() {
+ const encryptedValue = this.getDataValue('cookie');
+ if (!encryptedValue) return null;
+ return decryptData(encryptedValue);
+ }
},
userData: {
- type: DataTypes.JSON,
+ type: DataTypes.TEXT, // Changed from JSON to TEXT to store encrypted JSON string
allowNull: true,
- field: 'user_data'
+ field: 'user_data',
+ set(value) {
+ if (value === null || value === undefined) {
+ this.setDataValue('userData', null);
+ } else {
+ const jsonString = typeof value === 'string' ? value : JSON.stringify(value);
+ const encryptedValue = encryptData(jsonString);
+ this.setDataValue('userData', encryptedValue);
+ }
+ },
+ get() {
+ const encryptedValue = this.getDataValue('userData');
+ if (!encryptedValue) return null;
+ try {
+ const decryptedString = decryptData(encryptedValue);
+ return JSON.parse(decryptedString);
+ } catch (error) {
+ console.error('Error decrypting/parsing userData:', error);
+ return null;
+ }
+ }
},
clubId: {
type: DataTypes.STRING,
allowNull: true,
- field: 'club_id'
+ field: 'club_id',
+ set(value) {
+ if (value === null || value === undefined) {
+ this.setDataValue('clubId', null);
+ } else {
+ const encryptedValue = encryptData(value);
+ this.setDataValue('clubId', encryptedValue);
+ }
+ },
+ get() {
+ const encryptedValue = this.getDataValue('clubId');
+ if (!encryptedValue) return null;
+ return decryptData(encryptedValue);
+ }
},
clubName: {
type: DataTypes.STRING,
allowNull: true,
- field: 'club_name'
+ field: 'club_name',
+ set(value) {
+ if (value === null || value === undefined) {
+ this.setDataValue('clubName', null);
+ } else {
+ const encryptedValue = encryptData(value);
+ this.setDataValue('clubName', encryptedValue);
+ }
+ },
+ get() {
+ const encryptedValue = this.getDataValue('clubName');
+ if (!encryptedValue) return null;
+ return decryptData(encryptedValue);
+ }
},
fedNickname: {
type: DataTypes.STRING,
allowNull: true,
- field: 'fed_nickname'
+ field: 'fed_nickname',
+ set(value) {
+ if (value === null || value === undefined) {
+ this.setDataValue('fedNickname', null);
+ } else {
+ const encryptedValue = encryptData(value);
+ this.setDataValue('fedNickname', encryptedValue);
+ }
+ },
+ get() {
+ const encryptedValue = this.getDataValue('fedNickname');
+ if (!encryptedValue) return null;
+ return decryptData(encryptedValue);
+ }
},
lastLoginAttempt: {
type: DataTypes.DATE,
diff --git a/backend/scripts/migrateMyTischtennisEncryption.js b/backend/scripts/migrateMyTischtennisEncryption.js
new file mode 100644
index 0000000..ef86835
--- /dev/null
+++ b/backend/scripts/migrateMyTischtennisEncryption.js
@@ -0,0 +1,169 @@
+/**
+ * Migration Script: Verschlüsselung für bestehende MyTischtennis-Daten
+ *
+ * WICHTIG: Dieses Script verschlüsselt bestehende unverschlüsselte MyTischtennis-Daten.
+ * Es sollte NUR EINMAL ausgeführt werden, nachdem das Model aktualisiert wurde.
+ *
+ * Vorsicht: Wenn Daten bereits verschlüsselt sind, wird dieses Script sie doppelt verschlüsseln!
+ *
+ * Usage: node backend/scripts/migrateMyTischtennisEncryption.js
+ */
+
+import sequelize from '../database.js';
+import MyTischtennis from '../models/MyTischtennis.js';
+import { encryptData } from '../utils/encrypt.js';
+
+async function migrateMyTischtennisEncryption() {
+ console.log('🔄 Starte Migration: Verschlüsselung für MyTischtennis-Daten\n');
+
+ try {
+ // Hole alle MyTischtennis-Einträge mit raw: true, um unverschlüsselte Daten zu bekommen
+ const accounts = await MyTischtennis.findAll({
+ raw: true,
+ attributes: ['id', 'email', 'access_token', 'refresh_token', 'cookie', 'user_data', 'club_id', 'club_name', 'fed_nickname']
+ });
+
+ console.log(`📊 Gefundene Einträge: ${accounts.length}\n`);
+
+ if (accounts.length === 0) {
+ console.log('✅ Keine Einträge gefunden. Migration nicht erforderlich.');
+ return;
+ }
+
+ let migrated = 0;
+ let skipped = 0;
+ let errors = 0;
+
+ for (const account of accounts) {
+ try {
+ // Prüfe, ob Daten bereits verschlüsselt sind
+ // Verschlüsselte Daten sind hex-Strings und haben eine bestimmte Länge
+ // Unverschlüsselte E-Mail-Adressen enthalten normalerweise @
+ const emailIsEncrypted = !account.email.includes('@') && account.email.length > 32;
+
+ if (emailIsEncrypted) {
+ console.log(`⏭️ Eintrag ${account.id}: Bereits verschlüsselt, überspringe...`);
+ skipped++;
+ continue;
+ }
+
+ console.log(`🔐 Verschlüssele Eintrag ${account.id}...`);
+
+ // Verschlüssele alle Felder direkt in der Datenbank
+ const updateData = {};
+
+ if (account.email && account.email.includes('@')) {
+ updateData.email = encryptData(account.email);
+ }
+
+ if (account.access_token && !account.access_token.startsWith('encrypted_')) {
+ // Prüfe, ob es bereits verschlüsselt aussieht (hex-String)
+ const looksEncrypted = /^[0-9a-f]+$/i.test(account.access_token) && account.access_token.length > 32;
+ if (!looksEncrypted) {
+ updateData.access_token = encryptData(account.access_token);
+ }
+ }
+
+ if (account.refresh_token && !account.refresh_token.startsWith('encrypted_')) {
+ const looksEncrypted = /^[0-9a-f]+$/i.test(account.refresh_token) && account.refresh_token.length > 32;
+ if (!looksEncrypted) {
+ updateData.refresh_token = encryptData(account.refresh_token);
+ }
+ }
+
+ if (account.cookie && !account.cookie.startsWith('encrypted_')) {
+ const looksEncrypted = /^[0-9a-f]+$/i.test(account.cookie) && account.cookie.length > 32;
+ if (!looksEncrypted) {
+ updateData.cookie = encryptData(account.cookie);
+ }
+ }
+
+ if (account.user_data) {
+ // user_data ist JSON, muss zuerst zu String konvertiert werden
+ try {
+ const userDataStr = typeof account.user_data === 'string'
+ ? account.user_data
+ : JSON.stringify(account.user_data);
+ // Prüfe, ob bereits verschlüsselt
+ const looksEncrypted = /^[0-9a-f]+$/i.test(userDataStr) && userDataStr.length > 32;
+ if (!looksEncrypted) {
+ updateData.user_data = encryptData(userDataStr);
+ }
+ } catch (e) {
+ console.error(` ⚠️ Fehler bei user_data für Eintrag ${account.id}:`, e.message);
+ }
+ }
+
+ if (account.club_id && account.club_id.length > 0 && !account.club_id.startsWith('encrypted_')) {
+ const looksEncrypted = /^[0-9a-f]+$/i.test(account.club_id) && account.club_id.length > 32;
+ if (!looksEncrypted) {
+ updateData.club_id = encryptData(account.club_id);
+ }
+ }
+
+ if (account.club_name && account.club_name.length > 0 && !account.club_name.startsWith('encrypted_')) {
+ const looksEncrypted = /^[0-9a-f]+$/i.test(account.club_name) && account.club_name.length > 32;
+ if (!looksEncrypted) {
+ updateData.club_name = encryptData(account.club_name);
+ }
+ }
+
+ if (account.fed_nickname && account.fed_nickname.length > 0 && !account.fed_nickname.startsWith('encrypted_')) {
+ const looksEncrypted = /^[0-9a-f]+$/i.test(account.fed_nickname) && account.fed_nickname.length > 32;
+ if (!looksEncrypted) {
+ updateData.fed_nickname = encryptData(account.fed_nickname);
+ }
+ }
+
+ // Update nur, wenn es etwas zu aktualisieren gibt
+ if (Object.keys(updateData).length > 0) {
+ await sequelize.query(
+ `UPDATE my_tischtennis SET ${Object.keys(updateData).map(key => `\`${key}\` = :${key}`).join(', ')} WHERE id = :id`,
+ {
+ replacements: { ...updateData, id: account.id },
+ type: sequelize.QueryTypes.UPDATE
+ }
+ );
+ migrated++;
+ console.log(` ✅ Eintrag ${account.id} erfolgreich verschlüsselt`);
+ } else {
+ skipped++;
+ console.log(` ⏭️ Eintrag ${account.id}: Keine unverschlüsselten Daten gefunden`);
+ }
+
+ } catch (error) {
+ errors++;
+ console.error(` ❌ Fehler bei Eintrag ${account.id}:`, error.message);
+ }
+ }
+
+ console.log('\n📊 Migrations-Zusammenfassung:');
+ console.log(` ✅ Migriert: ${migrated}`);
+ console.log(` ⏭️ Übersprungen: ${skipped}`);
+ console.log(` ❌ Fehler: ${errors}`);
+
+ if (errors === 0) {
+ console.log('\n✅ Migration erfolgreich abgeschlossen!');
+ } else {
+ console.log('\n⚠️ Migration abgeschlossen, aber es gab Fehler. Bitte prüfen Sie die Logs.');
+ }
+
+ } catch (error) {
+ console.error('❌ Kritischer Fehler bei Migration:', error);
+ throw error;
+ } finally {
+ await sequelize.close();
+ }
+}
+
+// Script ausführen
+migrateMyTischtennisEncryption()
+ .then(() => {
+ console.log('\n✅ Script beendet.');
+ process.exit(0);
+ })
+ .catch((error) => {
+ console.error('\n❌ Script fehlgeschlagen:', error);
+ process.exit(1);
+ });
+
diff --git a/backend/server.js b/backend/server.js
index 42a9847..2d512ef 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -139,12 +139,48 @@ app.use('/api/member-transfer-config', memberTransferConfigRoutes);
app.use('/api/training-groups', trainingGroupRoutes);
app.use('/api/training-times', trainingTimeRoutes);
-app.use(express.static(path.join(__dirname, '../frontend/dist')));
+// Middleware für dynamischen kanonischen Tag (vor express.static)
+const setCanonicalTag = (req, res, next) => {
+ // Nur für HTML-Anfragen (nicht für API, Assets, etc.)
+ if (req.path.startsWith('/api') || req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|mp3|webmanifest|xml|txt)$/)) {
+ return next();
+ }
-// Catch-All Handler für Frontend-Routen (muss nach den API-Routen stehen)
-app.get('*', (req, res) => {
- res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
-});
+ // Prüfe, ob die Datei als statische Datei existiert (außer index.html)
+ const staticPath = path.join(__dirname, '../frontend/dist', req.path);
+ fs.access(staticPath, fs.constants.F_OK, (err) => {
+ if (!err && req.path !== '/' && req.path !== '/index.html') {
+ // Datei existiert und ist nicht index.html, lasse express.static sie servieren
+ return next();
+ }
+
+ // Datei existiert nicht oder ist index.html, serviere index.html mit dynamischem kanonischen Tag
+ const indexPath = path.join(__dirname, '../frontend/dist/index.html');
+ fs.readFile(indexPath, 'utf8', (err, data) => {
+ if (err) {
+ return next();
+ }
+
+ // Bestimme die kanonische URL (bevorzuge non-www)
+ const protocol = req.protocol || 'https';
+ const host = req.get('host') || 'tt-tagebuch.de';
+ const canonicalHost = host.replace(/^www\./, ''); // Entferne www falls vorhanden
+ const canonicalUrl = `${protocol}://${canonicalHost}${req.path === '/' ? '' : req.path}`;
+
+ // Ersetze den kanonischen Tag
+ const updatedData = data.replace(
+ / /,
+ ` `
+ );
+
+ res.setHeader('Content-Type', 'text/html');
+ res.send(updatedData);
+ });
+ });
+};
+
+app.use(setCanonicalTag);
+app.use(express.static(path.join(__dirname, '../frontend/dist')));
(async () => {
try {
diff --git a/backend/services/apiLogService.js b/backend/services/apiLogService.js
index d736d56..f949288 100644
--- a/backend/services/apiLogService.js
+++ b/backend/services/apiLogService.js
@@ -1,9 +1,11 @@
import ApiLog from '../models/ApiLog.js';
import { Op } from 'sequelize';
+import { sanitizeLogData, truncateString, sanitizeIpAddress, sanitizeUserAgent } from '../utils/logDataSanitizer.js';
class ApiLogService {
/**
* Log an API request/response
+ * DSGVO-konform: Personenbezogene Daten werden nur bei Fehlern geloggt und dann verschlüsselt/gekürzt
*/
async logRequest(options) {
try {
@@ -22,24 +24,49 @@ class ApiLogService {
schedulerJobType = null
} = options;
- // Truncate long fields (raise limits to fit typical API JSON bodies)
- const truncate = (str, maxLen = 64000) => {
- if (!str) return null;
- const strVal = typeof str === 'string' ? str : JSON.stringify(str);
- return strVal.length > maxLen ? strVal.substring(0, maxLen) + '... (truncated)' : strVal;
- };
+ const isError = statusCode >= 400;
+
+ // DSGVO-konform: Nur bei Fehlern Request/Response-Bodies loggen
+ let sanitizedRequestBody = null;
+ let sanitizedResponseBody = null;
+
+ if (isError) {
+ // Bei Fehlern: Sanitize personenbezogene Daten
+ if (requestBody) {
+ sanitizedRequestBody = sanitizeLogData(requestBody, true); // Verschlüssele sensible Daten
+ // Prüfe, ob Ergebnis bereits ein String ist (sanitizeLogData kann String oder Objekt zurückgeben)
+ const requestBodyStr = typeof sanitizedRequestBody === 'string'
+ ? sanitizedRequestBody
+ : JSON.stringify(sanitizedRequestBody);
+ sanitizedRequestBody = truncateString(requestBodyStr, 2000);
+ }
+
+ if (responseBody) {
+ sanitizedResponseBody = sanitizeLogData(responseBody, true); // Verschlüssele sensible Daten
+ // Prüfe, ob Ergebnis bereits ein String ist (sanitizeLogData kann String oder Objekt zurückgeben)
+ const responseBodyStr = typeof sanitizedResponseBody === 'string'
+ ? sanitizedResponseBody
+ : JSON.stringify(sanitizedResponseBody);
+ sanitizedResponseBody = truncateString(responseBodyStr, 2000);
+ }
+ }
+ // Bei Erfolg: Keine Bodies loggen (Datenminimierung)
+
+ // IP-Adresse und User-Agent sanitizen
+ const sanitizedIp = sanitizeIpAddress(ipAddress);
+ const sanitizedUA = sanitizeUserAgent(userAgent);
await ApiLog.create({
userId,
method,
path,
statusCode,
- requestBody: truncate(requestBody, 64000),
- responseBody: truncate(responseBody, 64000),
+ requestBody: sanitizedRequestBody,
+ responseBody: sanitizedResponseBody,
executionTime,
- errorMessage: truncate(errorMessage, 5000),
- ipAddress,
- userAgent,
+ errorMessage: errorMessage ? truncateString(errorMessage, 5000) : null,
+ ipAddress: sanitizedIp,
+ userAgent: sanitizedUA,
logType,
schedulerJobType
});
@@ -157,6 +184,8 @@ class ApiLogService {
'id', 'userId', 'method', 'path', 'statusCode',
'executionTime', 'errorMessage', 'ipAddress', 'logType',
'schedulerJobType', 'createdAt'
+ // requestBody und responseBody werden NICHT zurückgegeben (DSGVO: Datenminimierung)
+ // Nur bei expliziter Anfrage über getLogById verfügbar
]
});
diff --git a/backend/services/myTischtennisService.js b/backend/services/myTischtennisService.js
index 8abc8ac..9134a2c 100644
--- a/backend/services/myTischtennisService.js
+++ b/backend/services/myTischtennisService.js
@@ -11,10 +11,30 @@ class MyTischtennisService {
*/
async getAccount(userId) {
const account = await MyTischtennis.findOne({
- where: { userId },
- attributes: ['id', 'userId', 'email', 'savePassword', 'autoUpdateRatings', 'lastLoginAttempt', 'lastLoginSuccess', 'lastUpdateRatings', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
+ where: { userId }
+ // Keine attributes-Limitierung, damit getter-Methoden für Verschlüsselung funktionieren
});
- return account;
+ if (!account) {
+ return null;
+ }
+ // Rückgabe mit automatischer Entschlüsselung durch Model-Getters
+ return {
+ id: account.id,
+ userId: account.userId,
+ email: account.email, // Automatisch entschlüsselt
+ savePassword: account.savePassword,
+ autoUpdateRatings: account.autoUpdateRatings,
+ lastLoginAttempt: account.lastLoginAttempt,
+ lastLoginSuccess: account.lastLoginSuccess,
+ lastUpdateRatings: account.lastUpdateRatings,
+ expiresAt: account.expiresAt,
+ userData: account.userData, // Automatisch entschlüsselt
+ clubId: account.clubId, // Automatisch entschlüsselt
+ clubName: account.clubName, // Automatisch entschlüsselt
+ fedNickname: account.fedNickname, // Automatisch entschlüsselt
+ createdAt: account.createdAt,
+ updatedAt: account.updatedAt
+ };
}
/**
diff --git a/backend/utils/logDataSanitizer.js b/backend/utils/logDataSanitizer.js
new file mode 100644
index 0000000..13f9169
--- /dev/null
+++ b/backend/utils/logDataSanitizer.js
@@ -0,0 +1,214 @@
+import { encryptData } from './encrypt.js';
+
+/**
+ * Utility-Funktionen zum Sanitizing von Log-Daten
+ * Entfernt oder verschlüsselt personenbezogene Daten aus Request/Response-Bodies
+ */
+
+// Felder, die personenbezogene Daten enthalten können
+const SENSITIVE_FIELDS = [
+ 'password',
+ 'passwort',
+ 'email',
+ 'eMail',
+ 'e-mail',
+ 'phone',
+ 'telefon',
+ 'telephone',
+ 'firstName',
+ 'first_name',
+ 'lastName',
+ 'last_name',
+ 'name',
+ 'street',
+ 'address',
+ 'adresse',
+ 'city',
+ 'stadt',
+ 'postalCode',
+ 'postal_code',
+ 'plz',
+ 'birthDate',
+ 'birth_date',
+ 'geburtstag',
+ 'token',
+ 'accessToken',
+ 'access_token',
+ 'refreshToken',
+ 'refresh_token',
+ 'cookie',
+ 'authcode',
+ 'authCode',
+ 'session',
+ 'sessionId',
+ 'session_id',
+ 'apiKey',
+ 'api_key',
+ 'secret',
+ 'credentials',
+ 'creditCard',
+ 'credit_card',
+ 'iban',
+ 'accountNumber',
+ 'account_number'
+];
+
+/**
+ * Entfernt oder maskiert personenbezogene Daten aus einem Objekt
+ * @param {Object|string} data - Das zu sanitizierende Objekt oder JSON-String
+ * @param {boolean} encrypt - Wenn true, werden sensible Felder verschlüsselt statt entfernt
+ * @returns {Object|string} - Das sanitizierte Objekt oder JSON-String
+ */
+export function sanitizeLogData(data, encrypt = false) {
+ if (!data) {
+ return null;
+ }
+
+ // Wenn es ein String ist, versuche es als JSON zu parsen
+ let obj;
+ let isString = false;
+
+ if (typeof data === 'string') {
+ isString = true;
+ try {
+ obj = JSON.parse(data);
+ } catch (e) {
+ // Wenn es kein JSON ist, kürze den String einfach
+ return data.length > 500 ? data.substring(0, 500) + '... (truncated)' : data;
+ }
+ } else if (typeof data === 'object') {
+ obj = data;
+ } else {
+ return data;
+ }
+
+ // Rekursiv durch das Objekt gehen
+ const sanitized = sanitizeObject(obj, encrypt);
+
+ // Wenn es ursprünglich ein String war, zurück zu String konvertieren
+ if (isString) {
+ try {
+ return JSON.stringify(sanitized);
+ } catch (e) {
+ return '[Unable to serialize sanitized data]';
+ }
+ }
+
+ return sanitized;
+}
+
+/**
+ * Sanitiziert ein Objekt rekursiv
+ */
+function sanitizeObject(obj, encrypt = false) {
+ if (obj === null || obj === undefined) {
+ return obj;
+ }
+
+ // Arrays
+ if (Array.isArray(obj)) {
+ return obj.map(item => sanitizeObject(item, encrypt));
+ }
+
+ // Objekte
+ if (typeof obj === 'object') {
+ const sanitized = {};
+
+ for (const [key, value] of Object.entries(obj)) {
+ const lowerKey = key.toLowerCase();
+
+ // Prüfe, ob das Feld sensibel ist
+ const isSensitive = SENSITIVE_FIELDS.some(field =>
+ lowerKey.includes(field.toLowerCase())
+ );
+
+ if (isSensitive) {
+ if (encrypt && value) {
+ // Verschlüssele den Wert vollständig
+ try {
+ const valueStr = typeof value === 'string' ? value : JSON.stringify(value);
+ const encrypted = encryptData(valueStr);
+ // Speichere vollständig verschlüsselt (kann bei Bedarf entschlüsselt werden)
+ sanitized[key] = encrypted;
+ } catch (e) {
+ // Bei Verschlüsselungsfehler: Maskiere stattdessen
+ sanitized[key] = '[REDACTED]';
+ }
+ } else {
+ // Entferne oder maskiere den Wert (ohne Verschlüsselung)
+ if (typeof value === 'string' && value.length > 0) {
+ // Zeige nur die ersten 2 Zeichen
+ sanitized[key] = value.substring(0, 2) + '***[REDACTED]';
+ } else {
+ sanitized[key] = '[REDACTED]';
+ }
+ }
+ } else if (typeof value === 'object') {
+ // Rekursiv für verschachtelte Objekte
+ sanitized[key] = sanitizeObject(value, encrypt);
+ } else {
+ sanitized[key] = value;
+ }
+ }
+
+ return sanitized;
+ }
+
+ // Primitive Werte
+ return obj;
+}
+
+/**
+ * Kürzt einen String auf eine maximale Länge
+ */
+export function truncateString(str, maxLength = 1000) {
+ if (!str || typeof str !== 'string') {
+ return str;
+ }
+
+ if (str.length <= maxLength) {
+ return str;
+ }
+
+ return str.substring(0, maxLength) + '... (truncated)';
+}
+
+/**
+ * Sanitiziert IP-Adressen (kürzt auf ersten 3 Oktetten)
+ */
+export function sanitizeIpAddress(ip) {
+ if (!ip) {
+ return null;
+ }
+
+ // IPv4: 192.168.1.100 -> 192.168.1.xxx
+ if (ip.includes('.')) {
+ const parts = ip.split('.');
+ if (parts.length === 4) {
+ return `${parts[0]}.${parts[1]}.${parts[2]}.xxx`;
+ }
+ }
+
+ // IPv6: Kürze auf ersten Teil
+ if (ip.includes(':')) {
+ const parts = ip.split(':');
+ return parts[0] + ':xxx';
+ }
+
+ return ip;
+}
+
+/**
+ * Sanitiziert User-Agent-Strings (entfernt spezifische Versionen)
+ */
+export function sanitizeUserAgent(userAgent) {
+ if (!userAgent) {
+ return null;
+ }
+
+ // Entferne spezifische Versionsnummern, behalte nur Browser/OS-Typ
+ return userAgent
+ .replace(/\d+\.\d+\.\d+\.\d+/g, 'x.x.x.x') // Versionsnummern
+ .substring(0, 200); // Kürze auf 200 Zeichen
+}
+
diff --git a/frontend/index.html b/frontend/index.html
index f91e211..6e7d44d 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -11,22 +11,22 @@
- Trainingstagebuch – Vereinsverwaltung, Trainingsplanung & Turniere
-
+ Trainingstagebuch – Umfassende Vereinsverwaltung, Trainingsplanung & Turnierorganisation
+
-
-
+
+
-
-
+
+
@@ -50,7 +50,19 @@
"name": "Trainingstagebuch",
"applicationCategory": "SportsApplication",
"operatingSystem": "Web",
- "description": "Mitgliederverwaltung, Trainingstagebuch, Spiel- und Turnierorganisation sowie Statistiken – DSGVO-freundlich und einfach.",
+ "description": "Umfassende Vereinsverwaltung mit Mitgliederverwaltung, Trainingsgruppen, Trainingszeiten, Trainingstagebuch, Turnierorganisation (intern, offen, offiziell), Team-Management, MyTischtennis-Integration, Statistiken und flexiblen Berechtigungssystemen – DSGVO‑konform und einfach zu bedienen.",
+ "featureList": [
+ "Mitgliederverwaltung",
+ "Trainingsgruppen & Trainingszeiten",
+ "Trainingstagebuch & Dokumentation",
+ "Turniere (intern, offen, offiziell)",
+ "Team-Management & Ligen",
+ "MyTischtennis-Integration",
+ "Statistiken & Auswertungen",
+ "Rollen & Berechtigungssystem",
+ "PDF-Export",
+ "Aktivitätsprotokoll"
+ ],
"offers": {
"@type": "Offer",
"price": "0",
diff --git a/frontend/src/views/Datenschutz.vue b/frontend/src/views/Datenschutz.vue
index a51f031..9bec716 100644
--- a/frontend/src/views/Datenschutz.vue
+++ b/frontend/src/views/Datenschutz.vue
@@ -28,7 +28,12 @@
Nutzungsdaten : IP-Adresse, Datum/Uhrzeit, abgerufene Inhalte, User-Agent (Server-Logfiles).
Registrierungs-/Profildaten : Benutzername, E-Mail-Adresse (und ggf. weitere durch den Nutzer bereitgestellte Angaben).
Vereins-/Aktivitätsdaten : Inhalte, die Nutzer im Rahmen der Anwendung anlegen (z. B. Mitglieder-/Trainingsdaten).
+ Mitgliederdaten : Name, Geburtsdatum, Adresse, Telefonnummer, E-Mail-Adresse (verschlüsselt gespeichert).
+ Trainingsdaten : Teilnahmen, Aktivitäten, Notizen zu Trainings.
+ Turnierdaten : Teilnahmen, Ergebnisse, Spielpläne.
+ MyTischtennis-Daten : E-Mail-Adresse, Zugriffstoken, Refresh-Token, Cookie, Benutzerdaten, Vereinsinformationen (bei Nutzung der MyTischtennis-Integration, alle verschlüsselt gespeichert).
Cookies/Local Storage : technisch notwendige Informationen (z. B. Session-/Auth-Token).
+ Logdaten : API-Requests, Aktivitäten, Fehlerprotokolle (können personenbezogene Daten enthalten).
@@ -37,35 +42,58 @@
Eine Weitergabe erfolgt nur, soweit dies zur Bereitstellung der Website und Funktionen notwendig ist (z. B. Hosting/Technik) oder eine rechtliche Verpflichtung besteht.
+
+ MyTischtennis.de-Integration: Bei Nutzung der optionalen MyTischtennis-Integration werden Daten an MyTischtennis.de (Deutscher Tischtennis-Bund e.V.) übertragen.
+ Die Datenübertragung erfolgt nur nach ausdrücklicher Einwilligung und dient der Synchronisation von Spielergebnissen und Statistiken.
+ Weitere Informationen zum Datenschutz bei MyTischtennis.de finden Sie in der Datenschutzerklärung von MyTischtennis.de .
+
5. Drittlandübermittlung
- Eine Übermittlung in Drittländer findet grundsätzlich nicht statt, es sei denn, dies ist zur Nutzung einzelner Dienste technisch erforderlich. In solchen Fällen wird auf geeignete Garantien geachtet.
+ Eine Übermittlung in Drittländer findet grundsätzlich nicht statt. Alle Daten werden in Deutschland bzw. innerhalb der EU verarbeitet und gespeichert.
+
+
+ MyTischtennis.de: Die MyTischtennis-Integration überträgt Daten an MyTischtennis.de (Deutscher Tischtennis-Bund e.V.),
+ welcher seinen Sitz in Deutschland hat. Eine Übermittlung in Drittländer erfolgt nicht.
6. Speicherdauer
- Personenbezogene Daten werden nur so lange gespeichert, wie es für die jeweiligen Zwecke erforderlich ist bzw. gesetzliche Aufbewahrungspflichten bestehen. Server-Logdaten werden in der Regel kurzfristig gelöscht.
+ Personenbezogene Daten werden nur so lange gespeichert, wie es für die jeweiligen Zwecke erforderlich ist bzw. gesetzliche Aufbewahrungspflichten bestehen.
+
+ Benutzerdaten: Bis zur Löschung des Accounts durch den Nutzer oder bei Inaktivität nach angemessener Frist.
+ Mitgliederdaten: Bis zur Löschung durch den Verein oder bei Austritt des Mitglieds (sofern keine gesetzlichen Aufbewahrungspflichten bestehen).
+ Logdaten: API-Logs werden für maximal 90 Tage gespeichert und anschließend automatisch gelöscht. Aktivitätslogs werden dauerhaft gespeichert, können aber auf Anfrage gelöscht werden.
+ MyTischtennis-Daten: Bis zur Löschung der Integration durch den Nutzer.
+ Server-Logdaten: Werden in der Regel kurzfristig (max. 7 Tage) gelöscht.
+
7. Rechte der betroffenen Personen
+ Sie haben folgende Rechte bezüglich Ihrer personenbezogenen Daten:
- Auskunft (Art. 15 DSGVO)
- Berichtigung (Art. 16 DSGVO)
- Löschung (Art. 17 DSGVO)
- Einschränkung (Art. 18 DSGVO)
- Datenübertragbarkeit (Art. 20 DSGVO)
- Widerspruch (Art. 21 DSGVO)
- Widerruf erteilter Einwilligungen (Art. 7 Abs. 3 DSGVO)
+ Auskunft (Art. 15 DSGVO): Sie können Auskunft über die zu Ihrer Person gespeicherten Daten verlangen.
+ Bitte kontaktieren Sie uns per E-Mail an tsschulz@tsschulz.de .
+ Berichtigung (Art. 16 DSGVO): Sie können die Berichtigung unrichtiger Daten verlangen.
+ Die meisten Daten können Sie direkt in der Anwendung bearbeiten.
+ Löschung (Art. 17 DSGVO): Sie können die Löschung Ihrer Daten verlangen, sofern keine gesetzlichen Aufbewahrungspflichten bestehen.
+ Bitte kontaktieren Sie uns per E-Mail an tsschulz@tsschulz.de .
+ Einschränkung (Art. 18 DSGVO): Sie können die Einschränkung der Verarbeitung Ihrer Daten verlangen.
+ Datenübertragbarkeit (Art. 20 DSGVO): Sie können Ihre Daten in einem strukturierten, gängigen und maschinenlesbaren Format erhalten.
+ Bitte kontaktieren Sie uns per E-Mail an tsschulz@tsschulz.de .
+ Widerspruch (Art. 21 DSGVO): Sie können der Verarbeitung Ihrer Daten widersprechen, soweit diese auf berechtigtem Interesse beruht.
+ Widerruf erteilter Einwilligungen (Art. 7 Abs. 3 DSGVO): Sie können erteilte Einwilligungen jederzeit widerrufen.
+ Die MyTischtennis-Integration kann in den Einstellungen deaktiviert werden.
- Zudem besteht ein Beschwerderecht bei einer Aufsichtsbehörde (Art. 77 DSGVO), z. B. beim HBDI in Hessen.
+ Zudem besteht ein Beschwerderecht bei einer Aufsichtsbehörde (Art. 77 DSGVO), z. B. beim
+ Hessischen Beauftragten für Datenschutz und Informationsfreiheit (HBDI) .
@@ -77,16 +105,57 @@
- 9. Cookies
+ 9. Cookies und lokale Speicherung
Es werden vorwiegend technisch notwendige Cookies bzw. Webspeicher (Local Storage/Session Storage) verwendet, um die Anmeldung und Sitzungen zu ermöglichen. Eine Nutzung findet ohne Tracking zu Werbezwecken statt.
+
+ Session Storage: Wird für temporäre Daten verwendet (z. B. Authentifizierungstoken, aktuelle Sitzung).
+
+
+ Local Storage: Wird für persistente Einstellungen verwendet (z. B. ausgewählter Verein, Berechtigungen, Sidebar-Status).
+
- 10. Stand
+ 10. Verschlüsselung und Sicherheit
- Diese Datenschutzerklärung ist aktuell und wird bei Bedarf angepasst.
+ Zum Schutz Ihrer Daten setzen wir folgende technische und organisatorische Maßnahmen ein:
+
+
+ Verschlüsselung: Sensible Daten werden mit AES-256-CBC verschlüsselt gespeichert:
+
+ Mitgliederdaten: Name, Adresse, Telefonnummer, E-Mail
+ MyTischtennis-Daten: E-Mail-Adresse, Zugriffstoken, Refresh-Token, Cookie, Benutzerdaten, Vereinsinformationen
+
+
+ HTTPS: Alle Verbindungen werden über HTTPS verschlüsselt übertragen.
+ Passwort-Hashing: Passwörter werden mit bcrypt gehasht und niemals im Klartext gespeichert.
+ Berechtigungssystem: Rollenbasierte Zugriffskontrolle gewährleistet, dass nur berechtigte Personen auf Daten zugreifen können.
+ Authentifizierung: Sichere Authentifizierung mit Token-basiertem System.
+
+
+
+
+ 11. Logging und Protokollierung
+
+ Zur Gewährleistung der Sicherheit und Stabilität der Anwendung werden folgende Daten protokolliert:
+
+
+ Aktivitätslogs: Wichtige Aktionen in der Anwendung werden protokolliert (z. B. Änderungen an Mitgliedern, Turnieren).
+ Server-Logs: Standard-Server-Logs für Fehlerbehebung und Sicherheit (IP-Adressen, Zugriffszeiten, Fehlermeldungen).
+
+
+ Die Logs werden ausschließlich für technische Zwecke (Fehlerbehebung, Sicherheit, Performance-Optimierung) verwendet und nicht an Dritte weitergegeben.
+
+
+
+
+ 12. Stand und Änderungen
+
+ Diese Datenschutzerklärung wurde zuletzt am 16. November 2024 aktualisiert.
+ Wir behalten uns vor, diese Datenschutzerklärung bei Bedarf anzupassen, um sie an geänderte Rechtslagen oder Funktionalitäten der Anwendung anzupassen.
+ Über wesentliche Änderungen werden wir Sie informieren.
diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
index d4b7955..087fbce 100644
--- a/frontend/src/views/Home.vue
+++ b/frontend/src/views/Home.vue
@@ -10,8 +10,9 @@
Vereinsverwaltung, Trainingsplanung und Turniere – alles an einem Ort
- Das TrainingsTagebuch hilft Vereinen und Trainerinnen/Trainern, Mitglieder zu verwalten, Trainings zu dokumentieren,
- Spielpläne zu organisieren und Ergebnisse auszuwerten – DSGVO‑konform und einfach zu bedienen.
+ Das TrainingsTagebuch ist die umfassende Lösung für Vereine: Mitgliederverwaltung, Trainingsgruppen, Trainingszeiten,
+ Trainingstagebuch, Turnierorganisation, Team-Management, MyTischtennis-Integration, Statistiken und mehr –
+ DSGVO‑konform und einfach zu bedienen.
@@ -25,9 +26,13 @@
✔️ Mitglieder- und Gruppenverwaltung
- ✔️ Trainings‑ und Turnierplanung
- ✔️ Trainingsstatistiken und Auswertungen
- ✔️ Rollen, Freigaben und sichere Zugriffe
+ ✔️ Trainingsgruppen & Trainingszeiten
+ ✔️ Trainingstagebuch & Dokumentation
+ ✔️ Turniere (intern, offen, offiziell)
+ ✔️ Team-Management & Ligen
+ ✔️ MyTischtennis-Integration
+ ✔️ Statistiken & Auswertungen
+ ✔️ Rollen, Berechtigungen & DSGVO
@@ -81,6 +86,62 @@
Nutze Vorlagen für wiederkehrende Übungen und beschleunige deine Dokumentation.
+
+
+
👥
+
Trainingsgruppen & Zeiten
+
+ Organisiere Trainingsgruppen, definiere Trainingszeiten und verwalte Gruppenzuordnungen.
+
+
+
+
+
🏅
+
Offizielle Turniere
+
+ Importiere und verwalte offizielle Turniere, verwalte Teilnahmen und Ergebnisse.
+
+
+
+
+
👔
+
Team-Management
+
+ Verwalte Mannschaften, Ligen, Spielpläne und Ergebnisse für deinen Verein.
+
+
+
+
+
🔗
+
MyTischtennis-Integration
+
+ Automatische Synchronisation mit MyTischtennis.de für Spielergebnisse und Statistiken.
+
+
+
+
+
📄
+
PDF-Export
+
+ Exportiere Trainingstage als PDF mit Teilnehmern, Aktivitäten und Statistiken.
+
+
+
+
+
🔐
+
Berechtigungssystem
+
+ Rollenbasierte Zugriffe (Admin, Trainer, Mannschaftsführer, Mitglied) mit individuellen Berechtigungen.
+
+
+
+
+
📋
+
Aktivitätsprotokoll
+
+ Vollständiges Logging aller Aktionen für Transparenz und Nachvollziehbarkeit.
+
+
@@ -108,11 +169,14 @@
Für wen ist das TrainingsTagebuch?
- Das TrainingsTagebuch ist die zentrale Plattform für Vereine, Abteilungen und Trainerteams.
- Es vereint Mitgliederverwaltung, Trainingsplanung, Spiel‑ und Turnierorganisation sowie aussagekräftige
- Statistiken in einer modernen Web‑Anwendung. Durch klare Rollen und Freigaben behalten Verantwortliche die
- Kontrolle, während Mitglieder selbstbestimmt mitwirken können. Ideal für Mannschafts‑, Racket‑ und
- Individualsportarten – vom Nachwuchs bis zum Leistungsbereich.
+ Das TrainingsTagebuch ist die umfassende Plattform für Vereine, Abteilungen und Trainerteams.
+ Es vereint Mitgliederverwaltung mit Trainingsgruppen und -zeiten, detailliertes Trainingstagebuch,
+ umfassende Turnierorganisation (interne, offene und offizielle Turniere), Team-Management mit Liga-Integration,
+ MyTischtennis-Synchronisation, aussagekräftige Statistiken und Auswertungen sowie ein flexibles
+ Berechtigungssystem in einer modernen Web‑Anwendung. Durch klare Rollen (Admin, Trainer, Mannschaftsführer, Mitglied)
+ und individuelle Berechtigungen behalten Verantwortliche die Kontrolle, während Mitglieder selbstbestimmt mitwirken können.
+ Ideal für Mannschafts‑, Racket‑ und Individualsportarten – vom Nachwuchs bis zum Leistungsbereich.
+ DSGVO‑konform mit transparenten Freigaben und vollständigem Aktivitätsprotokoll.
@@ -124,11 +188,27 @@
Wie steht es um den Datenschutz?
- Wir setzen auf Datensparsamkeit, transparente Freigaben und rollenbasierte Zugriffe.
+ Wir setzen auf Datensparsamkeit, transparente Freigaben, rollenbasierte Zugriffe und vollständiges Aktivitätsprotokoll. Die Anwendung ist DSGVO‑konform.
Benötige ich eine Installation?
- Nein, es handelt sich um eine Web‑Anwendung. Du nutzt sie direkt im Browser.
+ Nein, es handelt sich um eine Web‑Anwendung. Du nutzt sie direkt im Browser – auf Desktop, Tablet und Smartphone.
+
+
+ Welche Turnierarten werden unterstützt?
+ Du kannst interne Turniere, offene Turniere und offizielle Turniere (z.B. von Verbänden) verwalten. Offizielle Turniere können importiert werden.
+
+
+ Funktioniert die MyTischtennis-Integration automatisch?
+ Ja, nach der Einrichtung synchronisiert sich die Anwendung automatisch mit MyTischtennis.de und importiert Spielergebnisse und Statistiken.
+
+
+ Kann ich Trainingsgruppen und -zeiten verwalten?
+ Ja, du kannst Trainingsgruppen anlegen, Trainingszeiten definieren und Mitglieder den Gruppen zuordnen. Das Trainingstagebuch schlägt automatisch passende Gruppen und Zeiten vor.
+
+
+ Wie funktioniert das Berechtigungssystem?
+ Es gibt vier Rollen: Admin, Trainer, Mannschaftsführer und Mitglied. Jede Rolle hat spezifische Berechtigungen, die individuell angepasst werden können.
@@ -169,23 +249,23 @@
👥
Mitglieder verwalten
- Verwalte deine Vereinsmitglieder, erstelle Gruppen und behalte den Überblick über alle Teilnehmer.
+ Verwalte deine Vereinsmitglieder, erstelle Trainingsgruppen und behalte den Überblick über alle Teilnehmer.
📝
-
Tagebuch führen
+
Trainingstagebuch führen
- Dokumentiere deine Trainingsaktivitäten, Notizen und wichtige Ereignisse im Verein.
+ Dokumentiere Trainingsaktivitäten, Teilnehmer, Aktivitäten und Notizen für jeden Trainingstag.
-
📅
-
Spielpläne organisieren
+
👥
+
Trainingsgruppen & Zeiten
- Plane und organisiere Spiele, Turniere und andere Veranstaltungen für deinen Verein.
+ Organisiere Trainingsgruppen, definiere Trainingszeiten und verwalte Gruppenzuordnungen.
@@ -193,7 +273,39 @@
🏆
Turniere verwalten
- Erstelle und verwalte Turniere, Gruppen und Ergebnisse für deine Vereinsaktivitäten.
+ Erstelle interne und offene Turniere, importiere offizielle Turniere und verwalte Teilnahmen.
+
+
+
+
+
👔
+
Team-Management
+
+ Verwalte Mannschaften, Ligen, Spielpläne und Ergebnisse für deinen Verein.
+
+
+
+
+
🔗
+
MyTischtennis-Integration
+
+ Synchronisiere automatisch Spielergebnisse und Statistiken mit MyTischtennis.de.
+
+
+
+
+
📊
+
Statistiken & Auswertungen
+
+ Erhalte detaillierte Trainings- und Teilnahmeübersichten sowie Aktivitätsstatistiken.
+
+
+
+
+
📄
+
PDF-Export
+
+ Exportiere Trainingstage als PDF mit Teilnehmern, Aktivitäten und Statistiken.