Implement 301 redirects for www to non-www and enhance canonical tag handling
This commit adds 301 redirects in the Apache configuration to redirect traffic from www.tt-tagebuch.de to tt-tagebuch.de for both HTTP and HTTPS. Additionally, it introduces middleware in the backend to dynamically set canonical tags based on the request URL, ensuring proper SEO practices. The request logging middleware has been disabled, and sensitive data handling has been improved in the MyTischtennis model and API logging service, ensuring compliance with data protection regulations. Frontend updates include enhanced descriptions and features in the application, improving user experience and clarity.
This commit is contained in:
342
DSGVO_CHECKLIST.md
Normal file
342
DSGVO_CHECKLIST.md
Normal file
@@ -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
|
||||
|
||||
@@ -8,9 +8,29 @@
|
||||
# sudo a2enmod headers
|
||||
# sudo systemctl restart apache2
|
||||
|
||||
# 301-Weiterleitung von www auf non-www (HTTP)
|
||||
<VirtualHost *:80>
|
||||
ServerName www.tt-tagebuch.de
|
||||
Redirect permanent / https://tt-tagebuch.de/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:80>
|
||||
ServerName tt-tagebuch.de
|
||||
Redirect permanent / https://tt-tagebuch.de/
|
||||
</VirtualHost>
|
||||
|
||||
# 301-Weiterleitung von www auf non-www (HTTPS)
|
||||
<VirtualHost *:443>
|
||||
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/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName tt-tagebuch.de
|
||||
ServerAlias www.tt-tagebuch.de
|
||||
|
||||
DocumentRoot /var/www/tt-tagebuch.de
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
169
backend/scripts/migrateMyTischtennisEncryption.js
Normal file
169
backend/scripts/migrateMyTischtennisEncryption.js
Normal file
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
/<link rel="canonical" href="[^"]*" \/>/,
|
||||
`<link rel="canonical" href="${canonicalUrl}" />`
|
||||
);
|
||||
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.send(updatedData);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
app.use(setCanonicalTag);
|
||||
app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
|
||||
@@ -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
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
214
backend/utils/logDataSanitizer.js
Normal file
214
backend/utils/logDataSanitizer.js
Normal file
@@ -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
|
||||
}
|
||||
|
||||
@@ -11,22 +11,22 @@
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<title>Trainingstagebuch – Vereinsverwaltung, Trainingsplanung & Turniere</title>
|
||||
<meta name="description" content="Das TrainingsTagebuch hilft Vereinen und Trainer:innen, Mitglieder zu verwalten, Trainings zu dokumentieren, Spielpläne zu organisieren und Statistiken auszuwerten – alles in einer modernen Web‑App." />
|
||||
<title>Trainingstagebuch – Umfassende Vereinsverwaltung, Trainingsplanung & Turnierorganisation</title>
|
||||
<meta name="description" content="Das TrainingsTagebuch ist die umfassende Lösung für Vereine: Mitgliederverwaltung, Trainingsgruppen, Trainingszeiten, Trainingstagebuch, Turniere (intern, offen, offiziell), Team-Management, MyTischtennis-Integration, Statistiken, Berechtigungssystem – DSGVO‑konform und einfach zu bedienen." />
|
||||
<link rel="canonical" href="https://tt-tagebuch.de/" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="Trainingstagebuch" />
|
||||
<meta property="og:title" content="Trainingstagebuch – Vereinsverwaltung, Trainingsplanung & Turniere" />
|
||||
<meta property="og:description" content="Mitgliederverwaltung, Trainingstagebuch, Spiel‑ und Turnierorganisation sowie Statistiken – DSGVO‑freundlich und einfach." />
|
||||
<meta property="og:title" content="Trainingstagebuch – Umfassende Vereinsverwaltung, Trainingsplanung & Turnierorganisation" />
|
||||
<meta property="og:description" content="Mitgliederverwaltung, Trainingsgruppen, Trainingszeiten, Trainingstagebuch, Turniere (intern, offen, offiziell), Team-Management, MyTischtennis-Integration, Statistiken, Berechtigungssystem – DSGVO‑konform und einfach." />
|
||||
<meta property="og:url" content="https://tt-tagebuch.de/" />
|
||||
<meta property="og:image" content="https://tt-tagebuch.de/android-chrome-512x512.png" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Trainingstagebuch – Vereinsverwaltung, Trainingsplanung & Turniere" />
|
||||
<meta name="twitter:description" content="Mitgliederverwaltung, Trainingstagebuch, Spiel‑ und Turnierorganisation sowie Statistiken – DSGVO‑freundlich und einfach." />
|
||||
<meta name="twitter:title" content="Trainingstagebuch – Umfassende Vereinsverwaltung, Trainingsplanung & Turnierorganisation" />
|
||||
<meta name="twitter:description" content="Mitgliederverwaltung, Trainingsgruppen, Trainingszeiten, Trainingstagebuch, Turniere (intern, offen, offiziell), Team-Management, MyTischtennis-Integration, Statistiken, Berechtigungssystem – DSGVO‑konform und einfach." />
|
||||
<meta name="twitter:image" content="https://tt-tagebuch.de/android-chrome-512x512.png" />
|
||||
|
||||
<!-- JSON-LD: Website + Organization -->
|
||||
@@ -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",
|
||||
|
||||
@@ -28,7 +28,12 @@
|
||||
<li><strong>Nutzungsdaten</strong>: IP-Adresse, Datum/Uhrzeit, abgerufene Inhalte, User-Agent (Server-Logfiles).</li>
|
||||
<li><strong>Registrierungs-/Profildaten</strong>: Benutzername, E-Mail-Adresse (und ggf. weitere durch den Nutzer bereitgestellte Angaben).</li>
|
||||
<li><strong>Vereins-/Aktivitätsdaten</strong>: Inhalte, die Nutzer im Rahmen der Anwendung anlegen (z. B. Mitglieder-/Trainingsdaten).</li>
|
||||
<li><strong>Mitgliederdaten</strong>: Name, Geburtsdatum, Adresse, Telefonnummer, E-Mail-Adresse (verschlüsselt gespeichert).</li>
|
||||
<li><strong>Trainingsdaten</strong>: Teilnahmen, Aktivitäten, Notizen zu Trainings.</li>
|
||||
<li><strong>Turnierdaten</strong>: Teilnahmen, Ergebnisse, Spielpläne.</li>
|
||||
<li><strong>MyTischtennis-Daten</strong>: E-Mail-Adresse, Zugriffstoken, Refresh-Token, Cookie, Benutzerdaten, Vereinsinformationen (bei Nutzung der MyTischtennis-Integration, alle verschlüsselt gespeichert).</li>
|
||||
<li><strong>Cookies/Local Storage</strong>: technisch notwendige Informationen (z. B. Session-/Auth-Token).</li>
|
||||
<li><strong>Logdaten</strong>: API-Requests, Aktivitäten, Fehlerprotokolle (können personenbezogene Daten enthalten).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -37,35 +42,58 @@
|
||||
<p>
|
||||
Eine Weitergabe erfolgt nur, soweit dies zur Bereitstellung der Website und Funktionen notwendig ist (z. B. Hosting/Technik) oder eine rechtliche Verpflichtung besteht.
|
||||
</p>
|
||||
<p>
|
||||
<strong>MyTischtennis.de-Integration:</strong> 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 <a href="https://www.mytischtennis.de/datenschutz" target="_blank" rel="noopener noreferrer">Datenschutzerklärung von MyTischtennis.de</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Drittlandübermittlung</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
<strong>MyTischtennis.de:</strong> 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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Speicherdauer</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Benutzerdaten:</strong> Bis zur Löschung des Accounts durch den Nutzer oder bei Inaktivität nach angemessener Frist.</li>
|
||||
<li><strong>Mitgliederdaten:</strong> Bis zur Löschung durch den Verein oder bei Austritt des Mitglieds (sofern keine gesetzlichen Aufbewahrungspflichten bestehen).</li>
|
||||
<li><strong>Logdaten:</strong> 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.</li>
|
||||
<li><strong>MyTischtennis-Daten:</strong> Bis zur Löschung der Integration durch den Nutzer.</li>
|
||||
<li><strong>Server-Logdaten:</strong> Werden in der Regel kurzfristig (max. 7 Tage) gelöscht.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>7. Rechte der betroffenen Personen</h2>
|
||||
<p>Sie haben folgende Rechte bezüglich Ihrer personenbezogenen Daten:</p>
|
||||
<ul>
|
||||
<li>Auskunft (Art. 15 DSGVO)</li>
|
||||
<li>Berichtigung (Art. 16 DSGVO)</li>
|
||||
<li>Löschung (Art. 17 DSGVO)</li>
|
||||
<li>Einschränkung (Art. 18 DSGVO)</li>
|
||||
<li>Datenübertragbarkeit (Art. 20 DSGVO)</li>
|
||||
<li>Widerspruch (Art. 21 DSGVO)</li>
|
||||
<li>Widerruf erteilter Einwilligungen (Art. 7 Abs. 3 DSGVO)</li>
|
||||
<li><strong>Auskunft (Art. 15 DSGVO):</strong> Sie können Auskunft über die zu Ihrer Person gespeicherten Daten verlangen.
|
||||
Bitte kontaktieren Sie uns per E-Mail an <a href="mailto:tsschulz@tsschulz.de">tsschulz@tsschulz.de</a>.</li>
|
||||
<li><strong>Berichtigung (Art. 16 DSGVO):</strong> Sie können die Berichtigung unrichtiger Daten verlangen.
|
||||
Die meisten Daten können Sie direkt in der Anwendung bearbeiten.</li>
|
||||
<li><strong>Löschung (Art. 17 DSGVO):</strong> Sie können die Löschung Ihrer Daten verlangen, sofern keine gesetzlichen Aufbewahrungspflichten bestehen.
|
||||
Bitte kontaktieren Sie uns per E-Mail an <a href="mailto:tsschulz@tsschulz.de">tsschulz@tsschulz.de</a>.</li>
|
||||
<li><strong>Einschränkung (Art. 18 DSGVO):</strong> Sie können die Einschränkung der Verarbeitung Ihrer Daten verlangen.</li>
|
||||
<li><strong>Datenübertragbarkeit (Art. 20 DSGVO):</strong> Sie können Ihre Daten in einem strukturierten, gängigen und maschinenlesbaren Format erhalten.
|
||||
Bitte kontaktieren Sie uns per E-Mail an <a href="mailto:tsschulz@tsschulz.de">tsschulz@tsschulz.de</a>.</li>
|
||||
<li><strong>Widerspruch (Art. 21 DSGVO):</strong> Sie können der Verarbeitung Ihrer Daten widersprechen, soweit diese auf berechtigtem Interesse beruht.</li>
|
||||
<li><strong>Widerruf erteilter Einwilligungen (Art. 7 Abs. 3 DSGVO):</strong> Sie können erteilte Einwilligungen jederzeit widerrufen.
|
||||
Die MyTischtennis-Integration kann in den Einstellungen deaktiviert werden.</li>
|
||||
</ul>
|
||||
<p>
|
||||
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
|
||||
<a href="https://datenschutz.hessen.de/" target="_blank" rel="noopener noreferrer">Hessischen Beauftragten für Datenschutz und Informationsfreiheit (HBDI)</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -77,16 +105,57 @@
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. Cookies</h2>
|
||||
<h2>9. Cookies und lokale Speicherung</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Session Storage:</strong> Wird für temporäre Daten verwendet (z. B. Authentifizierungstoken, aktuelle Sitzung).
|
||||
</p>
|
||||
<p>
|
||||
<strong>Local Storage:</strong> Wird für persistente Einstellungen verwendet (z. B. ausgewählter Verein, Berechtigungen, Sidebar-Status).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>10. Stand</h2>
|
||||
<h2>10. Verschlüsselung und Sicherheit</h2>
|
||||
<p>
|
||||
Diese Datenschutzerklärung ist aktuell und wird bei Bedarf angepasst.
|
||||
Zum Schutz Ihrer Daten setzen wir folgende technische und organisatorische Maßnahmen ein:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Verschlüsselung:</strong> Sensible Daten werden mit AES-256-CBC verschlüsselt gespeichert:
|
||||
<ul>
|
||||
<li>Mitgliederdaten: Name, Adresse, Telefonnummer, E-Mail</li>
|
||||
<li>MyTischtennis-Daten: E-Mail-Adresse, Zugriffstoken, Refresh-Token, Cookie, Benutzerdaten, Vereinsinformationen</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>HTTPS:</strong> Alle Verbindungen werden über HTTPS verschlüsselt übertragen.</li>
|
||||
<li><strong>Passwort-Hashing:</strong> Passwörter werden mit bcrypt gehasht und niemals im Klartext gespeichert.</li>
|
||||
<li><strong>Berechtigungssystem:</strong> Rollenbasierte Zugriffskontrolle gewährleistet, dass nur berechtigte Personen auf Daten zugreifen können.</li>
|
||||
<li><strong>Authentifizierung:</strong> Sichere Authentifizierung mit Token-basiertem System.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>11. Logging und Protokollierung</h2>
|
||||
<p>
|
||||
Zur Gewährleistung der Sicherheit und Stabilität der Anwendung werden folgende Daten protokolliert:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Aktivitätslogs:</strong> Wichtige Aktionen in der Anwendung werden protokolliert (z. B. Änderungen an Mitgliedern, Turnieren).</li>
|
||||
<li><strong>Server-Logs:</strong> Standard-Server-Logs für Fehlerbehebung und Sicherheit (IP-Adressen, Zugriffszeiten, Fehlermeldungen).</li>
|
||||
</ul>
|
||||
<p>
|
||||
Die Logs werden ausschließlich für technische Zwecke (Fehlerbehebung, Sicherheit, Performance-Optimierung) verwendet und nicht an Dritte weitergegeben.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>12. Stand und Änderungen</h2>
|
||||
<p>
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
<section class="hero">
|
||||
<h1 class="hero-title">Vereinsverwaltung, Trainingsplanung und Turniere – alles an einem Ort</h1>
|
||||
<p class="hero-subtitle">
|
||||
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.
|
||||
</p>
|
||||
<div class="auth-actions">
|
||||
<router-link to="/register" class="btn-primary">
|
||||
@@ -25,9 +26,13 @@
|
||||
</div>
|
||||
<ul class="hero-bullets">
|
||||
<li>✔️ Mitglieder- und Gruppenverwaltung</li>
|
||||
<li>✔️ Trainings‑ und Turnierplanung</li>
|
||||
<li>✔️ Trainingsstatistiken und Auswertungen</li>
|
||||
<li>✔️ Rollen, Freigaben und sichere Zugriffe</li>
|
||||
<li>✔️ Trainingsgruppen & Trainingszeiten</li>
|
||||
<li>✔️ Trainingstagebuch & Dokumentation</li>
|
||||
<li>✔️ Turniere (intern, offen, offiziell)</li>
|
||||
<li>✔️ Team-Management & Ligen</li>
|
||||
<li>✔️ MyTischtennis-Integration</li>
|
||||
<li>✔️ Statistiken & Auswertungen</li>
|
||||
<li>✔️ Rollen, Berechtigungen & DSGVO</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -81,6 +86,62 @@
|
||||
Nutze Vorlagen für wiederkehrende Übungen und beschleunige deine Dokumentation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Trainingsgruppen & Zeiten</h4>
|
||||
<p class="feature-description">
|
||||
Organisiere Trainingsgruppen, definiere Trainingszeiten und verwalte Gruppenzuordnungen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🏅</div>
|
||||
<h4 class="feature-title">Offizielle Turniere</h4>
|
||||
<p class="feature-description">
|
||||
Importiere und verwalte offizielle Turniere, verwalte Teilnahmen und Ergebnisse.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👔</div>
|
||||
<h4 class="feature-title">Team-Management</h4>
|
||||
<p class="feature-description">
|
||||
Verwalte Mannschaften, Ligen, Spielpläne und Ergebnisse für deinen Verein.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h4 class="feature-title">MyTischtennis-Integration</h4>
|
||||
<p class="feature-description">
|
||||
Automatische Synchronisation mit MyTischtennis.de für Spielergebnisse und Statistiken.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📄</div>
|
||||
<h4 class="feature-title">PDF-Export</h4>
|
||||
<p class="feature-description">
|
||||
Exportiere Trainingstage als PDF mit Teilnehmern, Aktivitäten und Statistiken.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🔐</div>
|
||||
<h4 class="feature-title">Berechtigungssystem</h4>
|
||||
<p class="feature-description">
|
||||
Rollenbasierte Zugriffe (Admin, Trainer, Mannschaftsführer, Mitglied) mit individuellen Berechtigungen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📋</div>
|
||||
<h4 class="feature-title">Aktivitätsprotokoll</h4>
|
||||
<p class="feature-description">
|
||||
Vollständiges Logging aller Aktionen für Transparenz und Nachvollziehbarkeit.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -108,11 +169,14 @@
|
||||
<section class="seo-copy">
|
||||
<h3 class="section-title">Für wen ist das TrainingsTagebuch?</h3>
|
||||
<p class="long-text">
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -124,11 +188,27 @@
|
||||
</details>
|
||||
<details>
|
||||
<summary>Wie steht es um den Datenschutz?</summary>
|
||||
<p>Wir setzen auf Datensparsamkeit, transparente Freigaben und rollenbasierte Zugriffe.</p>
|
||||
<p>Wir setzen auf Datensparsamkeit, transparente Freigaben, rollenbasierte Zugriffe und vollständiges Aktivitätsprotokoll. Die Anwendung ist DSGVO‑konform.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Benötige ich eine Installation?</summary>
|
||||
<p>Nein, es handelt sich um eine Web‑Anwendung. Du nutzt sie direkt im Browser.</p>
|
||||
<p>Nein, es handelt sich um eine Web‑Anwendung. Du nutzt sie direkt im Browser – auf Desktop, Tablet und Smartphone.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Welche Turnierarten werden unterstützt?</summary>
|
||||
<p>Du kannst interne Turniere, offene Turniere und offizielle Turniere (z.B. von Verbänden) verwalten. Offizielle Turniere können importiert werden.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Funktioniert die MyTischtennis-Integration automatisch?</summary>
|
||||
<p>Ja, nach der Einrichtung synchronisiert sich die Anwendung automatisch mit MyTischtennis.de und importiert Spielergebnisse und Statistiken.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Kann ich Trainingsgruppen und -zeiten verwalten?</summary>
|
||||
<p>Ja, du kannst Trainingsgruppen anlegen, Trainingszeiten definieren und Mitglieder den Gruppen zuordnen. Das Trainingstagebuch schlägt automatisch passende Gruppen und Zeiten vor.</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Wie funktioniert das Berechtigungssystem?</summary>
|
||||
<p>Es gibt vier Rollen: Admin, Trainer, Mannschaftsführer und Mitglied. Jede Rolle hat spezifische Berechtigungen, die individuell angepasst werden können.</p>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
@@ -169,23 +249,23 @@
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Mitglieder verwalten</h4>
|
||||
<p class="feature-description">
|
||||
Verwalte deine Vereinsmitglieder, erstelle Gruppen und behalte den Überblick über alle Teilnehmer.
|
||||
Verwalte deine Vereinsmitglieder, erstelle Trainingsgruppen und behalte den Überblick über alle Teilnehmer.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📝</div>
|
||||
<h4 class="feature-title">Tagebuch führen</h4>
|
||||
<h4 class="feature-title">Trainingstagebuch führen</h4>
|
||||
<p class="feature-description">
|
||||
Dokumentiere deine Trainingsaktivitäten, Notizen und wichtige Ereignisse im Verein.
|
||||
Dokumentiere Trainingsaktivitäten, Teilnehmer, Aktivitäten und Notizen für jeden Trainingstag.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📅</div>
|
||||
<h4 class="feature-title">Spielpläne organisieren</h4>
|
||||
<div class="feature-icon">👥</div>
|
||||
<h4 class="feature-title">Trainingsgruppen & Zeiten</h4>
|
||||
<p class="feature-description">
|
||||
Plane und organisiere Spiele, Turniere und andere Veranstaltungen für deinen Verein.
|
||||
Organisiere Trainingsgruppen, definiere Trainingszeiten und verwalte Gruppenzuordnungen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -193,7 +273,39 @@
|
||||
<div class="feature-icon">🏆</div>
|
||||
<h4 class="feature-title">Turniere verwalten</h4>
|
||||
<p class="feature-description">
|
||||
Erstelle und verwalte Turniere, Gruppen und Ergebnisse für deine Vereinsaktivitäten.
|
||||
Erstelle interne und offene Turniere, importiere offizielle Turniere und verwalte Teilnahmen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">👔</div>
|
||||
<h4 class="feature-title">Team-Management</h4>
|
||||
<p class="feature-description">
|
||||
Verwalte Mannschaften, Ligen, Spielpläne und Ergebnisse für deinen Verein.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h4 class="feature-title">MyTischtennis-Integration</h4>
|
||||
<p class="feature-description">
|
||||
Synchronisiere automatisch Spielergebnisse und Statistiken mit MyTischtennis.de.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📊</div>
|
||||
<h4 class="feature-title">Statistiken & Auswertungen</h4>
|
||||
<p class="feature-description">
|
||||
Erhalte detaillierte Trainings- und Teilnahmeübersichten sowie Aktivitätsstatistiken.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card card">
|
||||
<div class="feature-icon">📄</div>
|
||||
<h4 class="feature-title">PDF-Export</h4>
|
||||
<p class="feature-description">
|
||||
Exportiere Trainingstage als PDF mit Teilnehmern, Aktivitäten und Statistiken.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user