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.

    +

    7. Rechte der betroffenen Personen

    +

    Sie haben folgende Rechte bezüglich Ihrer personenbezogenen Daten:

    - 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: +

    + +
    + +
    +

    11. Logging und Protokollierung

    +

    + Zur Gewährleistung der Sicherheit und Stabilität der Anwendung werden folgende Daten protokolliert: +

    + +

    + 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 @@
    @@ -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.