Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 438029a3a4 | |||
| c58491c97a | |||
| 1d9b9dbc45 | |||
| dc791dc33d | |||
| 57fbbff353 | |||
| b00a35af30 | |||
|
|
dd0f29124c | ||
|
|
dc084806ab | ||
|
|
4b4c48a50f | ||
|
|
65acc9e0d5 | ||
|
|
13cd55c051 | ||
|
|
9bf37399d5 | ||
|
|
047b1801b3 | ||
|
|
945ec0d48c | ||
|
|
e83bc250a8 | ||
|
|
0c28b12978 | ||
|
|
5aa11151cf | ||
|
|
a651113dee | ||
|
|
bf0d5b0935 | ||
|
|
6acdcfa5c3 | ||
|
|
dc2c60cefe | ||
|
|
bdbbb88be9 | ||
|
|
e6146b8f5a | ||
|
|
f7a799ea7f | ||
|
|
b74cb30cf6 | ||
|
|
0d2dfd9a07 | ||
|
|
61e5efadb8 | ||
|
|
88d050392f | ||
|
|
08b0be78ad | ||
|
|
b0e610f3ab | ||
|
|
0285c05fa6 | ||
|
|
5d4f2ebd4b | ||
|
|
bfa908ac9a | ||
|
|
9592459348 | ||
|
|
47f53ee3fd | ||
|
|
c22f4016cc | ||
|
|
2458ba2d37 | ||
|
|
6eb42812fd | ||
|
|
938ce4d991 | ||
|
|
cb6e84945b | ||
|
|
8c6be234c6 | ||
|
|
fe160420c1 | ||
|
|
167e3ba3ec | ||
|
|
9455b5d65a | ||
|
|
e6627a897e | ||
|
|
71fc85427b | ||
|
|
76597a4360 | ||
|
|
4f9761efb0 | ||
|
|
51e47cf9f9 | ||
|
|
0525f7908d | ||
|
|
a4d89374b7 | ||
|
|
de907df092 | ||
|
|
b906ac64b3 | ||
|
|
b7bbb92f86 | ||
|
|
6896484e9e | ||
|
|
9cc9db3a5a | ||
|
|
1c99fb30a1 | ||
|
|
2782661206 | ||
|
|
d10b663dc1 | ||
|
|
9baa6bae01 | ||
|
|
945fd85e39 | ||
|
|
5b04ed7904 | ||
|
|
de36a8ce2b | ||
|
|
903b036a63 | ||
|
|
5f3b6200ec | ||
|
|
eff211856f | ||
|
|
a81c3453b5 | ||
|
|
56c708d3a0 | ||
|
|
062bddcf52 | ||
|
|
4f98c782f3 | ||
|
|
3ea2907d08 | ||
|
|
ba5d6b14a8 | ||
|
|
004a94404a | ||
|
|
5ddf998672 | ||
|
|
baf5bda6f2 | ||
|
|
572de5f7d4 | ||
|
|
37893474b1 | ||
|
|
f437747664 | ||
|
|
22e9750e5d | ||
|
|
bd95f77131 | ||
|
|
bbdc923950 | ||
|
|
3e5ddd8a05 | ||
|
|
f4e5cf2edb | ||
|
|
44dba70aac | ||
|
|
7698d87ba0 | ||
|
|
201d5e9214 | ||
|
|
c21544d9b6 | ||
|
|
6167116630 | ||
|
|
1bb5f61b57 | ||
|
|
1535c8795b | ||
|
|
cb2d7d3936 | ||
|
|
5b4a5ba501 | ||
|
|
90b5f8d63d | ||
|
|
1ff3d9d1a6 | ||
|
|
df6fb23132 | ||
|
|
1e86b821e8 | ||
|
|
5923ef8bba | ||
|
|
cd8f40aa9d | ||
|
|
d392ccddd5 | ||
|
|
4a83e5c159 | ||
|
|
911c07e522 | ||
|
|
cd89c68a69 | ||
|
|
f1321b18bb | ||
|
|
54ce09e9a9 | ||
|
|
7a9e856961 | ||
|
|
fd4b47327f | ||
|
|
3a26f10110 | ||
|
|
ce2bda37ac | ||
|
|
5dda346fd7 | ||
|
|
28c92b66af | ||
|
|
d08835e206 | ||
|
|
3334d76688 | ||
|
|
d48cc4385f | ||
|
|
9b8dcd8561 | ||
|
|
2b06a8dd10 | ||
|
|
58e773e51e | ||
|
|
8d17cad299 | ||
|
|
156f4d6921 | ||
|
|
e27a4d960d | ||
|
|
c589c11607 | ||
|
|
0caa31e3eb | ||
|
|
fff5d404f5 | ||
|
|
7aff827711 |
0
.cursor/commands/club-settings.md
Normal file
0
.cursor/commands/club-settings.md
Normal file
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,4 +7,5 @@ backend/.env
|
||||
|
||||
backend/images/*
|
||||
backend/backend-debug.log
|
||||
backend/*.log
|
||||
backend/*.log
|
||||
backend/.env.local
|
||||
|
||||
86
CHECK_SERVER.md
Normal file
86
CHECK_SERVER.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Server-Prüfung: i18n-Fixes
|
||||
|
||||
## Lokale Prüfung (bereits durchgeführt)
|
||||
|
||||
✅ Alle Dateien sind lokal korrekt:
|
||||
- `TeamManagementView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
|
||||
- `PermissionsView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
|
||||
- `LogsView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
|
||||
- `SeasonSelector.vue` - Bereits korrekt
|
||||
|
||||
## Server-Prüfung
|
||||
|
||||
### 1. Prüfskript auf den Server kopieren
|
||||
|
||||
```bash
|
||||
# Vom lokalen Rechner aus:
|
||||
scp check-i18n-fixes.sh rv2756:/var/www/tt-tagebuch.de/
|
||||
```
|
||||
|
||||
### 2. Auf dem Server ausführen
|
||||
|
||||
```bash
|
||||
# Auf dem Server:
|
||||
cd /var/www/tt-tagebuch.de
|
||||
chmod +x check-i18n-fixes.sh
|
||||
./check-i18n-fixes.sh
|
||||
```
|
||||
|
||||
### 3. Falls Dateien nicht aktualisiert sind
|
||||
|
||||
```bash
|
||||
# Auf dem Server:
|
||||
cd /var/www/tt-tagebuch.de
|
||||
git pull origin main
|
||||
cd backend
|
||||
npm install # Erstellt automatisch den Frontend-Build (via postinstall script)
|
||||
```
|
||||
|
||||
### 4. Backend neu starten (falls nötig)
|
||||
|
||||
```bash
|
||||
# Falls als systemd-Service:
|
||||
sudo systemctl restart tt-tagebuch
|
||||
|
||||
# Oder falls als PM2-Prozess:
|
||||
pm2 restart tt-tagebuch-backend
|
||||
```
|
||||
|
||||
## Erwartete Ergebnisse
|
||||
|
||||
Das Prüfskript sollte folgende Ausgabe zeigen:
|
||||
|
||||
```
|
||||
1. TeamManagementView.vue:
|
||||
✓ Enthält 'const t = (key, params) => i18n.global.t'
|
||||
✓ Enthält keine $t() Aufrufe mehr
|
||||
✓ 't' ist im return Statement enthalten
|
||||
|
||||
2. PermissionsView.vue:
|
||||
✓ Enthält 'const t = (key, params) => i18n.global.t'
|
||||
✓ Enthält keine $t() Aufrufe mehr
|
||||
✓ 't' ist im return Statement enthalten
|
||||
|
||||
3. LogsView.vue:
|
||||
✓ Enthält 'const t = (key, params) => i18n.global.t'
|
||||
✓ Enthält keine $t() Aufrufe mehr
|
||||
✓ 't' ist im return Statement enthalten
|
||||
|
||||
4. SeasonSelector.vue:
|
||||
✓ Enthält 'const t = (key, params) => i18n.global.t'
|
||||
✓ Enthält keine $t() Aufrufe mehr
|
||||
```
|
||||
|
||||
## Commits, die auf den Server müssen
|
||||
|
||||
Die folgenden Commits müssen auf dem Server sein:
|
||||
|
||||
- `b0e610f` - Fix: Replace all $t() calls with t() in PermissionsView and LogsView templates
|
||||
- `0285c05` - Fix: Replace all $t() calls with t() in TeamManagementView template
|
||||
- `5d4f2eb` - Update localization handling in TeamManagementView
|
||||
|
||||
Prüfe mit:
|
||||
```bash
|
||||
git log --oneline -5
|
||||
```
|
||||
|
||||
191
DEPLOYMENT_SOCKET_IO.md
Normal file
191
DEPLOYMENT_SOCKET_IO.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# Deployment-Anleitung: Socket.IO mit SSL
|
||||
|
||||
Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy).
|
||||
|
||||
## Schritte nach dem Deployment
|
||||
|
||||
### 1. Firewall-Port öffnen
|
||||
|
||||
```bash
|
||||
# UFW (Ubuntu Firewall)
|
||||
sudo ufw allow 3051/tcp
|
||||
```
|
||||
|
||||
### 2. Apache-Konfiguration aktualisieren
|
||||
|
||||
```bash
|
||||
sudo cp /var/www/tt-tagebuch.de/apache.conf.example /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
### 3. systemd-Service konfigurieren (als www-data)
|
||||
|
||||
**WICHTIG:** Der Service sollte als `www-data` laufen, nicht als `nobody`!
|
||||
|
||||
```bash
|
||||
# Service-Datei installieren
|
||||
sudo cp /var/www/tt-tagebuch.de/tt-tagebuch.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
Die Service-Datei konfiguriert:
|
||||
- User: `www-data` (Standard-Webserver-Benutzer)
|
||||
- Group: `www-data`
|
||||
- Port: 3050 (HTTP) und 3051 (HTTPS)
|
||||
|
||||
### 4. SSL-Zertifikat-Berechtigungen setzen
|
||||
|
||||
**WICHTIG:** Der Node.js-Prozess muss Zugriff auf die SSL-Zertifikate haben!
|
||||
|
||||
```bash
|
||||
cd /var/www/tt-tagebuch.de/backend
|
||||
chmod +x scripts/fixCertPermissions.sh
|
||||
sudo ./scripts/fixCertPermissions.sh
|
||||
```
|
||||
|
||||
Dieses Skript:
|
||||
- Erstellt die Gruppe `ssl-cert` (falls nicht vorhanden)
|
||||
- Fügt den Service-Benutzer (`www-data`) zur Gruppe hinzu
|
||||
- Setzt die Berechtigungen für die Zertifikate
|
||||
|
||||
### 5. Backend neu starten
|
||||
|
||||
**WICHTIG:** Der Backend-Server muss neu gestartet werden, damit der HTTPS-Server auf Port 3051 läuft!
|
||||
|
||||
```bash
|
||||
# Falls als systemd-Service:
|
||||
sudo systemctl restart tt-tagebuch
|
||||
|
||||
# Oder falls als PM2-Prozess:
|
||||
pm2 restart tt-tagebuch-backend
|
||||
```
|
||||
|
||||
### 6. Prüfen, ob HTTPS-Server läuft
|
||||
|
||||
```bash
|
||||
# Prüfe, ob Port 3051 geöffnet ist
|
||||
sudo netstat -tlnp | grep 3051
|
||||
# Oder:
|
||||
sudo ss -tlnp | grep 3051
|
||||
|
||||
# Prüfe Backend-Logs
|
||||
sudo journalctl -u tt-tagebuch -f
|
||||
# Oder bei PM2:
|
||||
pm2 logs tt-tagebuch-backend
|
||||
```
|
||||
|
||||
Du solltest folgende Meldung sehen:
|
||||
```
|
||||
🚀 HTTPS-Server für Socket.IO läuft auf Port 3051
|
||||
```
|
||||
|
||||
### 7. Diagnose-Skript ausführen
|
||||
|
||||
```bash
|
||||
cd /var/www/tt-tagebuch.de/backend
|
||||
node scripts/checkSocketIOServer.js
|
||||
```
|
||||
|
||||
Dieses Skript prüft:
|
||||
- Ob SSL-Zertifikate existieren
|
||||
- Ob Port 3051 geöffnet ist
|
||||
- Ob der Server erreichbar ist
|
||||
|
||||
### 8. Testen
|
||||
|
||||
Im Browser sollte Socket.IO jetzt direkt zu `wss://tt-tagebuch.de:3051` verbinden.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port 3051 ist nicht erreichbar
|
||||
|
||||
1. **Prüfe Firewall:**
|
||||
```bash
|
||||
sudo ufw status
|
||||
sudo ufw allow 3051/tcp
|
||||
```
|
||||
|
||||
2. **Prüfe, ob der Server läuft:**
|
||||
```bash
|
||||
sudo netstat -tlnp | grep 3051
|
||||
sudo ss -tlnp | grep 3051
|
||||
```
|
||||
|
||||
3. **Prüfe Backend-Logs auf Fehler:**
|
||||
```bash
|
||||
sudo journalctl -u tt-tagebuch -n 50
|
||||
# Oder:
|
||||
pm2 logs tt-tagebuch-backend --lines 50
|
||||
```
|
||||
|
||||
4. **Prüfe, ob HTTPS-Server gestartet wurde:**
|
||||
- Suche in den Logs nach: `🚀 HTTPS-Server für Socket.IO läuft auf Port 3051`
|
||||
- Falls nicht vorhanden, prüfe auf Fehler: `⚠️ HTTPS-Server konnte nicht gestartet werden`
|
||||
|
||||
### SSL-Zertifikat-Fehler / Berechtigungsfehler
|
||||
|
||||
**Fehler:** `EACCES: permission denied, open '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem'`
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
cd /var/www/tt-tagebuch.de/backend
|
||||
chmod +x scripts/fixCertPermissions.sh
|
||||
sudo ./scripts/fixCertPermissions.sh
|
||||
sudo systemctl restart tt-tagebuch
|
||||
```
|
||||
|
||||
Stelle sicher, dass die Zertifikate existieren:
|
||||
```bash
|
||||
ls -la /etc/letsencrypt/live/tt-tagebuch.de/
|
||||
```
|
||||
|
||||
Falls die Zertifikate nicht existieren:
|
||||
```bash
|
||||
sudo certbot certonly --standalone -d tt-tagebuch.de
|
||||
```
|
||||
|
||||
### Service läuft als "nobody"
|
||||
|
||||
**Problem:** Der Service läuft als `nobody`, was zu eingeschränkt ist.
|
||||
|
||||
**Lösung:**
|
||||
1. Installiere die Service-Datei (siehe Schritt 3)
|
||||
2. Führe das Berechtigungs-Skript aus (siehe Schritt 4)
|
||||
3. Starte den Service neu
|
||||
|
||||
```bash
|
||||
# Prüfe aktuellen Service-User
|
||||
sudo systemctl show -p User tt-tagebuch.service
|
||||
|
||||
# Installiere Service-Datei
|
||||
sudo cp /var/www/tt-tagebuch.de/tt-tagebuch.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart tt-tagebuch
|
||||
|
||||
# Prüfe, ob jetzt als www-data läuft
|
||||
sudo systemctl show -p User tt-tagebuch.service
|
||||
```
|
||||
|
||||
### Frontend verbindet nicht
|
||||
|
||||
1. **Prüfe Browser-Konsole auf Fehler**
|
||||
2. **Prüfe, ob `import.meta.env.PROD` korrekt gesetzt ist:**
|
||||
- In Produktion sollte die Socket.IO-URL `https://tt-tagebuch.de:3051` sein
|
||||
- In Entwicklung sollte sie `http://localhost:3005` sein
|
||||
|
||||
3. **Prüfe, ob die Socket.IO-URL korrekt ist:**
|
||||
- Öffne Browser-Entwicklertools → Network
|
||||
- Suche nach WebSocket-Verbindungen
|
||||
- Die URL sollte `wss://tt-tagebuch.de:3051/socket.io/...` sein
|
||||
|
||||
### Server lauscht nur auf localhost
|
||||
|
||||
Der Server sollte auf `0.0.0.0` lauschen (nicht nur auf `localhost`).
|
||||
Dies ist bereits in der Konfiguration eingestellt:
|
||||
```javascript
|
||||
httpsServer.listen(httpsPort, '0.0.0.0', () => {
|
||||
console.log(`🚀 HTTPS-Server für Socket.IO läuft auf Port ${httpsPort}`);
|
||||
});
|
||||
```
|
||||
|
||||
Falls der Server trotzdem nicht erreichbar ist, prüfe die Backend-Logs.
|
||||
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
|
||||
|
||||
69
SERVER_NODE_UPGRADE.md
Normal file
69
SERVER_NODE_UPGRADE.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Server Node.js Upgrade-Anleitung
|
||||
|
||||
## Problem
|
||||
|
||||
Der Server verwendet Node.js 20.17.0, aber Vite 7.2.4 benötigt Node.js 20.19+ oder 22.12+.
|
||||
|
||||
## Lösung 1: Node.js auf dem Server upgraden (Empfohlen)
|
||||
|
||||
### Option A: Node.js 20.19+ installieren
|
||||
|
||||
```bash
|
||||
# Auf dem Server:
|
||||
# Mit nvm (falls installiert):
|
||||
nvm install 20.19.0
|
||||
nvm use 20.19.0
|
||||
nvm alias default 20.19.0
|
||||
|
||||
# Oder mit NodeSource Repository:
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs=20.19.0-1nodesource1
|
||||
|
||||
# Prüfe Version:
|
||||
node --version # Sollte 20.19.0 oder höher sein
|
||||
```
|
||||
|
||||
### Option B: Node.js 22.12+ installieren (LTS)
|
||||
|
||||
```bash
|
||||
# Auf dem Server:
|
||||
# Mit nvm:
|
||||
nvm install 22.12.0
|
||||
nvm use 22.12.0
|
||||
nvm alias default 22.12.0
|
||||
|
||||
# Oder mit NodeSource Repository:
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
|
||||
# Prüfe Version:
|
||||
node --version # Sollte 22.12.0 oder höher sein
|
||||
```
|
||||
|
||||
### Nach dem Upgrade
|
||||
|
||||
```bash
|
||||
cd /var/www/tt-tagebuch.de/backend
|
||||
npm install # Erstellt automatisch den Frontend-Build
|
||||
sudo systemctl restart tt-tagebuch
|
||||
```
|
||||
|
||||
## Lösung 2: Vite auf Version 6 downgraden (Temporär)
|
||||
|
||||
Falls Node.js nicht upgradet werden kann, wurde Vite bereits auf Version 6.0.0 downgraded.
|
||||
|
||||
```bash
|
||||
cd /var/www/tt-tagebuch.de/backend
|
||||
npm install # Erstellt automatisch den Frontend-Build
|
||||
sudo systemctl restart tt-tagebuch
|
||||
```
|
||||
|
||||
**Hinweis:** Vite 6 funktioniert mit Node.js 20.17.0, aber Vite 7 bietet bessere Performance und Features.
|
||||
|
||||
## Empfehlung
|
||||
|
||||
**Node.js upgraden** ist die bessere Lösung, da:
|
||||
- Vite 7 bessere Performance bietet
|
||||
- Zukünftige Updates einfacher sind
|
||||
- Node.js 20.19+ oder 22.12+ LTS-Versionen sind
|
||||
|
||||
109
SITEMAP_ANLEITUNG.md
Normal file
109
SITEMAP_ANLEITUNG.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Sitemap für Google Search Console einreichen
|
||||
|
||||
## Aktuelle Sitemap
|
||||
|
||||
Die Sitemap ist verfügbar unter: `https://tt-tagebuch.de/sitemap.xml`
|
||||
|
||||
Sie enthält folgende öffentliche Seiten:
|
||||
- `/` (Home) - Priorität: 1.0
|
||||
- `/register` (Registrierung) - Priorität: 0.8
|
||||
- `/login` (Anmeldung) - Priorität: 0.7
|
||||
- `/impressum` (Impressum) - Priorität: 0.3
|
||||
- `/datenschutz` (Datenschutz) - Priorität: 0.3
|
||||
|
||||
## Sitemap aktualisieren
|
||||
|
||||
### Automatisch (empfohlen)
|
||||
```bash
|
||||
./update-sitemap.sh
|
||||
```
|
||||
|
||||
Das Skript aktualisiert automatisch das `lastmod`-Datum auf das heutige Datum.
|
||||
|
||||
### Manuell
|
||||
Die Sitemap-Datei befindet sich in: `frontend/public/sitemap.xml`
|
||||
|
||||
Nach Änderungen:
|
||||
1. Frontend neu bauen: `cd frontend && npm run build`
|
||||
2. Backend neu starten (falls nötig)
|
||||
|
||||
## Sitemap in Google Search Console einreichen
|
||||
|
||||
### Schritt 1: Google Search Console öffnen
|
||||
1. Gehe zu: https://search.google.com/search-console
|
||||
2. Wähle die Property für `tt-tagebuch.de` aus
|
||||
|
||||
### Schritt 2: Sitemap hinzufügen
|
||||
1. Klicke im linken Menü auf **"Sitemaps"**
|
||||
2. Im Feld **"Neue Sitemap hinzufügen"** eingeben:
|
||||
```
|
||||
sitemap.xml
|
||||
```
|
||||
Oder die vollständige URL:
|
||||
```
|
||||
https://tt-tagebuch.de/sitemap.xml
|
||||
```
|
||||
3. Klicke auf **"Senden"**
|
||||
|
||||
### Schritt 3: Status prüfen
|
||||
- Google wird die Sitemap innerhalb weniger Minuten verarbeiten
|
||||
- Der Status wird angezeigt:
|
||||
- ✅ **Erfolgreich**: Sitemap wurde erfolgreich verarbeitet
|
||||
- ⚠️ **Warnung**: Sitemap wurde verarbeitet, aber es gibt Warnungen
|
||||
- ❌ **Fehler**: Sitemap konnte nicht verarbeitet werden
|
||||
|
||||
### Schritt 4: Indexierung anfordern
|
||||
Nach dem Einreichen der Sitemap kannst du auch einzelne URLs zur Indexierung anfordern:
|
||||
1. Gehe zu **"URL-Prüfung"**
|
||||
2. Gib die URL ein: `https://tt-tagebuch.de/`
|
||||
3. Klicke auf **"Indexierung anfordern"**
|
||||
|
||||
## Sitemap testen
|
||||
|
||||
### Online-Tools
|
||||
- Google Sitemap Tester: https://www.xml-sitemaps.com/validate-xml-sitemap.html
|
||||
- Sitemap Validator: https://validator.w3.org/
|
||||
|
||||
### Per Kommandozeile
|
||||
```bash
|
||||
# Sitemap abrufen
|
||||
curl https://tt-tagebuch.de/sitemap.xml
|
||||
|
||||
# XML-Validierung (falls xmllint installiert ist)
|
||||
curl -s https://tt-tagebuch.de/sitemap.xml | xmllint --noout -
|
||||
```
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
1. **robots.txt**: Die Sitemap ist bereits in der `robots.txt` referenziert:
|
||||
```
|
||||
Sitemap: https://tt-tagebuch.de/sitemap.xml
|
||||
```
|
||||
|
||||
2. **lastmod-Datum**: Wird automatisch beim Ausführen von `update-sitemap.sh` aktualisiert
|
||||
|
||||
3. **Nur öffentliche Seiten**: Die Sitemap enthält nur öffentlich zugängliche Seiten. Geschützte Seiten (die eine Anmeldung erfordern) sind nicht enthalten.
|
||||
|
||||
4. **Prioritäten**:
|
||||
- Homepage: 1.0 (höchste Priorität)
|
||||
- Registrierung/Login: 0.7-0.8 (wichtig für neue Nutzer)
|
||||
- Rechtliche Seiten: 0.3 (niedrige Priorität, ändern sich selten)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Sitemap wird nicht gefunden
|
||||
- Prüfe, ob die Sitemap unter `https://tt-tagebuch.de/sitemap.xml` erreichbar ist
|
||||
- Stelle sicher, dass das Frontend gebaut wurde: `cd frontend && npm run build`
|
||||
- Prüfe die Apache-Konfiguration (sollte statische Dateien aus `/var/www/tt-tagebuch.de` servieren)
|
||||
|
||||
### Sitemap wird nicht indexiert
|
||||
- Warte einige Stunden/Tage - Google braucht Zeit zum Crawlen
|
||||
- Prüfe in der Search Console, ob es Fehler gibt
|
||||
- Stelle sicher, dass die URLs in der Sitemap erreichbar sind
|
||||
- Prüfe, ob die `robots.txt` die Seiten nicht blockiert
|
||||
|
||||
### Sitemap enthält Fehler
|
||||
- Validiere die XML-Struktur mit einem XML-Validator
|
||||
- Prüfe, ob alle URLs korrekt sind (keine 404-Fehler)
|
||||
- Stelle sicher, dass alle URLs HTTPS verwenden (nicht HTTP)
|
||||
|
||||
22
apache-http.conf.example
Normal file
22
apache-http.conf.example
Normal file
@@ -0,0 +1,22 @@
|
||||
# Apache-Konfiguration für tt-tagebuch.de - HTTP (Port 80)
|
||||
#
|
||||
# Diese Datei kopieren nach: /etc/apache2/sites-available/tt-tagebuch.de.conf
|
||||
# Dann aktivieren mit: sudo a2ensite tt-tagebuch.de.conf
|
||||
# Und neu starten: sudo systemctl restart apache2
|
||||
#
|
||||
# WICHTIG: Folgende Module müssen aktiviert sein:
|
||||
# sudo a2enmod rewrite
|
||||
# sudo systemctl restart apache2
|
||||
|
||||
# HTTP: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de
|
||||
<VirtualHost *:80>
|
||||
ServerName www.tt-tagebuch.de
|
||||
Redirect permanent / https://tt-tagebuch.de/
|
||||
</VirtualHost>
|
||||
|
||||
# HTTP: tt-tagebuch.de -> HTTPS: tt-tagebuch.de
|
||||
<VirtualHost *:80>
|
||||
ServerName tt-tagebuch.de
|
||||
Redirect permanent / https://tt-tagebuch.de/
|
||||
</VirtualHost>
|
||||
|
||||
60
apache-https.conf.example
Normal file
60
apache-https.conf.example
Normal file
@@ -0,0 +1,60 @@
|
||||
# Apache-Konfiguration für tt-tagebuch.de - HTTPS (Port 443)
|
||||
#
|
||||
# Diese Datei kopieren nach: /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
|
||||
# Dann aktivieren mit: sudo a2ensite tt-tagebuch.de-le-ssl.conf
|
||||
# Und neu starten: sudo systemctl restart apache2
|
||||
#
|
||||
# WICHTIG: Folgende Module müssen aktiviert sein:
|
||||
# sudo a2enmod proxy
|
||||
# sudo a2enmod proxy_http
|
||||
# sudo a2enmod proxy_wstunnel
|
||||
# sudo a2enmod rewrite
|
||||
# sudo a2enmod headers
|
||||
# sudo systemctl restart apache2
|
||||
|
||||
# HTTPS: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de (301-Weiterleitung)
|
||||
<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>
|
||||
|
||||
# HTTPS: tt-tagebuch.de - Hauptkonfiguration (non-www)
|
||||
<VirtualHost *:443>
|
||||
ServerName tt-tagebuch.de
|
||||
|
||||
DocumentRoot /var/www/tt-tagebuch.de
|
||||
|
||||
<Directory /var/www/tt-tagebuch.de>
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/tt-tagebuch.de_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/tt-tagebuch.de_access.log combined
|
||||
|
||||
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
|
||||
|
||||
ProxyRequests Off
|
||||
|
||||
# HINWEIS: Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy)
|
||||
# Siehe backend/SOCKET_IO_SSL_SETUP.md für Details
|
||||
|
||||
# API-Routen
|
||||
ProxyPass /api http://localhost:3050/api
|
||||
ProxyPassReverse /api http://localhost:3050/api
|
||||
|
||||
# Alle anderen Anfragen an den Backend-Server (für Frontend)
|
||||
ProxyPass / http://localhost:3050/
|
||||
ProxyPassReverse / http://localhost:3050/
|
||||
</VirtualHost>
|
||||
|
||||
89
apache.conf.example
Normal file
89
apache.conf.example
Normal file
@@ -0,0 +1,89 @@
|
||||
# Apache-Konfiguration für tt-tagebuch.de
|
||||
#
|
||||
# HINWEIS: Diese Datei ist eine kombinierte Referenz.
|
||||
# Für die tatsächliche Konfiguration werden zwei separate Dateien verwendet:
|
||||
#
|
||||
# 1. apache-http.conf.example -> /etc/apache2/sites-available/tt-tagebuch.de.conf
|
||||
# (HTTP, Port 80 - Weiterleitung zu HTTPS)
|
||||
#
|
||||
# 2. apache-https.conf.example -> /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
|
||||
# (HTTPS, Port 443 - Hauptkonfiguration)
|
||||
#
|
||||
# Oder verwende das Update-Skript: ./update-apache-config.sh
|
||||
#
|
||||
# WICHTIG: Folgende Module müssen aktiviert sein:
|
||||
# sudo a2enmod proxy
|
||||
# sudo a2enmod proxy_http
|
||||
# sudo a2enmod proxy_wstunnel
|
||||
# sudo a2enmod rewrite
|
||||
# sudo a2enmod headers
|
||||
# sudo systemctl restart apache2
|
||||
|
||||
# ============================================
|
||||
# HTTP (Port 80) - Weiterleitung zu HTTPS
|
||||
# ============================================
|
||||
|
||||
# HTTP: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de
|
||||
<VirtualHost *:80>
|
||||
ServerName www.tt-tagebuch.de
|
||||
Redirect permanent / https://tt-tagebuch.de/
|
||||
</VirtualHost>
|
||||
|
||||
# HTTP: tt-tagebuch.de -> HTTPS: tt-tagebuch.de
|
||||
<VirtualHost *:80>
|
||||
ServerName tt-tagebuch.de
|
||||
Redirect permanent / https://tt-tagebuch.de/
|
||||
</VirtualHost>
|
||||
|
||||
# ============================================
|
||||
# HTTPS (Port 443) - Weiterleitung www -> non-www
|
||||
# ============================================
|
||||
|
||||
# HTTPS: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de (301-Weiterleitung)
|
||||
<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>
|
||||
|
||||
# ============================================
|
||||
# HTTPS (Port 443) - Hauptkonfiguration (non-www)
|
||||
# ============================================
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName tt-tagebuch.de
|
||||
|
||||
DocumentRoot /var/www/tt-tagebuch.de
|
||||
|
||||
<Directory /var/www/tt-tagebuch.de>
|
||||
Options Indexes FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
ErrorLog ${APACHE_LOG_DIR}/tt-tagebuch.de_error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/tt-tagebuch.de_access.log combined
|
||||
|
||||
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
|
||||
|
||||
ProxyRequests Off
|
||||
|
||||
# HINWEIS: Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy)
|
||||
# Siehe backend/SOCKET_IO_SSL_SETUP.md für Details
|
||||
|
||||
# API-Routen
|
||||
ProxyPass /api http://localhost:3050/api
|
||||
ProxyPassReverse /api http://localhost:3050/api
|
||||
|
||||
# Alle anderen Anfragen an den Backend-Server (für Frontend)
|
||||
ProxyPass / http://localhost:3050/
|
||||
ProxyPassReverse / http://localhost:3050/
|
||||
</VirtualHost>
|
||||
140
backend/SOCKET_IO_SSL_SETUP.md
Normal file
140
backend/SOCKET_IO_SSL_SETUP.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Socket.IO mit SSL direkt betreiben (Alternative zu Apache-Proxy)
|
||||
|
||||
Falls die Apache-WebSocket-Proxy-Konfiguration nicht funktioniert, kann Socket.IO direkt mit SSL betrieben werden.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
1. SSL-Zertifikat (z.B. von Let's Encrypt)
|
||||
2. Port in der Firewall öffnen (z.B. 3051)
|
||||
3. Socket.IO-Server auf HTTPS konfigurieren
|
||||
|
||||
## Backend-Konfiguration
|
||||
|
||||
### 1. Socket.IO auf HTTPS umstellen
|
||||
|
||||
Ändere `backend/server.js`:
|
||||
|
||||
```javascript
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
|
||||
// SSL-Zertifikat laden
|
||||
const httpsOptions = {
|
||||
key: fs.readFileSync('/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem'),
|
||||
cert: fs.readFileSync('/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem')
|
||||
};
|
||||
|
||||
// HTTPS-Server erstellen
|
||||
const httpsServer = https.createServer(httpsOptions, app);
|
||||
|
||||
// Socket.IO initialisieren
|
||||
initializeSocketIO(httpsServer);
|
||||
|
||||
// HTTPS-Server starten
|
||||
const httpsPort = process.env.HTTPS_PORT || 3051;
|
||||
httpsServer.listen(httpsPort, () => {
|
||||
console.log(`🚀 HTTPS-Server läuft auf Port ${httpsPort}`);
|
||||
});
|
||||
|
||||
// HTTP-Server für API (optional, falls API weiterhin über HTTP laufen soll)
|
||||
const httpServer = createServer(app);
|
||||
const httpPort = process.env.PORT || 3005;
|
||||
httpServer.listen(httpPort, () => {
|
||||
console.log(`🚀 HTTP-Server läuft auf Port ${httpPort}`);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Frontend-Konfiguration
|
||||
|
||||
Ändere `frontend/src/services/socketService.js`:
|
||||
|
||||
```javascript
|
||||
import { io } from 'socket.io-client';
|
||||
import { backendBaseUrl } from '../apiClient.js';
|
||||
|
||||
let socket = null;
|
||||
|
||||
export const connectSocket = (clubId) => {
|
||||
// Verwende HTTPS-URL für Socket.IO
|
||||
const socketUrl = backendBaseUrl.replace('http://', 'https://').replace(':3005', ':3051');
|
||||
|
||||
if (socket && socket.connected) {
|
||||
// Wenn bereits verbunden, verlasse den alten Club-Raum und trete dem neuen bei
|
||||
if (socket.currentClubId) {
|
||||
socket.emit('leave-club', socket.currentClubId);
|
||||
}
|
||||
} else {
|
||||
// Neue Verbindung erstellen
|
||||
socket = io(socketUrl, {
|
||||
path: '/socket.io/',
|
||||
transports: ['websocket', 'polling'],
|
||||
reconnection: true,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionAttempts: 5,
|
||||
timeout: 20000,
|
||||
upgrade: true,
|
||||
forceNew: false,
|
||||
secure: true // Wichtig für HTTPS
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Socket.IO verbunden');
|
||||
if (socket.currentClubId) {
|
||||
socket.emit('join-club', socket.currentClubId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Socket.IO getrennt');
|
||||
});
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('Socket.IO Verbindungsfehler:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Club-Raum beitreten
|
||||
if (clubId) {
|
||||
socket.emit('join-club', clubId);
|
||||
socket.currentClubId = clubId;
|
||||
}
|
||||
|
||||
return socket;
|
||||
};
|
||||
|
||||
export const disconnectSocket = () => {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSocket = () => socket;
|
||||
```
|
||||
|
||||
### 3. Firewall-Port öffnen
|
||||
|
||||
```bash
|
||||
# UFW (Ubuntu Firewall)
|
||||
sudo ufw allow 3051/tcp
|
||||
|
||||
# Oder iptables
|
||||
sudo iptables -A INPUT -p tcp --dport 3051 -j ACCEPT
|
||||
```
|
||||
|
||||
### 4. Apache-Konfiguration anpassen
|
||||
|
||||
Entferne die Socket.IO-Proxy-Konfiguration aus Apache, da Socket.IO jetzt direkt erreichbar ist.
|
||||
|
||||
## Vorteile
|
||||
|
||||
- Einfacher zu konfigurieren
|
||||
- Keine Apache-Proxy-Probleme
|
||||
- Direkte WebSocket-Verbindung
|
||||
|
||||
## Nachteile
|
||||
|
||||
- Separater Port muss geöffnet sein
|
||||
- Zwei Ports (HTTP für API, HTTPS für Socket.IO)
|
||||
- CORS-Konfiguration muss angepasst werden
|
||||
|
||||
@@ -17,19 +17,146 @@ class MyTischtennisClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get login page to extract XSRF token and CAPTCHA token
|
||||
* @returns {Promise<Object>} Object with xsrfToken, captchaToken, and captchaClicked flag
|
||||
*/
|
||||
async getLoginPage() {
|
||||
try {
|
||||
const response = await this.client.get('/login?next=%2F');
|
||||
const html = response.data;
|
||||
|
||||
// Extract XSRF token from hidden input
|
||||
const xsrfMatch = html.match(/<input[^>]*name="xsrf"[^>]*value="([^"]+)"/);
|
||||
const xsrfToken = xsrfMatch ? xsrfMatch[1] : null;
|
||||
|
||||
// Extract CAPTCHA token from hidden input (if present)
|
||||
const captchaMatch = html.match(/<input[^>]*name="captcha"[^>]*value="([^"]+)"/);
|
||||
const captchaToken = captchaMatch ? captchaMatch[1] : null;
|
||||
|
||||
// Check if captcha_clicked is true or false
|
||||
const captchaClickedMatch = html.match(/<input[^>]*name="captcha_clicked"[^>]*value="([^"]+)"/);
|
||||
const captchaClicked = captchaClickedMatch ? captchaClickedMatch[1] === 'true' : false;
|
||||
|
||||
// Check if CAPTCHA is required (look for private-captcha element or captcha input)
|
||||
const requiresCaptcha = html.includes('private-captcha') || html.includes('name="captcha"');
|
||||
|
||||
console.log('[myTischtennisClient.getLoginPage]', {
|
||||
hasXsrfToken: !!xsrfToken,
|
||||
hasCaptchaToken: !!captchaToken,
|
||||
captchaClicked,
|
||||
requiresCaptcha
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
xsrfToken,
|
||||
captchaToken,
|
||||
captchaClicked,
|
||||
requiresCaptcha
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching login page:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login to myTischtennis API
|
||||
* @param {string} email - myTischtennis email (not username!)
|
||||
* @param {string} password - myTischtennis password
|
||||
* @param {string} captchaToken - Optional CAPTCHA token if required
|
||||
* @param {string} xsrfToken - Optional XSRF token (will be fetched if not provided)
|
||||
* @returns {Promise<Object>} Login response with token and session data
|
||||
*/
|
||||
async login(email, password) {
|
||||
async login(email, password, captchaToken = null, xsrfToken = null) {
|
||||
try {
|
||||
let loginPage = null;
|
||||
let captchaClicked = false;
|
||||
|
||||
// If XSRF token not provided, fetch login page to get it
|
||||
if (!xsrfToken) {
|
||||
loginPage = await this.getLoginPage();
|
||||
if (!loginPage.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Konnte Login-Seite nicht abrufen: ' + loginPage.error
|
||||
};
|
||||
}
|
||||
xsrfToken = loginPage.xsrfToken;
|
||||
|
||||
// If CAPTCHA token not provided but found in HTML, use it
|
||||
if (!captchaToken && loginPage.captchaToken) {
|
||||
captchaToken = loginPage.captchaToken;
|
||||
captchaClicked = loginPage.captchaClicked;
|
||||
console.log('[myTischtennisClient.login] CAPTCHA-Token aus HTML extrahiert, captcha_clicked:', captchaClicked);
|
||||
}
|
||||
|
||||
// If CAPTCHA is required but no token found yet, wait and try to get it again
|
||||
// Das CAPTCHA-System löst das Puzzle im Hintergrund via JavaScript, daher kann es einen Moment dauern
|
||||
// Wir müssen mehrmals versuchen, da das Token erst generiert wird, nachdem das JavaScript gelaufen ist
|
||||
if (loginPage.requiresCaptcha && !captchaToken) {
|
||||
console.log('[myTischtennisClient.login] CAPTCHA erforderlich, aber noch kein Token gefunden. Warte und versuche erneut...');
|
||||
|
||||
// Versuche bis zu 5 Mal, das CAPTCHA-Token zu erhalten
|
||||
let maxRetries = 5;
|
||||
let retryCount = 0;
|
||||
let foundToken = false;
|
||||
|
||||
while (retryCount < maxRetries && !foundToken) {
|
||||
// Warte 2-4 Sekunden zwischen den Versuchen
|
||||
const waitMs = Math.floor(Math.random() * 2000) + 2000; // 2000-4000ms
|
||||
console.log(`[myTischtennisClient.login] Versuch ${retryCount + 1}/${maxRetries}: Warte ${waitMs}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, waitMs));
|
||||
|
||||
// Versuche erneut, die Login-Seite abzurufen, um das gelöste CAPTCHA-Token zu erhalten
|
||||
const retryLoginPage = await this.getLoginPage();
|
||||
if (retryLoginPage.success && retryLoginPage.captchaToken) {
|
||||
captchaToken = retryLoginPage.captchaToken;
|
||||
captchaClicked = retryLoginPage.captchaClicked;
|
||||
xsrfToken = retryLoginPage.xsrfToken || xsrfToken; // Aktualisiere XSRF-Token falls nötig
|
||||
foundToken = true;
|
||||
console.log(`[myTischtennisClient.login] CAPTCHA-Token nach ${retryCount + 1} Versuchen gefunden, captcha_clicked:`, captchaClicked);
|
||||
} else {
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundToken) {
|
||||
// Wenn nach allen Versuchen kein Token gefunden wurde, Fehler zurückgeben
|
||||
console.log('[myTischtennisClient.login] CAPTCHA-Token konnte nach mehreren Versuchen nicht gefunden werden');
|
||||
return {
|
||||
success: false,
|
||||
error: 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.',
|
||||
requiresCaptcha: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Zufällige Verzögerung von 2-5 Sekunden zwischen Laden des Forms und Absenden
|
||||
// Simuliert menschliches Verhalten und gibt dem CAPTCHA-System Zeit
|
||||
const delayMs = Math.floor(Math.random() * 3000) + 2000; // 2000-5000ms
|
||||
console.log(`[myTischtennisClient] Warte ${delayMs}ms vor Login-Request (simuliert menschliches Verhalten)`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
|
||||
// Create form data
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('email', email);
|
||||
formData.append('password', password);
|
||||
formData.append('intent', 'login');
|
||||
|
||||
if (xsrfToken) {
|
||||
formData.append('xsrf', xsrfToken);
|
||||
}
|
||||
|
||||
if (captchaToken) {
|
||||
formData.append('captcha', captchaToken);
|
||||
formData.append('captcha_clicked', captchaClicked ? 'true' : 'false');
|
||||
}
|
||||
|
||||
const response = await this.client.post(
|
||||
'/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
|
||||
@@ -86,11 +213,36 @@ class MyTischtennisClient {
|
||||
cookie: authCookie.split(';')[0] // Just the cookie value without attributes
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('MyTischtennis login error:', error.message);
|
||||
const statusCode = error.response?.status || 500;
|
||||
const responseData = error.response?.data;
|
||||
|
||||
// Check if response contains CAPTCHA error
|
||||
let errorMessage = error.response?.data?.message || error.message || 'Login fehlgeschlagen';
|
||||
let requiresCaptcha = false;
|
||||
|
||||
// Check for CAPTCHA-related errors in response
|
||||
if (typeof responseData === 'string') {
|
||||
if (responseData.includes('Captcha') || responseData.includes('CAPTCHA') ||
|
||||
responseData.includes('captcha') || responseData.includes('Captcha-Bestätigung')) {
|
||||
requiresCaptcha = true;
|
||||
errorMessage = 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.';
|
||||
}
|
||||
} else if (responseData && typeof responseData === 'object') {
|
||||
// Check for CAPTCHA errors in JSON response or HTML
|
||||
const dataString = JSON.stringify(responseData);
|
||||
if (dataString.includes('Captcha') || dataString.includes('CAPTCHA') ||
|
||||
dataString.includes('captcha') || dataString.includes('Captcha-Bestätigung')) {
|
||||
requiresCaptcha = true;
|
||||
errorMessage = 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.';
|
||||
}
|
||||
}
|
||||
|
||||
console.error('MyTischtennis login error:', errorMessage, `(Status: ${statusCode})`, requiresCaptcha ? '(CAPTCHA erforderlich)' : '');
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || 'Login fehlgeschlagen',
|
||||
status: error.response?.status || 500
|
||||
error: errorMessage,
|
||||
status: statusCode,
|
||||
requiresCaptcha
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
125
backend/constants/ERROR_CODES_USAGE.md
Normal file
125
backend/constants/ERROR_CODES_USAGE.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Fehlercode-System - Verwendungsanleitung
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Fehlercode-System ersetzt hardcodierte deutsche Fehlermeldungen durch strukturierte Fehlercodes, die im Frontend übersetzt werden.
|
||||
|
||||
## Backend-Verwendung
|
||||
|
||||
### 1. Fehlercode verwenden
|
||||
|
||||
```javascript
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import { ERROR_CODES, createError } from '../constants/errorCodes.js';
|
||||
|
||||
// Einfacher Fehlercode ohne Parameter
|
||||
throw new HttpError(createError(ERROR_CODES.USER_NOT_FOUND), 404);
|
||||
|
||||
// Fehlercode mit Parametern
|
||||
throw new HttpError(
|
||||
createError(ERROR_CODES.MEMBER_NOT_FOUND, { memberId: 123 }),
|
||||
404
|
||||
);
|
||||
|
||||
// Oder direkt:
|
||||
throw new HttpError(
|
||||
{ code: ERROR_CODES.MEMBER_NOT_FOUND, params: { memberId: 123 } },
|
||||
404
|
||||
);
|
||||
```
|
||||
|
||||
### 2. Legacy-Format (wird weiterhin unterstützt)
|
||||
|
||||
```javascript
|
||||
// Alte Variante funktioniert noch:
|
||||
throw new HttpError('Benutzer nicht gefunden', 404);
|
||||
```
|
||||
|
||||
## Frontend-Verwendung
|
||||
|
||||
### 1. Fehlermeldungen automatisch übersetzen
|
||||
|
||||
Die `getSafeErrorMessage`-Funktion erkennt automatisch Fehlercodes:
|
||||
|
||||
```javascript
|
||||
import { getSafeErrorMessage } from '../utils/errorMessages.js';
|
||||
|
||||
// In einer Vue-Komponente (Options API)
|
||||
try {
|
||||
await apiClient.post('/api/endpoint', data);
|
||||
} catch (error) {
|
||||
const message = getSafeErrorMessage(error, this.$t('errors.ERROR_UNKNOWN_ERROR'), this.$t);
|
||||
await this.showInfo(this.$t('messages.error'), message, '', 'error');
|
||||
}
|
||||
|
||||
// In einer Vue-Komponente (Composition API)
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const { t } = useI18n();
|
||||
|
||||
try {
|
||||
await apiClient.post('/api/endpoint', data);
|
||||
} catch (error) {
|
||||
const message = getSafeErrorMessage(error, t('errors.ERROR_UNKNOWN_ERROR'), t);
|
||||
await showInfo(t('messages.error'), message, '', 'error');
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Dialog-Utils mit Übersetzung
|
||||
|
||||
```javascript
|
||||
import { buildInfoConfig, safeErrorMessage } from '../utils/dialogUtils.js';
|
||||
|
||||
// Mit Übersetzungsfunktion
|
||||
this.infoDialog = buildInfoConfig({
|
||||
title: this.$t('messages.error'),
|
||||
message: safeErrorMessage(error, this.$t('errors.ERROR_UNKNOWN_ERROR'), this.$t),
|
||||
type: 'error'
|
||||
}, this.$t);
|
||||
```
|
||||
|
||||
## API-Response-Format
|
||||
|
||||
### Neues Format (mit Fehlercode):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"code": "ERROR_MEMBER_NOT_FOUND",
|
||||
"params": {
|
||||
"memberId": 123
|
||||
},
|
||||
"error": "ERROR_MEMBER_NOT_FOUND" // Für Rückwärtskompatibilität
|
||||
}
|
||||
```
|
||||
|
||||
### Legacy-Format (wird weiterhin unterstützt):
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Mitglied nicht gefunden",
|
||||
"error": "Mitglied nicht gefunden"
|
||||
}
|
||||
```
|
||||
|
||||
## Übersetzungen hinzufügen
|
||||
|
||||
1. **Backend**: Fehlercode in `backend/constants/errorCodes.js` definieren
|
||||
2. **Frontend**: Übersetzung in `frontend/src/i18n/locales/de.json` unter `errors` hinzufügen
|
||||
|
||||
Beispiel:
|
||||
```json
|
||||
{
|
||||
"errors": {
|
||||
"ERROR_MEMBER_NOT_FOUND": "Mitglied nicht gefunden.",
|
||||
"ERROR_MEMBER_NOT_FOUND_WITH_ID": "Mitglied mit ID {memberId} nicht gefunden."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration bestehender Fehler
|
||||
|
||||
1. Hardcodierte Fehlermeldung identifizieren
|
||||
2. Passenden Fehlercode in `errorCodes.js` finden oder erstellen
|
||||
3. Backend-Code anpassen: `throw new HttpError(createError(ERROR_CODES.XXX), status)`
|
||||
4. Übersetzung in `de.json` hinzufügen
|
||||
5. Frontend-Code muss nicht geändert werden (automatische Erkennung)
|
||||
|
||||
121
backend/constants/errorCodes.js
Normal file
121
backend/constants/errorCodes.js
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Fehlercodes für die API
|
||||
* Diese Codes werden an das Frontend gesendet und dort übersetzt
|
||||
*
|
||||
* Format: { code: string, params?: object }
|
||||
*
|
||||
* Beispiel:
|
||||
* - { code: 'ERROR_USER_NOT_FOUND' }
|
||||
* - { code: 'ERROR_MEMBER_NOT_FOUND', params: { memberId: 123 } }
|
||||
* - { code: 'ERROR_VALIDATION_FAILED', params: { field: 'email', value: 'invalid' } }
|
||||
*/
|
||||
|
||||
export const ERROR_CODES = {
|
||||
// Allgemeine Fehler
|
||||
INTERNAL_SERVER_ERROR: 'ERROR_INTERNAL_SERVER_ERROR',
|
||||
UNKNOWN_ERROR: 'ERROR_UNKNOWN_ERROR',
|
||||
VALIDATION_FAILED: 'ERROR_VALIDATION_FAILED',
|
||||
NOT_FOUND: 'ERROR_NOT_FOUND',
|
||||
UNAUTHORIZED: 'ERROR_UNAUTHORIZED',
|
||||
FORBIDDEN: 'ERROR_FORBIDDEN',
|
||||
BAD_REQUEST: 'ERROR_BAD_REQUEST',
|
||||
|
||||
// Authentifizierung
|
||||
USER_NOT_FOUND: 'ERROR_USER_NOT_FOUND',
|
||||
INVALID_PASSWORD: 'ERROR_INVALID_PASSWORD',
|
||||
LOGIN_FAILED: 'ERROR_LOGIN_FAILED',
|
||||
SESSION_EXPIRED: 'ERROR_SESSION_EXPIRED',
|
||||
|
||||
// MyTischtennis
|
||||
MYTISCHTENNIS_USER_NOT_FOUND: 'ERROR_MYTISCHTENNIS_USER_NOT_FOUND',
|
||||
MYTISCHTENNIS_INVALID_PASSWORD: 'ERROR_MYTISCHTENNIS_INVALID_PASSWORD',
|
||||
MYTISCHTENNIS_LOGIN_FAILED: 'ERROR_MYTISCHTENNIS_LOGIN_FAILED',
|
||||
MYTISCHTENNIS_ACCOUNT_NOT_LINKED: 'ERROR_MYTISCHTENNIS_ACCOUNT_NOT_LINKED',
|
||||
MYTISCHTENNIS_PASSWORD_NOT_SAVED: 'ERROR_MYTISCHTENNIS_PASSWORD_NOT_SAVED',
|
||||
MYTISCHTENNIS_SESSION_EXPIRED: 'ERROR_MYTISCHTENNIS_SESSION_EXPIRED',
|
||||
MYTISCHTENNIS_NO_PASSWORD_SAVED: 'ERROR_MYTISCHTENNIS_NO_PASSWORD_SAVED',
|
||||
MYTISCHTENNIS_CAPTCHA_REQUIRED: 'ERROR_MYTISCHTENNIS_CAPTCHA_REQUIRED',
|
||||
|
||||
// Mitglieder
|
||||
MEMBER_NOT_FOUND: 'ERROR_MEMBER_NOT_FOUND',
|
||||
MEMBER_ALREADY_EXISTS: 'ERROR_MEMBER_ALREADY_EXISTS',
|
||||
MEMBER_FIRSTNAME_REQUIRED: 'ERROR_MEMBER_FIRSTNAME_REQUIRED',
|
||||
MEMBER_LASTNAME_REQUIRED: 'ERROR_MEMBER_LASTNAME_REQUIRED',
|
||||
|
||||
// Gruppen
|
||||
GROUP_NOT_FOUND: 'ERROR_GROUP_NOT_FOUND',
|
||||
GROUP_NAME_REQUIRED: 'ERROR_GROUP_NAME_REQUIRED',
|
||||
GROUP_ALREADY_EXISTS: 'ERROR_GROUP_ALREADY_EXISTS',
|
||||
GROUP_INVALID_PRESET_TYPE: 'ERROR_GROUP_INVALID_PRESET_TYPE',
|
||||
GROUP_CANNOT_RENAME_PRESET: 'ERROR_GROUP_CANNOT_RENAME_PRESET',
|
||||
|
||||
// Turniere
|
||||
TOURNAMENT_NOT_FOUND: 'ERROR_TOURNAMENT_NOT_FOUND',
|
||||
TOURNAMENT_NO_DATE: 'ERROR_TOURNAMENT_NO_DATE',
|
||||
TOURNAMENT_CLASS_NAME_REQUIRED: 'ERROR_TOURNAMENT_CLASS_NAME_REQUIRED',
|
||||
TOURNAMENT_NO_PARTICIPANTS: 'ERROR_TOURNAMENT_NO_PARTICIPANTS',
|
||||
TOURNAMENT_NO_VALID_PARTICIPANTS: 'ERROR_TOURNAMENT_NO_VALID_PARTICIPANTS',
|
||||
TOURNAMENT_NO_TRAINING_DAY: 'ERROR_TOURNAMENT_NO_TRAINING_DAY',
|
||||
TOURNAMENT_PDF_GENERATION_FAILED: 'ERROR_TOURNAMENT_PDF_GENERATION_FAILED',
|
||||
TOURNAMENT_SELECT_FIRST: 'ERROR_TOURNAMENT_SELECT_FIRST',
|
||||
|
||||
// Trainingstagebuch
|
||||
DIARY_DATE_NOT_FOUND: 'ERROR_DIARY_DATE_NOT_FOUND',
|
||||
DIARY_DATE_UPDATED: 'ERROR_DIARY_DATE_UPDATED',
|
||||
DIARY_NO_PARTICIPANTS: 'ERROR_DIARY_NO_PARTICIPANTS',
|
||||
DIARY_PDF_GENERATION_FAILED: 'ERROR_DIARY_PDF_GENERATION_FAILED',
|
||||
DIARY_IMAGE_LOAD_FAILED: 'ERROR_DIARY_IMAGE_LOAD_FAILED',
|
||||
DIARY_STATS_LOAD_FAILED: 'ERROR_DIARY_STATS_LOAD_FAILED',
|
||||
DIARY_NO_EXERCISE_DATA: 'ERROR_DIARY_NO_EXERCISE_DATA',
|
||||
DIARY_ACTIVITY_PARTICIPANTS_UPDATE_FAILED: 'ERROR_DIARY_ACTIVITY_PARTICIPANTS_UPDATE_FAILED',
|
||||
DIARY_GROUP_ASSIGNMENT_UPDATED: 'SUCCESS_DIARY_GROUP_ASSIGNMENT_UPDATED',
|
||||
DIARY_GROUP_ASSIGNMENT_UPDATE_FAILED: 'ERROR_DIARY_GROUP_ASSIGNMENT_UPDATE_FAILED',
|
||||
DIARY_ASSIGN_ALL_PARTICIPANTS_FAILED: 'ERROR_DIARY_ASSIGN_ALL_PARTICIPANTS_FAILED',
|
||||
DIARY_ASSIGN_GROUP_FAILED: 'ERROR_DIARY_ASSIGN_GROUP_FAILED',
|
||||
DIARY_PARTICIPANT_ASSIGN_FAILED: 'ERROR_DIARY_PARTICIPANT_ASSIGN_FAILED',
|
||||
DIARY_PARTICIPANT_GROUP_ASSIGNMENT_UPDATE_FAILED: 'ERROR_DIARY_PARTICIPANT_GROUP_ASSIGNMENT_UPDATE_FAILED',
|
||||
DIARY_MEMBER_CREATED: 'SUCCESS_DIARY_MEMBER_CREATED',
|
||||
DIARY_MEMBER_CREATE_FAILED: 'ERROR_DIARY_MEMBER_CREATE_FAILED',
|
||||
|
||||
// Team Management
|
||||
TEAM_NOT_LINKED_TO_LEAGUE: 'ERROR_TEAM_NOT_LINKED_TO_LEAGUE',
|
||||
TEAM_LINK_TO_LEAGUE_REQUIRED: 'ERROR_TEAM_LINK_TO_LEAGUE_REQUIRED',
|
||||
TEAM_PDF_LOAD_FAILED: 'ERROR_TEAM_PDF_LOAD_FAILED',
|
||||
TEAM_STATS_LOAD_FAILED: 'ERROR_TEAM_STATS_LOAD_FAILED',
|
||||
|
||||
// Aktivitäten
|
||||
ACTIVITY_IMAGE_DELETE_FAILED: 'ERROR_ACTIVITY_IMAGE_DELETE_FAILED',
|
||||
|
||||
// Offizielle Turniere
|
||||
OFFICIAL_TOURNAMENT_PDF_UPLOAD_SUCCESS: 'SUCCESS_OFFICIAL_TOURNAMENT_PDF_UPLOAD',
|
||||
OFFICIAL_TOURNAMENT_PDF_UPLOAD_FAILED: 'ERROR_OFFICIAL_TOURNAMENT_PDF_UPLOAD',
|
||||
|
||||
// Vereine
|
||||
CLUB_NOT_FOUND: 'ERROR_CLUB_NOT_FOUND',
|
||||
CLUB_ALREADY_EXISTS: 'ERROR_CLUB_ALREADY_EXISTS',
|
||||
CLUB_NAME_REQUIRED: 'ERROR_CLUB_NAME_REQUIRED',
|
||||
CLUB_NAME_TOO_SHORT: 'ERROR_CLUB_NAME_TOO_SHORT',
|
||||
|
||||
// Mitglieder-Übertragung
|
||||
MEMBER_TRANSFER_BULK_FAILED: 'ERROR_MEMBER_TRANSFER_BULK_FAILED',
|
||||
|
||||
// Training
|
||||
TRAINING_STATS_LOAD_FAILED: 'ERROR_TRAINING_STATS_LOAD_FAILED',
|
||||
|
||||
// Logs
|
||||
LOG_NOT_FOUND: 'ERROR_LOG_NOT_FOUND',
|
||||
};
|
||||
|
||||
/**
|
||||
* Erstellt ein Fehler-Objekt mit Code und optionalen Parametern
|
||||
* @param {string} code - Fehlercode aus ERROR_CODES
|
||||
* @param {object} params - Optionale Parameter für die Fehlermeldung
|
||||
* @returns {object} Fehler-Objekt mit code und params
|
||||
*/
|
||||
export function createError(code, params = null) {
|
||||
return {
|
||||
code,
|
||||
...(params && { params })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import diaryService from '../services/diaryService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
import { emitDiaryDateUpdated, emitDiaryTagAdded, emitDiaryTagRemoved } from '../services/socketService.js';
|
||||
const getDatesForClub = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
@@ -43,6 +43,10 @@ const updateTrainingTimes = async (req, res) => {
|
||||
throw new HttpError('notallfieldsfilled', 400);
|
||||
}
|
||||
const updatedDate = await diaryService.updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd);
|
||||
|
||||
// Emit Socket-Event
|
||||
emitDiaryDateUpdated(clubId, dateId, { trainingStart, trainingEnd });
|
||||
|
||||
res.status(200).json(updatedDate);
|
||||
} catch (error) {
|
||||
console.error('[updateTrainingTimes] - Error:', error);
|
||||
@@ -79,6 +83,14 @@ const addDiaryTag = async (req, res) => {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { diaryDateId, tagName } = req.body;
|
||||
const tags = await diaryService.addTagToDate(userToken, diaryDateId, tagName);
|
||||
|
||||
// Hole clubId für Event
|
||||
const { DiaryDate } = await import('../models/index.js');
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
if (diaryDate?.clubId && tags && tags.length > 0) {
|
||||
emitDiaryTagAdded(diaryDate.clubId, diaryDateId, tags[tags.length - 1]);
|
||||
}
|
||||
|
||||
res.status(201).json(tags);
|
||||
} catch (error) {
|
||||
console.error('[addDiaryTag] - Error:', error);
|
||||
@@ -95,6 +107,12 @@ const addTagToDiaryDate = async (req, res) => {
|
||||
return res.status(400).json({ message: 'diaryDateId and tagId are required.' });
|
||||
}
|
||||
const result = await diaryService.addTagToDiaryDate(userToken, clubId, diaryDateId, tagId);
|
||||
|
||||
// Emit Socket-Event
|
||||
if (result && result.tag) {
|
||||
emitDiaryTagAdded(clubId, diaryDateId, result.tag);
|
||||
}
|
||||
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[addTagToDiaryDate] - Error:', error);
|
||||
@@ -106,8 +124,20 @@ const deleteTagFromDiaryDate = async (req, res) => {
|
||||
try {
|
||||
const { tagId } = req.query;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const { clubId } = req.params;
|
||||
|
||||
// Hole diaryDateId vor dem Löschen
|
||||
const { DiaryDateTag } = await import('../models/index.js');
|
||||
const diaryDateTag = await DiaryDateTag.findByPk(tagId);
|
||||
const diaryDateId = diaryDateTag?.diaryDateId;
|
||||
|
||||
await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId);
|
||||
|
||||
// Emit Socket-Event
|
||||
if (diaryDateId) {
|
||||
emitDiaryTagRemoved(clubId, diaryDateId, tagId);
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Tag deleted' });
|
||||
} catch (error) {
|
||||
console.error('[deleteTag] - Error:', error);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import diaryDateActivityService from '../services/diaryDateActivityService.js';
|
||||
import { emitActivityChanged } from '../services/socketService.js';
|
||||
import DiaryDate from '../models/DiaryDates.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const createDiaryDateActivity = async (req, res) => {
|
||||
@@ -14,6 +16,13 @@ export const createDiaryDateActivity = async (req, res) => {
|
||||
orderId,
|
||||
isTimeblock,
|
||||
});
|
||||
|
||||
// Emit Socket-Event
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitActivityChanged(diaryDate.clubId, diaryDateId);
|
||||
}
|
||||
|
||||
res.status(201).json(activityItem);
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
@@ -34,6 +43,15 @@ export const updateDiaryDateActivity = async (req, res) => {
|
||||
orderId,
|
||||
groupId, // Pass groupId to the service
|
||||
});
|
||||
|
||||
// Emit Socket-Event
|
||||
if (updatedActivity?.diaryDateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(updatedActivity.diaryDateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitActivityChanged(diaryDate.clubId, updatedActivity.diaryDateId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(updatedActivity);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Error updating activity' });
|
||||
@@ -44,7 +62,22 @@ export const deleteDiaryDateActivity = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, id } = req.params;
|
||||
|
||||
// Hole diaryDateId vor dem Löschen
|
||||
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
|
||||
const activity = await DiaryDateActivity.findByPk(id);
|
||||
const diaryDateId = activity?.diaryDateId;
|
||||
|
||||
await diaryDateActivityService.deleteActivity(userToken, clubId, id);
|
||||
|
||||
// Emit Socket-Event
|
||||
if (diaryDateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitActivityChanged(diaryDate.clubId, diaryDateId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Activity deleted' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Error deleting activity' });
|
||||
@@ -57,6 +90,15 @@ export const updateDiaryDateActivityOrder = async (req, res) => {
|
||||
const { clubId, id } = req.params;
|
||||
const { orderId } = req.body;
|
||||
const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId);
|
||||
|
||||
// Emit Socket-Event
|
||||
if (updatedActivity?.diaryDateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(updatedActivity.diaryDateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitActivityChanged(diaryDate.clubId, updatedActivity.diaryDateId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(updatedActivity);
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
@@ -79,8 +121,15 @@ export const getDiaryDateActivities = async (req, res) => {
|
||||
export const addGroupActivity = async(req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, diaryDateId, groupId, activity, timeblockId } = req.body;
|
||||
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, timeblockId);
|
||||
const { clubId, diaryDateId, groupId, activity, predefinedActivityId, timeblockId } = req.body;
|
||||
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, predefinedActivityId, timeblockId);
|
||||
|
||||
// Emit Socket-Event
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitActivityChanged(diaryDate.clubId, diaryDateId);
|
||||
}
|
||||
|
||||
res.status(201).json(activityItem);
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
@@ -88,11 +137,61 @@ export const addGroupActivity = async(req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const updateGroupActivity = async(req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, groupActivityId } = req.params;
|
||||
const { predefinedActivityId } = req.body;
|
||||
const activityItem = await diaryDateActivityService.updateGroupActivity(userToken, clubId, groupActivityId, predefinedActivityId);
|
||||
|
||||
// Emit Socket-Event
|
||||
const GroupActivity = (await import('../models/GroupActivity.js')).default;
|
||||
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
|
||||
const groupActivity = await GroupActivity.findByPk(groupActivityId);
|
||||
let diaryDateId = null;
|
||||
if (groupActivity?.diaryDateActivity) {
|
||||
const activity = await DiaryDateActivity.findByPk(groupActivity.diaryDateActivity);
|
||||
diaryDateId = activity?.diaryDateId;
|
||||
}
|
||||
if (diaryDateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitActivityChanged(diaryDate.clubId, diaryDateId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(activityItem);
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Error updating group activity' });
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteGroupActivity = async(req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, groupActivityId } = req.params;
|
||||
|
||||
// Hole diaryDateId vor dem Löschen
|
||||
const GroupActivity = (await import('../models/GroupActivity.js')).default;
|
||||
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
|
||||
const groupActivity = await GroupActivity.findByPk(groupActivityId);
|
||||
let diaryDateId = null;
|
||||
if (groupActivity?.diaryDateActivity) {
|
||||
const activity = await DiaryDateActivity.findByPk(groupActivity.diaryDateActivity);
|
||||
diaryDateId = activity?.diaryDateId;
|
||||
}
|
||||
|
||||
await diaryDateActivityService.deleteGroupActivity(userToken, clubId, groupActivityId);
|
||||
|
||||
// Emit Socket-Event
|
||||
if (diaryDateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitActivityChanged(diaryDate.clubId, diaryDateId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Group activity deleted' });
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
|
||||
import DiaryDateActivity from '../models/DiaryDateActivity.js';
|
||||
import DiaryDates from '../models/DiaryDates.js';
|
||||
import Participant from '../models/Participant.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import { emitActivityMemberAdded, emitActivityMemberRemoved } from '../services/socketService.js';
|
||||
|
||||
export const getMembersForActivity = async (req, res) => {
|
||||
try {
|
||||
@@ -31,6 +34,13 @@ export const addMembersToActivity = async (req, res) => {
|
||||
|
||||
const validIds = new Set(validParticipants.map(p => p.id));
|
||||
const created = [];
|
||||
|
||||
// Hole clubId und dateId für Events (falls nicht aus params verfügbar)
|
||||
const activity = await DiaryDateActivity.findByPk(diaryDateActivityId);
|
||||
const diaryDate = activity ? await DiaryDates.findByPk(activity.diaryDateId) : null;
|
||||
const eventClubId = diaryDate?.clubId || clubId;
|
||||
const dateId = diaryDate?.id || null;
|
||||
|
||||
for (const pid of participantIds) {
|
||||
if (!validIds.has(pid)) {
|
||||
continue;
|
||||
@@ -39,6 +49,11 @@ export const addMembersToActivity = async (req, res) => {
|
||||
if (!existing) {
|
||||
const rec = await DiaryMemberActivity.create({ diaryDateActivityId, participantId: pid });
|
||||
created.push(rec);
|
||||
|
||||
// Emit Socket-Event
|
||||
if (eventClubId && dateId) {
|
||||
emitActivityMemberAdded(eventClubId, diaryDateActivityId, pid, dateId);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
@@ -54,7 +69,19 @@ export const removeMemberFromActivity = async (req, res) => {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, diaryDateActivityId, participantId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
// Hole dateId für Event
|
||||
const activity = await DiaryDateActivity.findByPk(diaryDateActivityId);
|
||||
const diaryDate = activity ? await DiaryDates.findByPk(activity.diaryDateId) : null;
|
||||
const dateId = diaryDate?.id || null;
|
||||
|
||||
await DiaryMemberActivity.destroy({ where: { diaryDateActivityId, participantId } });
|
||||
|
||||
// Emit Socket-Event
|
||||
if (dateId) {
|
||||
emitActivityMemberRemoved(clubId, diaryDateActivityId, participantId, dateId);
|
||||
}
|
||||
|
||||
res.status(200).json({ ok: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Error removing member from activity' });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DiaryNote, DiaryTag } from '../models/index.js';
|
||||
import { DiaryNote, DiaryTag, DiaryDate } from '../models/index.js';
|
||||
import diaryService from '../services/diaryService.js';
|
||||
import { emitDiaryNoteAdded, emitDiaryNoteDeleted } from '../services/socketService.js';
|
||||
|
||||
export const getNotes = async (req, res) => {
|
||||
try {
|
||||
@@ -26,6 +27,9 @@ export const createNote = async (req, res) => {
|
||||
|
||||
const newNote = await DiaryNote.create({ memberId, diaryDateId, content });
|
||||
|
||||
// Hole DiaryDate für clubId
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
|
||||
if (Array.isArray(tags) && tags.length > 0 && typeof newNote.addTags === 'function') {
|
||||
const tagInstances = await DiaryTag.findAll({ where: { id: tags } });
|
||||
await newNote.addTags(tagInstances);
|
||||
@@ -34,9 +38,19 @@ export const createNote = async (req, res) => {
|
||||
include: [{ model: DiaryTag, as: 'tags', required: false }],
|
||||
});
|
||||
|
||||
// Emit Socket-Event
|
||||
if (diaryDate?.clubId) {
|
||||
emitDiaryNoteAdded(diaryDate.clubId, diaryDateId, noteWithTags ?? newNote);
|
||||
}
|
||||
|
||||
return res.status(201).json(noteWithTags ?? newNote);
|
||||
}
|
||||
|
||||
// Emit Socket-Event
|
||||
if (diaryDate?.clubId) {
|
||||
emitDiaryNoteAdded(diaryDate.clubId, diaryDateId, newNote);
|
||||
}
|
||||
|
||||
res.status(201).json(newNote);
|
||||
} catch (error) {
|
||||
console.error('[createNote] - Error:', error);
|
||||
@@ -47,7 +61,25 @@ export const createNote = async (req, res) => {
|
||||
export const deleteNote = async (req, res) => {
|
||||
try {
|
||||
const { noteId } = req.params;
|
||||
|
||||
// Hole Note für diaryDateId vor dem Löschen
|
||||
const note = await DiaryNote.findByPk(noteId);
|
||||
const diaryDateId = note?.diaryDateId;
|
||||
|
||||
// Hole DiaryDate für clubId
|
||||
let clubId = null;
|
||||
if (diaryDateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
clubId = diaryDate?.clubId;
|
||||
}
|
||||
|
||||
await DiaryNote.destroy({ where: { id: noteId } });
|
||||
|
||||
// Emit Socket-Event
|
||||
if (clubId && diaryDateId) {
|
||||
emitDiaryNoteDeleted(clubId, diaryDateId, noteId);
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Note deleted' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Error deleting note' });
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import groupService from '../services/groupService.js';
|
||||
import { emitActivityChanged, emitGroupChanged } from '../services/socketService.js';
|
||||
import DiaryDate from '../models/DiaryDates.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const addGroup = async(req, res) => {
|
||||
@@ -7,6 +9,15 @@ const addGroup = async(req, res) => {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubid: clubId, dateid: dateId, name, lead } = req.body;
|
||||
const result = await groupService.addGroup(userToken, clubId, dateId, name, lead);
|
||||
|
||||
// Emit Socket-Event für Gruppen-Änderungen
|
||||
if (dateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(dateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitGroupChanged(diaryDate.clubId, dateId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json(result);
|
||||
} catch (error) {
|
||||
console.error('[addGroup] - Error:', error);
|
||||
@@ -33,6 +44,15 @@ const changeGroup = async(req, res) => {
|
||||
const { groupId } = req.params;
|
||||
const { clubid: clubId, dateid: dateId, name, lead } = req.body;
|
||||
const result = await groupService.changeGroup(userToken, groupId, clubId, dateId, name, lead);
|
||||
|
||||
// Emit Socket-Event für Gruppen-Änderungen
|
||||
if (dateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(dateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitGroupChanged(diaryDate.clubId, dateId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[changeGroup] - Error:', error);
|
||||
@@ -40,4 +60,27 @@ const changeGroup = async(req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
export { addGroup, getGroups, changeGroup};
|
||||
const deleteGroup = async(req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { groupId } = req.params;
|
||||
const { clubid: clubId, dateid: dateId } = req.body;
|
||||
const result = await groupService.deleteGroup(userToken, groupId, clubId, dateId);
|
||||
|
||||
// Emit Socket-Events für Gruppen- und Aktivitäts-Änderungen (Gruppen werden in Aktivitäten verwendet)
|
||||
if (dateId) {
|
||||
const diaryDate = await DiaryDate.findByPk(dateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitGroupChanged(diaryDate.clubId, dateId);
|
||||
emitActivityChanged(diaryDate.clubId, dateId);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[deleteGroup] - Error:', error);
|
||||
res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
export { addGroup, getGroups, changeGroup, deleteGroup};
|
||||
@@ -49,13 +49,19 @@ export const getMemberActivities = async (req, res) => {
|
||||
|
||||
const participantIds = participants.map(p => p.id);
|
||||
|
||||
// Get all diary member activities for this member
|
||||
const whereClause = {
|
||||
participantId: participantIds
|
||||
};
|
||||
// Sammle alle Gruppen-IDs, zu denen der Member gehört
|
||||
const memberGroupIds = new Set();
|
||||
participants.forEach(p => {
|
||||
if (p.groupId !== null && p.groupId !== undefined) {
|
||||
memberGroupIds.add(p.groupId);
|
||||
}
|
||||
});
|
||||
|
||||
// 1. Get all diary member activities explicitly assigned to this member
|
||||
const memberActivities = await DiaryMemberActivity.findAll({
|
||||
where: whereClause,
|
||||
where: {
|
||||
participantId: participantIds
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Participant,
|
||||
@@ -90,47 +96,186 @@ export const getMemberActivities = async (req, res) => {
|
||||
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']]
|
||||
});
|
||||
|
||||
// Group activities by name and count occurrences, considering group assignment
|
||||
// 2. Get all group activities for groups the member belongs to
|
||||
const groupActivities = [];
|
||||
if (memberGroupIds.size > 0) {
|
||||
// Suche direkt nach GroupActivity-Einträgen für die Gruppen des Members
|
||||
const groupActivitiesData = await GroupActivity.findAll({
|
||||
where: {
|
||||
groupId: {
|
||||
[Op.in]: Array.from(memberGroupIds)
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: DiaryDateActivity,
|
||||
as: 'activityGroupActivity',
|
||||
include: [
|
||||
{
|
||||
model: DiaryDates,
|
||||
as: 'diaryDate',
|
||||
where: startDate ? {
|
||||
date: {
|
||||
[Op.gte]: startDate
|
||||
}
|
||||
} : {}
|
||||
},
|
||||
{
|
||||
model: PredefinedActivity,
|
||||
as: 'predefinedActivity',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: PredefinedActivity,
|
||||
as: 'groupPredefinedActivity',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// Erstelle virtuelle DiaryMemberActivity-Objekte für Gruppen-Aktivitäten
|
||||
for (const groupActivity of groupActivitiesData) {
|
||||
if (!groupActivity.activityGroupActivity || !groupActivity.activityGroupActivity.diaryDate) {
|
||||
continue; // Überspringe, wenn keine DiaryDateActivity oder kein DiaryDate vorhanden
|
||||
}
|
||||
|
||||
const activity = groupActivity.activityGroupActivity;
|
||||
const diaryDateId = activity.diaryDateId;
|
||||
|
||||
// Finde alle relevanten Participants für dieses DiaryDate
|
||||
const relevantParticipants = participants.filter(p =>
|
||||
p.diaryDateId === diaryDateId &&
|
||||
p.groupId === groupActivity.groupId
|
||||
);
|
||||
|
||||
for (const participant of relevantParticipants) {
|
||||
// Verwende die PredefinedActivity aus GroupActivity, falls vorhanden
|
||||
// Sonst die aus DiaryDateActivity
|
||||
const predefinedActivity = groupActivity.groupPredefinedActivity || activity.predefinedActivity;
|
||||
|
||||
if (predefinedActivity) {
|
||||
// Erstelle ein modifiziertes Activity-Objekt
|
||||
const modifiedActivity = {
|
||||
...activity.toJSON(),
|
||||
predefinedActivity: predefinedActivity
|
||||
};
|
||||
groupActivities.push({
|
||||
activity: modifiedActivity,
|
||||
participant: participant,
|
||||
id: null // Virtuell, nicht in DB
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter: explizite Zuordnungen sollen nur dann zählen, wenn
|
||||
// - der Participant keine Gruppe hat UND die Aktivität KEINE Gruppenbindung hat, oder
|
||||
// - die Aktivität keine Gruppenbindung hat, oder
|
||||
// - es eine Gruppenbindung gibt, die zur Gruppe des Participants passt.
|
||||
const filteredMemberActivities = memberActivities.filter((ma) => {
|
||||
if (!ma?.participant || !ma?.activity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const participantGroupId = ma.participant.groupId;
|
||||
const groupActivitiesForActivity = ma.activity.groupActivities || [];
|
||||
|
||||
// Participant ohne Gruppe -> nur Aktivitäten ohne Gruppenbindung zählen
|
||||
if (participantGroupId === null || participantGroupId === undefined) {
|
||||
return !groupActivitiesForActivity.length;
|
||||
}
|
||||
|
||||
// Keine Gruppenbindung -> immer zählen
|
||||
if (!groupActivitiesForActivity.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Gruppenbindung vorhanden -> nur zählen, wenn die Gruppe passt
|
||||
return groupActivitiesForActivity.some((ga) => Number(ga.groupId) === Number(participantGroupId));
|
||||
});
|
||||
|
||||
// 3. Kombiniere beide Listen und entferne Duplikate
|
||||
// Ein Duplikat liegt vor, wenn dieselbe Aktivität für denselben Participant bereits explizit zugeordnet ist
|
||||
const explicitActivityKeys = new Set();
|
||||
filteredMemberActivities.forEach(ma => {
|
||||
if (ma.activity && ma.activity.id && ma.participant && ma.participant.id) {
|
||||
// Erstelle einen eindeutigen Schlüssel: activityId-participantId
|
||||
const key = `${ma.activity.id}-${ma.participant.id}`;
|
||||
explicitActivityKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Filtere Gruppen-Aktivitäten, die bereits explizit zugeordnet sind
|
||||
const uniqueGroupActivities = groupActivities.filter(ga => {
|
||||
if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) {
|
||||
return false;
|
||||
}
|
||||
const key = `${ga.activity.id}-${ga.participant.id}`;
|
||||
return !explicitActivityKeys.has(key);
|
||||
});
|
||||
|
||||
// Kombiniere beide Listen
|
||||
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
|
||||
|
||||
// Group activities by name and count occurrences
|
||||
// Verwende einen Set pro Aktivität, um eindeutige Datum-Aktivität-Kombinationen zu tracken
|
||||
const activityMap = new Map();
|
||||
|
||||
for (const ma of memberActivities) {
|
||||
for (const ma of allActivities) {
|
||||
if (!ma.activity || !ma.activity.predefinedActivity || !ma.participant) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check group assignment
|
||||
const participantGroupId = ma.participant.groupId;
|
||||
const activityGroupIds = ma.activity.groupActivities?.map(ga => ga.groupId) || [];
|
||||
|
||||
// Filter: Only count if:
|
||||
// 1. Activity has no group assignment (empty activityGroupIds) - activity is for all groups OR
|
||||
// 2. Participant's group matches one of the activity's groups
|
||||
const shouldCount = activityGroupIds.length === 0 ||
|
||||
(participantGroupId !== null && activityGroupIds.includes(participantGroupId));
|
||||
|
||||
if (!shouldCount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const activity = ma.activity.predefinedActivity;
|
||||
const activityName = activity.name;
|
||||
const activityCode = activity.code || activity.name; // Verwende Code falls vorhanden, sonst Name
|
||||
const date = ma.activity.diaryDate?.date;
|
||||
|
||||
if (!activityMap.has(activityName)) {
|
||||
activityMap.set(activityName, {
|
||||
name: activityName,
|
||||
count: 0,
|
||||
if (!date) {
|
||||
continue; // Überspringe Einträge ohne Datum
|
||||
}
|
||||
|
||||
// Verwende Code als Key, falls vorhanden, sonst Name
|
||||
const key = activityCode;
|
||||
|
||||
if (!activityMap.has(key)) {
|
||||
activityMap.set(key, {
|
||||
name: activityName, // Vollständiger Name für Tooltip
|
||||
code: activityCode, // Code/Kürzel für Anzeige
|
||||
uniqueDates: new Set(), // Set für eindeutige Daten
|
||||
dates: []
|
||||
});
|
||||
}
|
||||
|
||||
const activityData = activityMap.get(activityName);
|
||||
activityData.count++;
|
||||
if (date) {
|
||||
const activityData = activityMap.get(key);
|
||||
// Konvertiere Datum zu String für Set-Vergleich (nur Datum, keine Zeit)
|
||||
const dateString = date instanceof Date
|
||||
? date.toISOString().split('T')[0]
|
||||
: new Date(date).toISOString().split('T')[0];
|
||||
|
||||
// Füge Datum nur hinzu, wenn es noch nicht vorhanden ist
|
||||
if (!activityData.uniqueDates.has(dateString)) {
|
||||
activityData.uniqueDates.add(dateString);
|
||||
activityData.dates.push(date);
|
||||
}
|
||||
}
|
||||
|
||||
// Konvertiere Sets zu Arrays und setze count basierend auf eindeutigen Daten
|
||||
activityMap.forEach((activityData, key) => {
|
||||
activityData.count = activityData.uniqueDates.size;
|
||||
// Sortiere Daten (neueste zuerst)
|
||||
activityData.dates.sort((a, b) => {
|
||||
const dateA = new Date(a);
|
||||
const dateB = new Date(b);
|
||||
return dateB - dateA;
|
||||
});
|
||||
// Entferne uniqueDates, da es nicht an Frontend gesendet werden muss
|
||||
delete activityData.uniqueDates;
|
||||
});
|
||||
|
||||
// Convert map to array and sort by count
|
||||
const activities = Array.from(activityMap.values())
|
||||
.sort((a, b) => b.count - a.count);
|
||||
@@ -162,7 +307,15 @@ export const getMemberLastParticipations = async (req, res) => {
|
||||
|
||||
const participantIds = participants.map(p => p.id);
|
||||
|
||||
// Get last participations for this member
|
||||
// Sammle alle Gruppen-IDs, zu denen der Member gehört
|
||||
const memberGroupIds = new Set();
|
||||
participants.forEach(p => {
|
||||
if (p.groupId !== null && p.groupId !== undefined) {
|
||||
memberGroupIds.add(p.groupId);
|
||||
}
|
||||
});
|
||||
|
||||
// 1. Get last participations explicitly assigned to this member
|
||||
const memberActivities = await DiaryMemberActivity.findAll({
|
||||
where: {
|
||||
participantId: participantIds
|
||||
@@ -196,31 +349,177 @@ export const getMemberLastParticipations = async (req, res) => {
|
||||
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
|
||||
limit: parseInt(limit) * 10 // Get more to filter by group
|
||||
});
|
||||
|
||||
// Siehe getMemberActivities(): nur zählen, wenn Gruppenbindung passt (oder keine existiert)
|
||||
const filteredMemberActivities = memberActivities.filter((ma) => {
|
||||
if (!ma?.participant || !ma?.activity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const participantGroupId = ma.participant.groupId;
|
||||
const groupActivitiesForActivity = ma.activity.groupActivities || [];
|
||||
|
||||
if (!groupActivitiesForActivity.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return groupActivitiesForActivity.some((ga) => Number(ga.groupId) === Number(participantGroupId));
|
||||
});
|
||||
|
||||
// Format the results, considering group assignment
|
||||
const participations = memberActivities
|
||||
// 2. Get all group activities for groups the member belongs to
|
||||
const groupActivities = [];
|
||||
if (memberGroupIds.size > 0) {
|
||||
// Suche direkt nach GroupActivity-Einträgen für die Gruppen des Members
|
||||
const groupActivitiesData = await GroupActivity.findAll({
|
||||
where: {
|
||||
groupId: {
|
||||
[Op.in]: Array.from(memberGroupIds)
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: DiaryDateActivity,
|
||||
as: 'activityGroupActivity',
|
||||
include: [
|
||||
{
|
||||
model: DiaryDates,
|
||||
as: 'diaryDate'
|
||||
},
|
||||
{
|
||||
model: PredefinedActivity,
|
||||
as: 'predefinedActivity',
|
||||
required: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: PredefinedActivity,
|
||||
as: 'groupPredefinedActivity',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [[{ model: DiaryDateActivity, as: 'activityGroupActivity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
|
||||
limit: parseInt(limit) * 10 // Get more to filter
|
||||
});
|
||||
|
||||
// Erstelle virtuelle DiaryMemberActivity-Objekte für Gruppen-Aktivitäten
|
||||
for (const groupActivity of groupActivitiesData) {
|
||||
if (!groupActivity.activityGroupActivity || !groupActivity.activityGroupActivity.diaryDate) {
|
||||
continue; // Überspringe, wenn keine DiaryDateActivity oder kein DiaryDate vorhanden
|
||||
}
|
||||
|
||||
const activity = groupActivity.activityGroupActivity;
|
||||
const diaryDateId = activity.diaryDateId;
|
||||
|
||||
// Finde alle relevanten Participants für dieses DiaryDate
|
||||
const relevantParticipants = participants.filter(p =>
|
||||
p.diaryDateId === diaryDateId &&
|
||||
p.groupId === groupActivity.groupId
|
||||
);
|
||||
|
||||
for (const participant of relevantParticipants) {
|
||||
// Verwende die PredefinedActivity aus GroupActivity, falls vorhanden
|
||||
// Sonst die aus DiaryDateActivity
|
||||
const predefinedActivity = groupActivity.groupPredefinedActivity || activity.predefinedActivity;
|
||||
|
||||
if (predefinedActivity) {
|
||||
// Erstelle ein modifiziertes Activity-Objekt
|
||||
const modifiedActivity = {
|
||||
...activity.toJSON(),
|
||||
predefinedActivity: predefinedActivity
|
||||
};
|
||||
groupActivities.push({
|
||||
activity: modifiedActivity,
|
||||
participant: participant,
|
||||
id: null // Virtuell, nicht in DB
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Kombiniere beide Listen und entferne Duplikate
|
||||
// Ein Duplikat liegt vor, wenn dieselbe Aktivität für denselben Participant bereits explizit zugeordnet ist
|
||||
const explicitActivityKeys = new Set();
|
||||
filteredMemberActivities.forEach(ma => {
|
||||
if (ma.activity && ma.activity.id && ma.participant && ma.participant.id) {
|
||||
// Erstelle einen eindeutigen Schlüssel: activityId-participantId
|
||||
const key = `${ma.activity.id}-${ma.participant.id}`;
|
||||
explicitActivityKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
// Filtere Gruppen-Aktivitäten, die bereits explizit zugeordnet sind
|
||||
const uniqueGroupActivities = groupActivities.filter(ga => {
|
||||
if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) {
|
||||
return false;
|
||||
}
|
||||
const key = `${ga.activity.id}-${ga.participant.id}`;
|
||||
return !explicitActivityKeys.has(key);
|
||||
});
|
||||
|
||||
// Kombiniere beide Listen
|
||||
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
|
||||
|
||||
// Gruppiere nach Datum
|
||||
const participationsByDate = new Map();
|
||||
|
||||
allActivities
|
||||
.filter(ma => {
|
||||
if (!ma.activity || !ma.activity.predefinedActivity || !ma.activity.diaryDate || !ma.participant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check group assignment
|
||||
const participantGroupId = ma.participant.groupId;
|
||||
const activityGroupIds = ma.activity.groupActivities?.map(ga => ga.groupId) || [];
|
||||
|
||||
// Filter: Only count if:
|
||||
// 1. Activity has no group assignment (empty activityGroupIds) - activity is for all groups OR
|
||||
// 2. Participant's group matches one of the activity's groups
|
||||
return activityGroupIds.length === 0 ||
|
||||
(participantGroupId !== null && activityGroupIds.includes(participantGroupId));
|
||||
return true;
|
||||
})
|
||||
.slice(0, parseInt(limit)) // Limit after filtering
|
||||
.map(ma => ({
|
||||
id: ma.id,
|
||||
activityName: ma.activity.predefinedActivity.name,
|
||||
date: ma.activity.diaryDate.date,
|
||||
diaryDateId: ma.activity.diaryDate.id
|
||||
}));
|
||||
.forEach(ma => {
|
||||
const date = ma.activity.diaryDate.date;
|
||||
const diaryDateId = ma.activity.diaryDate.id;
|
||||
const activity = ma.activity.predefinedActivity;
|
||||
const activityName = activity.name;
|
||||
const activityCode = activity.code || activity.name;
|
||||
|
||||
if (!participationsByDate.has(date)) {
|
||||
participationsByDate.set(date, {
|
||||
date: date,
|
||||
diaryDateId: diaryDateId,
|
||||
activities: []
|
||||
});
|
||||
}
|
||||
|
||||
const dateEntry = participationsByDate.get(date);
|
||||
// Füge Aktivität nur hinzu, wenn sie noch nicht vorhanden ist (vermeide Duplikate)
|
||||
// Speichere sowohl code als auch name
|
||||
const activityEntry = {
|
||||
code: activityCode,
|
||||
name: activityName
|
||||
};
|
||||
if (!dateEntry.activities.find(a => (a.code || a.name) === activityCode)) {
|
||||
dateEntry.activities.push(activityEntry);
|
||||
}
|
||||
});
|
||||
|
||||
// Sortiere nach Datum (neueste zuerst) und nehme die letzten N Daten
|
||||
const sortedDates = Array.from(participationsByDate.values())
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.date);
|
||||
const dateB = new Date(b.date);
|
||||
return dateB - dateA;
|
||||
})
|
||||
.slice(0, parseInt(limit));
|
||||
|
||||
// Formatiere für das Frontend: Flache Liste mit Datum und Aktivität
|
||||
const participations = [];
|
||||
sortedDates.forEach(dateEntry => {
|
||||
dateEntry.activities.forEach(activity => {
|
||||
participations.push({
|
||||
id: null, // Virtuell
|
||||
activityName: activity.code || activity.name, // Code für Anzeige
|
||||
activityFullName: activity.name, // Vollständiger Name für Tooltip
|
||||
date: dateEntry.date,
|
||||
diaryDateId: dateEntry.diaryDateId
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return res.status(200).json(participations);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import MemberService from "../services/memberService.js";
|
||||
import MemberTransferService from "../services/memberTransferService.js";
|
||||
import { emitMemberChanged } from '../services/socketService.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const getClubMembers = async(req, res) => {
|
||||
@@ -32,6 +33,12 @@ const setClubMembers = async (req, res) => {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate,
|
||||
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts);
|
||||
|
||||
// Emit Socket-Event wenn Member erfolgreich erstellt/aktualisiert wurde
|
||||
if (addResult.status === 200) {
|
||||
emitMemberChanged(clubId);
|
||||
}
|
||||
|
||||
res.status(addResult.status || 500).json(addResult.response);
|
||||
} catch (error) {
|
||||
console.error('[setClubMembers] - Error:', error);
|
||||
@@ -124,10 +131,14 @@ const generateMemberGallery = async (req, res) => {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const size = parseInt(req.query.size) || 200; // Default: 200x200
|
||||
const format = req.query.format || 'image'; // 'image' or 'json'
|
||||
const result = await MemberService.generateMemberGallery(userToken, clubId, size);
|
||||
|
||||
// Bei format=json wird kein Bild erstellt, nur die Mitgliederliste zurückgegeben
|
||||
const createImage = format !== 'json';
|
||||
const result = await MemberService.generateMemberGallery(userToken, clubId, size, createImage);
|
||||
|
||||
if (result.status === 200) {
|
||||
if (format === 'json') {
|
||||
// Return member information for interactive gallery
|
||||
// Return member information for interactive gallery (ohne Bild zu erstellen)
|
||||
return res.status(200).json({
|
||||
members: result.galleryEntries.map(entry => ({
|
||||
memberId: entry.memberId,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import myTischtennisService from '../services/myTischtennisService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import axios from 'axios';
|
||||
|
||||
class MyTischtennisController {
|
||||
/**
|
||||
@@ -199,6 +200,292 @@ class MyTischtennisController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mytischtennis/login-page
|
||||
* Proxy für Login-Seite (für iframe)
|
||||
* Lädt die Login-Seite von mytischtennis.de und modifiziert sie, sodass Form-Submissions über unseren Proxy gehen
|
||||
* Authentifizierung ist optional - Token kann als Query-Parameter übergeben werden
|
||||
*/
|
||||
async getLoginPage(req, res, next) {
|
||||
try {
|
||||
// Versuche, userId aus Token zu bekommen (optional)
|
||||
let userId = null;
|
||||
const token = req.query.token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode'];
|
||||
if (token) {
|
||||
try {
|
||||
const jwt = (await import('jsonwebtoken')).default;
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
userId = decoded.userId;
|
||||
} catch (err) {
|
||||
// Token ungültig - ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Speichere userId im Request für submitLogin
|
||||
req.userId = userId;
|
||||
|
||||
// Lade die Login-Seite von mytischtennis.de
|
||||
const response = await axios.get('https://www.mytischtennis.de/login?next=%2F', {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
|
||||
},
|
||||
maxRedirects: 5,
|
||||
validateStatus: () => true // Akzeptiere alle Status-Codes
|
||||
});
|
||||
|
||||
// Setze Cookies aus der Response
|
||||
const setCookieHeaders = response.headers['set-cookie'];
|
||||
if (setCookieHeaders) {
|
||||
res.setHeader('Set-Cookie', setCookieHeaders);
|
||||
}
|
||||
|
||||
// Modifiziere HTML: Ändere Form-Action auf unseren Proxy
|
||||
let html = response.data;
|
||||
if (typeof html === 'string') {
|
||||
// Füge Token als Hidden-Input hinzu, damit submitLogin die userId bekommt
|
||||
const tokenInput = userId ? `<input type="hidden" name="__token" value="${token}" />` : '';
|
||||
|
||||
// Ersetze Form-Action URLs und füge Token-Input hinzu
|
||||
html = html.replace(
|
||||
/(<form[^>]*action="[^"]*\/login[^"]*"[^>]*>)/g,
|
||||
`$1${tokenInput}`
|
||||
);
|
||||
html = html.replace(
|
||||
/action="([^"]*\/login[^"]*)"/g,
|
||||
'action="/api/mytischtennis/login-submit"'
|
||||
);
|
||||
// Ersetze auch relative URLs
|
||||
html = html.replace(
|
||||
/action="\/login/g,
|
||||
'action="/api/mytischtennis/login-submit'
|
||||
);
|
||||
}
|
||||
|
||||
// Setze Content-Type
|
||||
res.setHeader('Content-Type', response.headers['content-type'] || 'text/html; charset=utf-8');
|
||||
|
||||
// Sende den modifizierten HTML-Inhalt
|
||||
res.status(response.status).send(html);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Login-Seite:', error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/login-submit
|
||||
* Proxy für Login-Form-Submission
|
||||
* Leitet den Login-Request durch, damit Cookies im Backend-Kontext bleiben
|
||||
* Authentifizierung ist optional - iframe kann keinen Token mitsenden
|
||||
*/
|
||||
async submitLogin(req, res, next) {
|
||||
try {
|
||||
// Versuche, userId aus Token zu bekommen (aus Query-Parameter oder Hidden-Input)
|
||||
let userId = null;
|
||||
const token = req.query.token || req.body.__token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode'];
|
||||
if (token) {
|
||||
try {
|
||||
const jwt = (await import('jsonwebtoken')).default;
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
userId = decoded.userId;
|
||||
} catch (err) {
|
||||
// Token ungültig - ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Entferne __token aus req.body, damit es nicht an mytischtennis.de gesendet wird
|
||||
if (req.body.__token) {
|
||||
delete req.body.__token;
|
||||
}
|
||||
|
||||
// Hole Cookies aus dem Request
|
||||
const cookies = req.headers.cookie || '';
|
||||
|
||||
// Leite den Login-Request an mytischtennis.de weiter
|
||||
const response = await axios.post(
|
||||
'https://www.mytischtennis.de/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
|
||||
req.body, // Form-Daten
|
||||
{
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept': '*/*',
|
||||
'Referer': 'https://www.mytischtennis.de/login?next=%2F'
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: () => true
|
||||
}
|
||||
);
|
||||
|
||||
// Setze Cookies aus der Response
|
||||
const setCookieHeaders = response.headers['set-cookie'];
|
||||
if (setCookieHeaders) {
|
||||
res.setHeader('Set-Cookie', setCookieHeaders);
|
||||
}
|
||||
|
||||
// Setze andere relevante Headers
|
||||
if (response.headers['content-type']) {
|
||||
res.setHeader('Content-Type', response.headers['content-type']);
|
||||
}
|
||||
if (response.headers['location']) {
|
||||
res.setHeader('Location', response.headers['location']);
|
||||
}
|
||||
|
||||
// Prüfe, ob Login erfolgreich war (durch Prüfung der Cookies)
|
||||
const authCookie = setCookieHeaders?.find(cookie => cookie.startsWith('sb-10-auth-token='));
|
||||
if (authCookie && userId) {
|
||||
// Login erfolgreich - speichere Session (nur wenn userId vorhanden)
|
||||
await this.saveSessionFromCookie(userId, authCookie);
|
||||
}
|
||||
|
||||
// Sende Response weiter
|
||||
res.status(response.status).send(response.data);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Login-Submit:', error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichere Session-Daten aus Cookie
|
||||
*/
|
||||
async saveSessionFromCookie(userId, cookieString) {
|
||||
try {
|
||||
const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/);
|
||||
if (!tokenMatch) {
|
||||
throw new Error('Token-Format ungültig');
|
||||
}
|
||||
|
||||
const base64Token = tokenMatch[1];
|
||||
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
|
||||
const tokenData = JSON.parse(decodedToken);
|
||||
|
||||
const MyTischtennis = (await import('../models/MyTischtennis.js')).default;
|
||||
const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
if (myTischtennisAccount) {
|
||||
myTischtennisAccount.accessToken = tokenData.access_token;
|
||||
myTischtennisAccount.refreshToken = tokenData.refresh_token;
|
||||
myTischtennisAccount.expiresAt = tokenData.expires_at;
|
||||
myTischtennisAccount.cookie = cookieString.split(';')[0].trim();
|
||||
myTischtennisAccount.userData = tokenData.user;
|
||||
myTischtennisAccount.lastLoginSuccess = new Date();
|
||||
myTischtennisAccount.lastLoginAttempt = new Date();
|
||||
|
||||
// Hole Club-Informationen
|
||||
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
|
||||
const profileResult = await myTischtennisClient.getUserProfile(myTischtennisAccount.cookie);
|
||||
if (profileResult.success) {
|
||||
myTischtennisAccount.clubId = profileResult.clubId;
|
||||
myTischtennisAccount.clubName = profileResult.clubName;
|
||||
myTischtennisAccount.fedNickname = profileResult.fedNickname;
|
||||
}
|
||||
|
||||
await myTischtennisAccount.save();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Session:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/extract-session
|
||||
* Extrahiere Session nach Login im iframe
|
||||
* Versucht, die Session-Daten aus den Cookies zu extrahieren
|
||||
* Authentifizierung ist optional - iframe kann keinen Token mitsenden
|
||||
*/
|
||||
async extractSession(req, res, next) {
|
||||
try {
|
||||
// Versuche, userId aus Token zu bekommen (optional)
|
||||
let userId = req.user?.id;
|
||||
|
||||
// Falls kein Token vorhanden, versuche userId aus Account zu bekommen (falls E-Mail bekannt)
|
||||
if (!userId) {
|
||||
// Kann nicht ohne Authentifizierung arbeiten - Session kann nicht gespeichert werden
|
||||
return res.status(401).json({
|
||||
error: 'Authentifizierung erforderlich zum Speichern der Session'
|
||||
});
|
||||
}
|
||||
|
||||
// Hole die Cookies aus dem Request
|
||||
const cookies = req.headers.cookie || '';
|
||||
|
||||
// Versuche, die Session zu verifizieren, indem wir einen Request mit den Cookies machen
|
||||
const response = await axios.get('https://www.mytischtennis.de/?_data=root', {
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
// Prüfe, ob wir eingeloggt sind (durch Prüfung der Response)
|
||||
if (response.status === 200 && response.data?.userProfile) {
|
||||
// Session erfolgreich - speichere die Daten
|
||||
const account = await myTischtennisService.getAccount(userId);
|
||||
if (!account) {
|
||||
throw new HttpError('Kein myTischtennis-Account verknüpft', 404);
|
||||
}
|
||||
|
||||
// Extrahiere Cookie-String
|
||||
const cookieString = cookies.split(';').find(c => c.trim().startsWith('sb-10-auth-token='));
|
||||
if (!cookieString) {
|
||||
throw new HttpError('Kein Auth-Token in Cookies gefunden', 400);
|
||||
}
|
||||
|
||||
// Parse Token aus Cookie
|
||||
const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/);
|
||||
if (!tokenMatch) {
|
||||
throw new HttpError('Token-Format ungültig', 400);
|
||||
}
|
||||
|
||||
const base64Token = tokenMatch[1];
|
||||
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
|
||||
const tokenData = JSON.parse(decodedToken);
|
||||
|
||||
// Aktualisiere Account mit Session-Daten
|
||||
const MyTischtennis = (await import('../models/MyTischtennis.js')).default;
|
||||
const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
if (myTischtennisAccount) {
|
||||
myTischtennisAccount.accessToken = tokenData.access_token;
|
||||
myTischtennisAccount.refreshToken = tokenData.refresh_token;
|
||||
myTischtennisAccount.expiresAt = tokenData.expires_at;
|
||||
myTischtennisAccount.cookie = cookieString.trim();
|
||||
myTischtennisAccount.userData = tokenData.user;
|
||||
myTischtennisAccount.lastLoginSuccess = new Date();
|
||||
myTischtennisAccount.lastLoginAttempt = new Date();
|
||||
|
||||
// Hole Club-Informationen
|
||||
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
|
||||
const profileResult = await myTischtennisClient.getUserProfile(cookieString.trim());
|
||||
if (profileResult.success) {
|
||||
myTischtennisAccount.clubId = profileResult.clubId;
|
||||
myTischtennisAccount.clubName = profileResult.clubName;
|
||||
myTischtennisAccount.fedNickname = profileResult.fedNickname;
|
||||
}
|
||||
|
||||
await myTischtennisAccount.save();
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Session erfolgreich extrahiert und gespeichert'
|
||||
});
|
||||
} else {
|
||||
throw new HttpError('Nicht eingeloggt oder Session ungültig', 401);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Extrahieren der Session:', error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MyTischtennisController();
|
||||
|
||||
@@ -233,9 +233,11 @@ export const listOfficialTournaments = async (req, res) => {
|
||||
const { clubId } = req.params;
|
||||
await checkAccess(userToken, clubId);
|
||||
const list = await OfficialTournament.findAll({ where: { clubId } });
|
||||
res.status(200).json(list);
|
||||
res.status(200).json(Array.isArray(list) ? list : []);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Failed to list tournaments' });
|
||||
console.error('[listOfficialTournaments] Error:', e);
|
||||
const errorMessage = e.message || 'Failed to list tournaments';
|
||||
res.status(e.statusCode || 500).json({ error: errorMessage });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Participant from '../models/Participant.js';
|
||||
|
||||
import DiaryDates from '../models/DiaryDates.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
import { emitParticipantAdded, emitParticipantRemoved, emitParticipantUpdated } from '../services/socketService.js';
|
||||
export const getParticipants = async (req, res) => {
|
||||
try {
|
||||
const { dateId } = req.params;
|
||||
@@ -24,7 +25,12 @@ export const updateParticipantGroup = async (req, res) => {
|
||||
where: {
|
||||
diaryDateId: dateId,
|
||||
memberId: memberId
|
||||
}
|
||||
},
|
||||
include: [{
|
||||
model: DiaryDates,
|
||||
as: 'diaryDate',
|
||||
attributes: ['clubId']
|
||||
}]
|
||||
});
|
||||
|
||||
if (!participant) {
|
||||
@@ -34,7 +40,25 @@ export const updateParticipantGroup = async (req, res) => {
|
||||
participant.groupId = groupId || null;
|
||||
await participant.save();
|
||||
|
||||
res.status(200).json(participant);
|
||||
// Lade den Participant erneut aus der DB, um sicherzustellen, dass wir den aktuellen Wert haben
|
||||
const updatedParticipant = await Participant.findOne({
|
||||
where: {
|
||||
diaryDateId: dateId,
|
||||
memberId: memberId
|
||||
},
|
||||
include: [{
|
||||
model: DiaryDates,
|
||||
as: 'diaryDate',
|
||||
attributes: ['clubId']
|
||||
}]
|
||||
});
|
||||
|
||||
// Emit Socket-Event mit dem aktualisierten Participant
|
||||
if (updatedParticipant?.diaryDate?.clubId) {
|
||||
emitParticipantUpdated(updatedParticipant.diaryDate.clubId, dateId, updatedParticipant);
|
||||
}
|
||||
|
||||
res.status(200).json(updatedParticipant || participant);
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Fehler beim Aktualisieren der Teilnehmer-Gruppenzuordnung' });
|
||||
@@ -45,6 +69,13 @@ export const addParticipant = async (req, res) => {
|
||||
try {
|
||||
const { diaryDateId, memberId } = req.body;
|
||||
const participant = await Participant.create({ diaryDateId, memberId });
|
||||
|
||||
// Hole DiaryDate für clubId
|
||||
const diaryDate = await DiaryDates.findByPk(diaryDateId);
|
||||
if (diaryDate?.clubId) {
|
||||
emitParticipantAdded(diaryDate.clubId, diaryDateId, participant);
|
||||
}
|
||||
|
||||
res.status(201).json(participant);
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
@@ -55,7 +86,18 @@ export const addParticipant = async (req, res) => {
|
||||
export const removeParticipant = async (req, res) => {
|
||||
try {
|
||||
const { diaryDateId, memberId } = req.body;
|
||||
|
||||
// Hole DiaryDate für clubId vor dem Löschen
|
||||
const diaryDate = await DiaryDates.findByPk(diaryDateId);
|
||||
const clubId = diaryDate?.clubId;
|
||||
|
||||
await Participant.destroy({ where: { diaryDateId, memberId } });
|
||||
|
||||
// Emit Socket-Event
|
||||
if (clubId) {
|
||||
emitParticipantRemoved(clubId, diaryDateId, memberId);
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'Teilnehmer entfernt' });
|
||||
} catch (error) {
|
||||
devLog(error);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// controllers/tournamentController.js
|
||||
import tournamentService from "../services/tournamentService.js";
|
||||
import { emitTournamentChanged } from '../services/socketService.js';
|
||||
import TournamentClass from '../models/TournamentClass.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
// 1. Alle Turniere eines Vereins
|
||||
export const getTournaments = async (req, res) => {
|
||||
@@ -10,6 +13,11 @@ export const getTournaments = async (req, res) => {
|
||||
res.status(200).json(tournaments);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
res.set('x-debug-tournament-clubid', String(clubId));
|
||||
res.set('x-debug-tournament-clubid-num', String(Number(clubId)));
|
||||
return res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
@@ -17,36 +25,66 @@ export const getTournaments = async (req, res) => {
|
||||
// 2. Neues Turnier anlegen
|
||||
export const addTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentName, date } = req.body;
|
||||
const { clubId, tournamentName, date, winningSets, allowsExternal } = req.body;
|
||||
try {
|
||||
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date);
|
||||
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal);
|
||||
// Emit Socket-Event
|
||||
if (clubId && tournament && tournament.id) {
|
||||
emitTournamentChanged(clubId, tournament.id);
|
||||
}
|
||||
res.status(201).json(tournament);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('[addTournament] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Teilnehmer hinzufügen
|
||||
// 3. Teilnehmer hinzufügen - klassengebunden
|
||||
export const addParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participant: participantId } = req.body;
|
||||
const { clubId, classId, participant: participantId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.addParticipant(token, clubId, tournamentId, participantId);
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
// Payloads:
|
||||
// - Mit Klasse (klassengebunden): { clubId, classId, participant }
|
||||
// - Ohne Klasse (turnierweit): { clubId, tournamentId, participant, classId: null }
|
||||
if (!participantId) {
|
||||
return res.status(400).json({ error: 'Teilnehmer-ID ist erforderlich' });
|
||||
}
|
||||
// Allow adding a participant either to a specific class (classId) or to the whole tournament (no class)
|
||||
if (!classId && !tournamentId) {
|
||||
return res.status(400).json({ error: 'Klasse oder tournamentId ist erforderlich' });
|
||||
}
|
||||
|
||||
// Pass through to service. If classId is present it will be used, otherwise the service should add the participant with classId = null for the given tournamentId
|
||||
await tournamentService.addParticipant(token, clubId, classId || null, participantId, tournamentId || null);
|
||||
|
||||
// Determine tournamentId for response and event emission
|
||||
let respTournamentId = tournamentId;
|
||||
if (classId && !respTournamentId) {
|
||||
const tournamentClass = await TournamentClass.findByPk(classId);
|
||||
if (!tournamentClass) {
|
||||
return res.status(404).json({ error: 'Klasse nicht gefunden' });
|
||||
}
|
||||
respTournamentId = tournamentClass.tournamentId;
|
||||
}
|
||||
|
||||
// Fetch updated participants for the (optional) class or whole tournament
|
||||
const participants = await tournamentService.getParticipants(token, clubId, respTournamentId, classId || null);
|
||||
// Emit Socket-Event
|
||||
if (respTournamentId) emitTournamentChanged(clubId, respTournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('[addParticipant] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Teilnehmerliste abrufen
|
||||
// 4. Teilnehmerliste abrufen - nach Klasse oder Turnier
|
||||
export const getParticipants = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
const { clubId, tournamentId, classId } = req.body;
|
||||
try {
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId, classId || null);
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -60,6 +98,8 @@ export const setModus = async (req, res) => {
|
||||
const { clubId, tournamentId, type, numberOfGroups, advancingPerGroup } = req.body;
|
||||
try {
|
||||
await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups, advancingPerGroup);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -70,9 +110,48 @@ export const setModus = async (req, res) => {
|
||||
// 6. Gruppen-Strukturen anlegen (leere Gruppen)
|
||||
export const createGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
const { clubId, tournamentId, numberOfGroups } = req.body;
|
||||
try {
|
||||
await tournamentService.createGroups(token, clubId, tournamentId);
|
||||
// DEBUG: Eingehende Daten sichtbar machen (temporär)
|
||||
console.log('[tournamentController.createGroups] body:', req.body);
|
||||
console.log('[tournamentController.createGroups] types:', {
|
||||
clubId: typeof clubId,
|
||||
tournamentId: typeof tournamentId,
|
||||
numberOfGroups: typeof numberOfGroups,
|
||||
});
|
||||
|
||||
// Turniere ohne Klassen: `numberOfGroups: 0` kommt aus der UI (Default) vor.
|
||||
// Statt „nichts passiert“ normalisieren wir auf mindestens 1 Gruppe.
|
||||
let normalizedNumberOfGroups = numberOfGroups;
|
||||
if (normalizedNumberOfGroups !== undefined && normalizedNumberOfGroups !== null) {
|
||||
const n = Number(normalizedNumberOfGroups);
|
||||
console.log('[tournamentController.createGroups] parsed numberOfGroups:', n);
|
||||
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
||||
return res.status(400).json({ error: 'numberOfGroups muss eine ganze Zahl >= 0 sein' });
|
||||
}
|
||||
normalizedNumberOfGroups = Math.max(1, n);
|
||||
}
|
||||
|
||||
console.log('[tournamentController.createGroups] normalizedNumberOfGroups:', normalizedNumberOfGroups);
|
||||
|
||||
await tournamentService.createGroups(token, clubId, tournamentId, normalizedNumberOfGroups);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 6b. Gruppen-Strukturen pro Klasse anlegen
|
||||
export const createGroupsPerClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, groupsPerClass } = req.body;
|
||||
try {
|
||||
await tournamentService.createGroupsPerClass(token, clubId, tournamentId, groupsPerClass);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -86,6 +165,8 @@ export const fillGroups = async (req, res) => {
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
const updatedMembers = await tournamentService.fillGroups(token, clubId, tournamentId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(updatedMembers);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -93,6 +174,21 @@ export const fillGroups = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 7b. Gruppenspiele erstellen ohne Gruppenzuordnungen zu ändern
|
||||
export const createGroupMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.body;
|
||||
try {
|
||||
await tournamentService.createGroupMatches(token, clubId, tournamentId, classId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 8. Gruppen mit ihren Teilnehmern abfragen
|
||||
export const getGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
@@ -119,6 +215,23 @@ export const getTournament = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Update Turnier
|
||||
export const updateTournament = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
const { name, date, winningSets } = req.body;
|
||||
try {
|
||||
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(tournament);
|
||||
} catch (error) {
|
||||
console.error('[updateTournament] Error:', error);
|
||||
const status = error.message.includes('existiert bereits') ? 400 : 500;
|
||||
res.status(status).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// 10. Alle Spiele eines Turniers abfragen
|
||||
export const getTournamentMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
@@ -138,6 +251,8 @@ export const addMatchResult = async (req, res) => {
|
||||
const { clubId, tournamentId, matchId, set, result } = req.body;
|
||||
try {
|
||||
await tournamentService.addMatchResult(token, clubId, tournamentId, matchId, set, result);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: "Result added successfully" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -151,6 +266,8 @@ export const finishMatch = async (req, res) => {
|
||||
const { clubId, tournamentId, matchId } = req.body;
|
||||
try {
|
||||
await tournamentService.finishMatch(token, clubId, tournamentId, matchId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: "Match finished successfully" });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -164,6 +281,8 @@ export const startKnockout = async (req, res) => {
|
||||
|
||||
try {
|
||||
await tournamentService.startKnockout(token, clubId, tournamentId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: "K.o.-Runde erfolgreich gestartet" });
|
||||
} catch (error) {
|
||||
const status = /Gruppenmodus|Zu wenige Qualifikanten/.test(error.message) ? 400 : 500;
|
||||
@@ -190,6 +309,8 @@ export const manualAssignGroups = async (req, res) => {
|
||||
numberOfGroups, // neu
|
||||
maxGroupSize // neu
|
||||
);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(groupsWithParts);
|
||||
} catch (error) {
|
||||
console.error('Error in manualAssignGroups:', error);
|
||||
@@ -197,11 +318,35 @@ export const manualAssignGroups = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const assignParticipantToGroup = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId, groupNumber, isExternal } = req.body;
|
||||
|
||||
try {
|
||||
const groups = await tournamentService.assignParticipantToGroup(
|
||||
token,
|
||||
clubId,
|
||||
tournamentId,
|
||||
participantId,
|
||||
groupNumber,
|
||||
isExternal || false
|
||||
);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(groups);
|
||||
} catch (error) {
|
||||
console.error('Error in assignParticipantToGroup:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const resetGroups = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetGroups(token, clubId, tournamentId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -211,9 +356,11 @@ export const resetGroups = async (req, res) => {
|
||||
|
||||
export const resetMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
const { clubId, tournamentId, classId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetMatches(token, clubId, tournamentId);
|
||||
await tournamentService.resetMatches(token, clubId, tournamentId, classId || null);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -227,6 +374,8 @@ export const removeParticipant = async (req, res) => {
|
||||
try {
|
||||
await tournamentService.removeParticipant(token, clubId, tournamentId, participantId);
|
||||
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(participants);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -234,6 +383,21 @@ export const removeParticipant = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const updateParticipantSeeded = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.params;
|
||||
const { seeded } = req.body;
|
||||
try {
|
||||
await tournamentService.updateParticipantSeeded(token, clubId, tournamentId, participantId, seeded);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Gesetzt-Status aktualisiert' });
|
||||
} catch (err) {
|
||||
console.error('[updateParticipantSeeded] Error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteMatchResult = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId, set } = req.body;
|
||||
@@ -245,6 +409,8 @@ export const deleteMatchResult = async (req, res) => {
|
||||
matchId,
|
||||
set
|
||||
);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Einzelsatz gelöscht' });
|
||||
} catch (error) {
|
||||
console.error('Error in deleteMatchResult:', error);
|
||||
@@ -258,6 +424,8 @@ export const reopenMatch = async (req, res) => {
|
||||
const { clubId, tournamentId, matchId } = req.body;
|
||||
try {
|
||||
await tournamentService.reopenMatch(token, clubId, tournamentId, matchId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
// Gib optional das aktualisierte Match zurück
|
||||
res.status(200).json({ message: "Match reopened" });
|
||||
} catch (error) {
|
||||
@@ -268,13 +436,210 @@ export const reopenMatch = async (req, res) => {
|
||||
|
||||
export const deleteKnockoutMatches = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.body;
|
||||
const { clubId, tournamentId, classId } = req.body;
|
||||
try {
|
||||
await tournamentService.resetKnockout(token, clubId, tournamentId);
|
||||
await tournamentService.resetKnockout(token, clubId, tournamentId, classId);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: "K.o.-Runde gelöscht" });
|
||||
} catch (error) {
|
||||
console.error("Error in deleteKnockoutMatches:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const setMatchActive = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, matchId } = req.params;
|
||||
const { isActive } = req.body;
|
||||
try {
|
||||
await tournamentService.setMatchActive(token, clubId, tournamentId, matchId, isActive);
|
||||
// Emit Socket-Event
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Match-Status aktualisiert' });
|
||||
} catch (err) {
|
||||
console.error('[setMatchActive] Error:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Externe Teilnehmer hinzufügen
|
||||
export const addExternalParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId, firstName, lastName, club, birthDate, gender } = req.body;
|
||||
try {
|
||||
await tournamentService.addExternalParticipant(token, clubId, classId, firstName, lastName, club, birthDate, gender);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Externer Teilnehmer hinzugefügt' });
|
||||
} catch (error) {
|
||||
console.error('[addExternalParticipant] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Externe Teilnehmer abrufen - nach Klasse oder Turnier
|
||||
export const getExternalParticipants = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.body;
|
||||
try {
|
||||
const participants = await tournamentService.getExternalParticipants(token, clubId, tournamentId, classId || null);
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
console.error('[getExternalParticipants] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Externe Teilnehmer löschen
|
||||
export const removeExternalParticipant = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.body;
|
||||
try {
|
||||
await tournamentService.removeExternalParticipant(token, clubId, tournamentId, participantId);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Externer Teilnehmer entfernt' });
|
||||
} catch (error) {
|
||||
console.error('[removeExternalParticipant] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Gesetzt-Status für externe Teilnehmer aktualisieren
|
||||
export const updateExternalParticipantSeeded = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.params;
|
||||
const { seeded } = req.body;
|
||||
try {
|
||||
await tournamentService.updateExternalParticipantSeeded(token, clubId, tournamentId, participantId, seeded);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Gesetzt-Status aktualisiert' });
|
||||
} catch (error) {
|
||||
console.error('[updateExternalParticipantSeeded] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Tournament Classes
|
||||
export const getTournamentClasses = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
try {
|
||||
const classes = await tournamentService.getTournamentClasses(token, clubId, tournamentId);
|
||||
res.status(200).json(classes);
|
||||
} catch (error) {
|
||||
console.error('[getTournamentClasses] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const addTournamentClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.params;
|
||||
const { name, isDoubles, gender, minBirthYear } = req.body;
|
||||
try {
|
||||
const tournamentClass = await tournamentService.addTournamentClass(token, clubId, tournamentId, name, isDoubles, gender, minBirthYear);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(tournamentClass);
|
||||
} catch (error) {
|
||||
console.error('[addTournamentClass] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTournamentClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.params;
|
||||
const { name, sortOrder, isDoubles, gender, minBirthYear } = req.body;
|
||||
try {
|
||||
console.log('[updateTournamentClass] Request body:', { name, sortOrder, isDoubles, gender, minBirthYear });
|
||||
const tournamentClass = await tournamentService.updateTournamentClass(token, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear);
|
||||
console.log('[updateTournamentClass] Updated class:', JSON.stringify(tournamentClass.toJSON(), null, 2));
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(tournamentClass);
|
||||
} catch (error) {
|
||||
console.error('[updateTournamentClass] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTournamentClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.params;
|
||||
try {
|
||||
await tournamentService.deleteTournamentClass(token, clubId, tournamentId, classId);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Klasse gelöscht' });
|
||||
} catch (error) {
|
||||
console.error('[deleteTournamentClass] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateParticipantClass = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, participantId } = req.params;
|
||||
const { classId, isExternal } = req.body;
|
||||
try {
|
||||
await tournamentService.updateParticipantClass(token, clubId, tournamentId, participantId, classId, isExternal);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Klasse aktualisiert' });
|
||||
} catch (error) {
|
||||
console.error('[updateParticipantClass] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
// Tournament Pairings
|
||||
export const getPairings = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.params;
|
||||
try {
|
||||
const pairings = await tournamentService.getPairings(token, clubId, tournamentId, classId);
|
||||
res.status(200).json(pairings);
|
||||
} catch (error) {
|
||||
console.error('[getPairings] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const createPairing = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, classId } = req.params;
|
||||
const { player1Type, player1Id, player2Type, player2Id, seeded, groupId } = req.body;
|
||||
try {
|
||||
const pairing = await tournamentService.createPairing(token, clubId, tournamentId, classId, player1Type, player1Id, player2Type, player2Id, seeded, groupId);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(pairing);
|
||||
} catch (error) {
|
||||
console.error('[createPairing] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const updatePairing = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, pairingId } = req.params;
|
||||
const { player1Type, player1Id, player2Type, player2Id, seeded, groupId } = req.body;
|
||||
try {
|
||||
const pairing = await tournamentService.updatePairing(token, clubId, tournamentId, pairingId, player1Type, player1Id, player2Type, player2Id, seeded, groupId);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json(pairing);
|
||||
} catch (error) {
|
||||
console.error('[updatePairing] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const deletePairing = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, pairingId } = req.params;
|
||||
try {
|
||||
await tournamentService.deletePairing(token, clubId, tournamentId, pairingId);
|
||||
emitTournamentChanged(clubId, tournamentId);
|
||||
res.status(200).json({ message: 'Paarung gelöscht' });
|
||||
} catch (error) {
|
||||
console.error('[deletePairing] Error:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
70
backend/controllers/tournamentStagesController.js
Normal file
70
backend/controllers/tournamentStagesController.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import tournamentService from '../services/tournamentService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
export const getStages = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId } = req.query;
|
||||
try {
|
||||
if (clubId == null || tournamentId == null) {
|
||||
return res.status(400).json({ error: 'clubId und tournamentId sind erforderlich.' });
|
||||
}
|
||||
const data = await tournamentService.getTournamentStages(token, Number(clubId), Number(tournamentId));
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
// Debug-Hilfe: zeigt, welche IDs tatsächlich am Endpoint ankamen (ohne sensible Daten)
|
||||
res.set('x-debug-stages-clubid', String(clubId));
|
||||
res.set('x-debug-stages-tournamentid', String(tournamentId));
|
||||
res.set('x-debug-stages-clubid-num', String(Number(clubId)));
|
||||
return res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const upsertStages = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, stages, advancement, advancements } = req.body;
|
||||
try {
|
||||
const data = await tournamentService.upsertTournamentStages(
|
||||
token,
|
||||
Number(clubId),
|
||||
Number(tournamentId),
|
||||
stages,
|
||||
advancement,
|
||||
advancements
|
||||
);
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
res.set('x-debug-stages-clubid', String(clubId));
|
||||
res.set('x-debug-stages-tournamentid', String(tournamentId));
|
||||
res.set('x-debug-stages-clubid-num', String(Number(clubId)));
|
||||
return res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
export const advanceStage = async (req, res) => {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubId, tournamentId, fromStageIndex, toStageIndex } = req.body;
|
||||
try {
|
||||
const data = await tournamentService.advanceTournamentStage(
|
||||
token,
|
||||
Number(clubId),
|
||||
Number(tournamentId),
|
||||
Number(fromStageIndex || 1),
|
||||
(toStageIndex == null ? null : Number(toStageIndex))
|
||||
);
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof HttpError) {
|
||||
return res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
128
backend/controllers/trainingGroupController.js
Normal file
128
backend/controllers/trainingGroupController.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import trainingGroupService from '../services/trainingGroupService.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorUtils.js';
|
||||
|
||||
export const getTrainingGroups = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const groups = await trainingGroupService.getTrainingGroups(userToken, clubId);
|
||||
res.status(200).json(groups);
|
||||
} catch (error) {
|
||||
console.error('[getTrainingGroups] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingsgruppen');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const createTrainingGroup = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const { name, sortOrder } = req.body;
|
||||
const group = await trainingGroupService.createTrainingGroup(userToken, clubId, name, sortOrder);
|
||||
res.status(201).json(group);
|
||||
} catch (error) {
|
||||
console.error('[createTrainingGroup] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingsgruppe');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTrainingGroup = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, groupId } = req.params;
|
||||
const { name, sortOrder } = req.body;
|
||||
const group = await trainingGroupService.updateTrainingGroup(userToken, clubId, groupId, name, sortOrder);
|
||||
res.status(200).json(group);
|
||||
} catch (error) {
|
||||
console.error('[updateTrainingGroup] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Trainingsgruppe');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTrainingGroup = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, groupId } = req.params;
|
||||
await trainingGroupService.deleteTrainingGroup(userToken, clubId, groupId);
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[deleteTrainingGroup] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingsgruppe');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const addMemberToGroup = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, groupId, memberId } = req.params;
|
||||
const memberGroup = await trainingGroupService.addMemberToGroup(userToken, clubId, groupId, memberId);
|
||||
res.status(201).json(memberGroup);
|
||||
} catch (error) {
|
||||
console.error('[addMemberToGroup] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen des Mitglieds zur Gruppe');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const removeMemberFromGroup = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, groupId, memberId } = req.params;
|
||||
await trainingGroupService.removeMemberFromGroup(userToken, clubId, groupId, memberId);
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('[removeMemberFromGroup] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen des Mitglieds aus der Gruppe');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const getMemberGroups = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, memberId } = req.params;
|
||||
const groups = await trainingGroupService.getMemberGroups(userToken, clubId, memberId);
|
||||
res.status(200).json(groups);
|
||||
} catch (error) {
|
||||
console.error('[getMemberGroups] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Gruppen des Mitglieds');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const ensurePresetGroups = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const groups = await trainingGroupService.ensurePresetGroups(userToken, clubId);
|
||||
res.status(200).json({
|
||||
message: 'Preset-Gruppen wurden erstellt/überprüft',
|
||||
groups: groups.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[ensurePresetGroups] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Preset-Gruppen');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const enablePresetGroup = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, presetType } = req.params;
|
||||
const group = await trainingGroupService.enablePresetGroup(userToken, clubId, presetType);
|
||||
res.status(200).json({
|
||||
message: 'Preset-Gruppe wurde aktiviert',
|
||||
group
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[enablePresetGroup] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Aktivieren der Preset-Gruppe');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
80
backend/controllers/trainingTimeController.js
Normal file
80
backend/controllers/trainingTimeController.js
Normal file
@@ -0,0 +1,80 @@
|
||||
import trainingTimeService from '../services/trainingTimeService.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorUtils.js';
|
||||
|
||||
export const getTrainingTimes = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const groups = await trainingTimeService.getTrainingTimes(userToken, clubId);
|
||||
res.status(200).json(groups);
|
||||
} catch (error) {
|
||||
console.error('[getTrainingTimes] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingszeiten');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const createTrainingTime = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const { trainingGroupId, weekday, startTime, endTime } = req.body;
|
||||
|
||||
if (!trainingGroupId || weekday === undefined || !startTime || !endTime) {
|
||||
return res.status(400).json({ error: 'Alle Felder müssen ausgefüllt sein' });
|
||||
}
|
||||
|
||||
const trainingTime = await trainingTimeService.createTrainingTime(
|
||||
userToken,
|
||||
clubId,
|
||||
trainingGroupId,
|
||||
weekday,
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
|
||||
res.status(201).json(trainingTime);
|
||||
} catch (error) {
|
||||
console.error('[createTrainingTime] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingszeit');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const updateTrainingTime = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, timeId } = req.params;
|
||||
const { weekday, startTime, endTime } = req.body;
|
||||
|
||||
const trainingTime = await trainingTimeService.updateTrainingTime(
|
||||
userToken,
|
||||
clubId,
|
||||
timeId,
|
||||
weekday,
|
||||
startTime,
|
||||
endTime
|
||||
);
|
||||
|
||||
res.status(200).json(trainingTime);
|
||||
} catch (error) {
|
||||
console.error('[updateTrainingTime] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Trainingszeit');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteTrainingTime = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, timeId } = req.params;
|
||||
|
||||
const result = await trainingTimeService.deleteTrainingTime(userToken, clubId, timeId);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
console.error('[deleteTrainingTime] - Error:', error);
|
||||
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingszeit');
|
||||
res.status(error.statusCode || 500).json({ error: msg });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,54 @@
|
||||
/**
|
||||
* HttpError mit Unterstützung für Fehlercodes
|
||||
*
|
||||
* Verwendung:
|
||||
* - new HttpError('Fehlermeldung', 400) - Legacy, wird weiterhin unterstützt
|
||||
* - new HttpError({ code: 'ERROR_USER_NOT_FOUND' }, 404) - Mit Fehlercode
|
||||
* - new HttpError({ code: 'ERROR_MEMBER_NOT_FOUND', params: { memberId: 123 } }, 404) - Mit Parametern
|
||||
*/
|
||||
class HttpError extends Error {
|
||||
constructor(message, statusCode) {
|
||||
super(message);
|
||||
constructor(messageOrError, statusCode) {
|
||||
// Unterstützung für beide Formate:
|
||||
// 1. Legacy: new HttpError('Fehlermeldung', 400)
|
||||
// 2. Neu: new HttpError({ code: 'ERROR_CODE', params: {...} }, 400)
|
||||
if (typeof messageOrError === 'string') {
|
||||
// Legacy-Format
|
||||
super(messageOrError);
|
||||
this.errorCode = null;
|
||||
this.errorParams = null;
|
||||
} else if (messageOrError && typeof messageOrError === 'object' && messageOrError.code) {
|
||||
// Neues Format mit Fehlercode
|
||||
super(messageOrError.code); // Für Stack-Trace
|
||||
this.errorCode = messageOrError.code;
|
||||
this.errorParams = messageOrError.params || null;
|
||||
} else {
|
||||
// Fallback
|
||||
super('Unknown error');
|
||||
this.errorCode = null;
|
||||
this.errorParams = null;
|
||||
}
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.statusCode = statusCode;
|
||||
this.statusCode = statusCode || 500;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt das Fehler-Objekt für die API-Antwort zurück
|
||||
* @returns {object} Fehler-Objekt mit code und optional params
|
||||
*/
|
||||
toJSON() {
|
||||
if (this.errorCode) {
|
||||
return {
|
||||
code: this.errorCode,
|
||||
...(this.errorParams && { params: this.errorParams })
|
||||
};
|
||||
}
|
||||
// Legacy: Gib die Nachricht zurück
|
||||
return {
|
||||
message: this.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default HttpError;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
58
backend/migrations/20251213_add_tournament_stages.sql
Normal file
58
backend/migrations/20251213_add_tournament_stages.sql
Normal file
@@ -0,0 +1,58 @@
|
||||
-- Adds multi-stage tournaments (rounds) support
|
||||
-- MariaDB/MySQL compatible migration (manual execution)
|
||||
|
||||
-- 1) New table: tournament_stage
|
||||
CREATE TABLE IF NOT EXISTS tournament_stage (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tournament_id INT NOT NULL,
|
||||
stage_index INT NOT NULL,
|
||||
name VARCHAR(255) NULL,
|
||||
type VARCHAR(32) NOT NULL, -- 'groups' | 'knockout'
|
||||
number_of_groups INT NULL,
|
||||
advancing_per_group INT NULL,
|
||||
max_group_size INT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT fk_tournament_stage_tournament
|
||||
FOREIGN KEY (tournament_id) REFERENCES tournament(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE INDEX idx_tournament_stage_tournament_id ON tournament_stage (tournament_id);
|
||||
CREATE UNIQUE INDEX uq_tournament_stage_tournament_id_index ON tournament_stage (tournament_id, stage_index);
|
||||
|
||||
-- 2) New table: tournament_stage_advancement
|
||||
CREATE TABLE IF NOT EXISTS tournament_stage_advancement (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
tournament_id INT NOT NULL,
|
||||
from_stage_id INT NOT NULL,
|
||||
to_stage_id INT NOT NULL,
|
||||
mode VARCHAR(32) NOT NULL DEFAULT 'pools',
|
||||
config JSON NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT fk_tournament_stage_adv_tournament
|
||||
FOREIGN KEY (tournament_id) REFERENCES tournament(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_tournament_stage_adv_from
|
||||
FOREIGN KEY (from_stage_id) REFERENCES tournament_stage(id)
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT fk_tournament_stage_adv_to
|
||||
FOREIGN KEY (to_stage_id) REFERENCES tournament_stage(id)
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE INDEX idx_tournament_stage_adv_tournament_id ON tournament_stage_advancement (tournament_id);
|
||||
CREATE INDEX idx_tournament_stage_adv_from_stage_id ON tournament_stage_advancement (from_stage_id);
|
||||
CREATE INDEX idx_tournament_stage_adv_to_stage_id ON tournament_stage_advancement (to_stage_id);
|
||||
|
||||
-- 3) Add stage_id to tournament_group and tournament_match
|
||||
-- MariaDB has no IF NOT EXISTS for columns; run each ALTER once.
|
||||
-- If you rerun, comment out the ALTERs or check INFORMATION_SCHEMA first.
|
||||
ALTER TABLE tournament_group ADD COLUMN stage_id INT NULL;
|
||||
ALTER TABLE tournament_match ADD COLUMN stage_id INT NULL;
|
||||
|
||||
CREATE INDEX idx_tournament_group_tournament_stage ON tournament_group (tournament_id, stage_id);
|
||||
CREATE INDEX idx_tournament_match_tournament_stage ON tournament_match (tournament_id, stage_id);
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Allow NULL placeholders for KO (e.g. "Spiel um Platz 3")
|
||||
-- MariaDB/MySQL manual migration
|
||||
--
|
||||
-- Background: We create placeholder matches with player1_id/player2_id = NULL.
|
||||
-- Some prod DBs still have NOT NULL on these columns.
|
||||
|
||||
-- 1) Make player columns nullable
|
||||
ALTER TABLE tournament_match MODIFY COLUMN player1_id INT NULL;
|
||||
ALTER TABLE tournament_match MODIFY COLUMN player2_id INT NULL;
|
||||
|
||||
-- 2) (Optional) If you have foreign keys to tournament_member/external participant IDs,
|
||||
-- ensure they also allow NULL. (Not adding here because not all installations have FKs.)
|
||||
|
||||
-- 3) Verify
|
||||
-- SHOW COLUMNS FROM tournament_match LIKE 'player1_id';
|
||||
-- SHOW COLUMNS FROM tournament_match LIKE 'player2_id';
|
||||
77
backend/migrations/TABELLEN_LISTE.md
Normal file
77
backend/migrations/TABELLEN_LISTE.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Liste aller Tabellen im Trainingstagebuch-Projekt
|
||||
|
||||
## Basis-Tabellen
|
||||
1. `user` - Benutzer
|
||||
2. `user_club` - Verknüpfung Benutzer ↔ Verein
|
||||
3. `user_token` - Authentifizierungs-Tokens
|
||||
4. `clubs` - Vereine
|
||||
5. `log` - System-Logs
|
||||
|
||||
## Mitglieder-Verwaltung
|
||||
6. `member` - Mitglieder
|
||||
7. `member_contact` - Kontaktdaten der Mitglieder (Telefon, E-Mail)
|
||||
8. `member_image` - Bilder der Mitglieder
|
||||
9. `member_notes` - Notizen zu Mitgliedern
|
||||
10. `member_transfer_config` - Konfiguration für Mitgliederübertragung
|
||||
|
||||
## Trainingsgruppen (NEU)
|
||||
11. `training_group` - Trainingsgruppen
|
||||
12. `member_training_group` - Verknüpfung Mitglied ↔ Trainingsgruppe
|
||||
13. `club_disabled_preset_groups` - Deaktivierte Preset-Gruppen pro Verein
|
||||
14. `training_times` - Trainingszeiten pro Gruppe (NEU)
|
||||
|
||||
## Tagebuch
|
||||
15. `diary_dates` - Trainingstage
|
||||
16. `participants` - Teilnehmer an Trainingstagen
|
||||
17. `activities` - Aktivitäten
|
||||
18. `diary_notes` - Notizen zu Trainingstagen
|
||||
19. `diary_tags` - Tags für Tagebuch
|
||||
20. `member_diary_tags` - Verknüpfung Mitglied ↔ Tagebuch-Tag
|
||||
21. `diary_date_tags` - Verknüpfung Trainingstag ↔ Tag
|
||||
22. `diary_member_notes` - Notizen zu Mitgliedern an Trainingstagen
|
||||
23. `diary_member_tags` - Tags für Mitglieder an Trainingstagen
|
||||
24. `diary_date_activities` - Aktivitäten an Trainingstagen
|
||||
25. `diary_member_activities` - Verknüpfung Teilnehmer ↔ Aktivität
|
||||
26. `group` - Gruppen (für Trainingsplan)
|
||||
27. `group_activity` - Gruppenaktivitäten
|
||||
|
||||
## Vordefinierte Aktivitäten
|
||||
28. `predefined_activities` - Vordefinierte Aktivitäten
|
||||
29. `predefined_activity_images` - Bilder zu vordefinierten Aktivitäten
|
||||
|
||||
## Unfälle
|
||||
30. `accident` - Unfälle
|
||||
|
||||
## Teams & Ligen
|
||||
31. `season` - Saisons
|
||||
32. `league` - Ligen
|
||||
33. `team` - Teams
|
||||
34. `club_team` - Verknüpfung Verein ↔ Team
|
||||
35. `team_document` - Dokumente zu Teams
|
||||
36. `match` - Spiele
|
||||
37. `location` - Spielorte
|
||||
|
||||
## Turniere
|
||||
38. `tournament` - Turniere
|
||||
39. `tournament_class` - Turnierklassen
|
||||
40. `tournament_group` - Turniergruppen
|
||||
41. `tournament_member` - Teilnehmer an Turnieren
|
||||
42. `tournament_match` - Spiele in Turnieren
|
||||
43. `tournament_result` - Ergebnisse von Turnierspielen
|
||||
44. `external_tournament_participant` - Externe Teilnehmer an Turnieren
|
||||
|
||||
## Offizielle Turniere (myTischtennis)
|
||||
45. `official_tournaments` - Offizielle Turniere
|
||||
46. `official_competitions` - Wettbewerbe in offiziellen Turnieren
|
||||
47. `official_competition_members` - Teilnehmer an offiziellen Wettbewerben
|
||||
|
||||
## myTischtennis Integration
|
||||
48. `my_tischtennis` - myTischtennis-Verbindungen
|
||||
49. `my_tischtennis_update_history` - Update-Historie
|
||||
50. `my_tischtennis_fetch_log` - Fetch-Logs
|
||||
|
||||
## API & Logging
|
||||
51. `api_log` - API-Logs
|
||||
|
||||
## Gesamt: 51 Tabellen
|
||||
|
||||
22
backend/migrations/add_allows_external_to_tournament.sql
Normal file
22
backend/migrations/add_allows_external_to_tournament.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Migration: Add 'allows_external' column to tournament table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament';
|
||||
SET @columnname = 'allows_external';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `winning_sets`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Migration: Add 'class_id' column to external_tournament_participant table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'external_tournament_participant';
|
||||
SET @columnname = 'class_id';
|
||||
|
||||
-- Check if column exists
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
);
|
||||
|
||||
-- Add column if it doesn't exist
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `external_tournament_participant` ADD COLUMN `class_id` INT(11) NULL AFTER `seeded`',
|
||||
'SELECT 1 AS column_already_exists'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
27
backend/migrations/add_class_id_to_tournament_group.sql
Normal file
27
backend/migrations/add_class_id_to_tournament_group.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Migration: Add 'class_id' column to tournament_group table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_group';
|
||||
SET @columnname = 'class_id';
|
||||
|
||||
-- Check if column exists
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
);
|
||||
|
||||
-- Add column if it doesn't exist
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `tournament_group` ADD COLUMN `class_id` INT(11) NULL AFTER `tournament_id`',
|
||||
'SELECT 1 AS column_already_exists'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
27
backend/migrations/add_class_id_to_tournament_match.sql
Normal file
27
backend/migrations/add_class_id_to_tournament_match.sql
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Migration: Add 'class_id' column to tournament_match table
|
||||
-- Date: 2025-01-16
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_match';
|
||||
SET @columnname = 'class_id';
|
||||
|
||||
-- Check if column exists
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
);
|
||||
|
||||
-- Add column if it doesn't exist
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `tournament_match` ADD COLUMN `class_id` INT(11) NULL AFTER `group_id`',
|
||||
'SELECT 1 AS column_already_exists'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
22
backend/migrations/add_class_id_to_tournament_member.sql
Normal file
22
backend/migrations/add_class_id_to_tournament_member.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Migration: Add 'class_id' column to tournament_member table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_member';
|
||||
SET @columnname = 'class_id';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` INT(11) NULL AFTER `seeded`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Migration: Geschlecht zu externen Turnierteilnehmern hinzufügen
|
||||
-- Datum: 2025-01-XX
|
||||
|
||||
ALTER TABLE `external_tournament_participant`
|
||||
ADD COLUMN `gender` ENUM('male', 'female', 'diverse', 'unknown') NULL DEFAULT 'unknown' AFTER `birth_date`;
|
||||
|
||||
|
||||
|
||||
8
backend/migrations/add_gender_to_tournament_class.sql
Normal file
8
backend/migrations/add_gender_to_tournament_class.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Migration: Geschlecht zu Turnierklassen hinzufügen
|
||||
-- Datum: 2025-01-XX
|
||||
|
||||
ALTER TABLE `tournament_class`
|
||||
ADD COLUMN `gender` ENUM('male', 'female', 'mixed') NULL DEFAULT NULL AFTER `is_doubles`;
|
||||
|
||||
|
||||
|
||||
22
backend/migrations/add_is_active_to_tournament_match.sql
Normal file
22
backend/migrations/add_is_active_to_tournament_match.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Migration: Add 'is_active' column to tournament_match table
|
||||
-- Date: 2025-01-14
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_match';
|
||||
SET @columnname = 'is_active';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `is_finished`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Migration: Add is_doubles column to tournament_class table
|
||||
-- Date: 2025-01-23
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
ALTER TABLE `tournament_class`
|
||||
ADD COLUMN `is_doubles` TINYINT(1) NOT NULL DEFAULT 0 AFTER `sort_order`;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Migration: Geburtsjahr-Beschränkung zu Turnierklassen hinzufügen
|
||||
-- Datum: 2025-01-XX
|
||||
-- Beschreibung: Fügt max_birth_year Feld hinzu für "geboren im Jahr X oder früher" (<=)
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_class';
|
||||
SET @columnname = 'max_birth_year';
|
||||
|
||||
-- Check if column exists
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
);
|
||||
|
||||
-- Add column if it doesn't exist
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `tournament_class` ADD COLUMN `max_birth_year` INT(11) NULL DEFAULT NULL AFTER `gender`',
|
||||
'SELECT 1 AS column_already_exists'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
29
backend/migrations/add_name_to_tournament.sql
Normal file
29
backend/migrations/add_name_to_tournament.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
-- Migration: Add name column to tournament table
|
||||
-- Date: 2025-01-13
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
-- Add name column if it doesn't exist
|
||||
-- Check if column exists and add it if not
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament';
|
||||
SET @columnname = 'name';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` VARCHAR(255) NOT NULL DEFAULT "" AFTER `id`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
-- Update existing tournaments: set name to formatted date if name is empty
|
||||
UPDATE `tournament`
|
||||
SET `name` = DATE_FORMAT(`date`, '%d.%m.%Y')
|
||||
WHERE `name` = '' OR `name` IS NULL;
|
||||
|
||||
24
backend/migrations/add_seeded_to_tournament_member.sql
Normal file
24
backend/migrations/add_seeded_to_tournament_member.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
-- Migration: Add seeded column to tournament_member table
|
||||
-- Date: 2025-01-13
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
-- Add seeded column if it doesn't exist
|
||||
-- Check if column exists and add it if not
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_member';
|
||||
SET @columnname = 'seeded';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `club_member_id`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
22
backend/migrations/add_winning_sets_to_tournament.sql
Normal file
22
backend/migrations/add_winning_sets_to_tournament.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Migration: Add 'winning_sets' column to tournament table
|
||||
-- Date: 2025-01-14
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament';
|
||||
SET @columnname = 'winning_sets';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @columnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` INT NOT NULL DEFAULT 3 AFTER `advancing_per_group`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- Migration: Change 'ttr' column to 'birth_date' in external_tournament_participant table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'external_tournament_participant';
|
||||
SET @oldcolumnname = 'ttr';
|
||||
SET @newcolumnname = 'birth_date';
|
||||
|
||||
-- Check if old column exists
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @oldcolumnname)
|
||||
) > 0,
|
||||
CONCAT('ALTER TABLE `', @tablename, '` CHANGE COLUMN `', @oldcolumnname, '` `', @newcolumnname, '` VARCHAR(255) NULL AFTER `club`'),
|
||||
'SELECT 1'
|
||||
));
|
||||
PREPARE alterIfExists FROM @preparedStatement;
|
||||
EXECUTE alterIfExists;
|
||||
DEALLOCATE PREPARE alterIfExists;
|
||||
|
||||
-- If old column didn't exist, check if new column exists and add it if not
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @newcolumnname)
|
||||
) > 0,
|
||||
'SELECT 1',
|
||||
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @newcolumnname, '` VARCHAR(255) NULL AFTER `club`')
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
62
backend/migrations/check_seasons_and_teams.sql
Normal file
62
backend/migrations/check_seasons_and_teams.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
-- Diagnose-Skript: Prüfe Seasons und Teams auf dem Server
|
||||
-- Führe diese Queries auf dem Server aus, um das Problem zu identifizieren
|
||||
|
||||
-- 1. Prüfe, ob die season-Tabelle existiert und Daten enthält
|
||||
SELECT '=== SEASONS ===' as info;
|
||||
SELECT * FROM `season` ORDER BY `id` DESC;
|
||||
|
||||
-- 2. Prüfe, ob die club_team-Tabelle existiert und welche season_id verwendet wird
|
||||
SELECT '=== CLUB_TEAMS ===' as info;
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
club_id,
|
||||
season_id,
|
||||
league_id,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM `club_team`
|
||||
ORDER BY `id`;
|
||||
|
||||
-- 3. Prüfe, ob es Teams gibt, die auf nicht-existierende Seasons verweisen
|
||||
SELECT '=== TEAMS MIT FEHLENDEN SEASONS ===' as info;
|
||||
SELECT
|
||||
ct.id,
|
||||
ct.name,
|
||||
ct.season_id,
|
||||
s.season
|
||||
FROM `club_team` ct
|
||||
LEFT JOIN `season` s ON ct.season_id = s.id
|
||||
WHERE s.id IS NULL;
|
||||
|
||||
-- 4. Prüfe, ob es Teams gibt, die keine season_id haben
|
||||
SELECT '=== TEAMS OHNE SEASON_ID ===' as info;
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
club_id,
|
||||
season_id
|
||||
FROM `club_team`
|
||||
WHERE season_id IS NULL;
|
||||
|
||||
-- 5. Prüfe die Struktur der club_team-Tabelle
|
||||
SELECT '=== CLUB_TEAM TABELLENSTRUKTUR ===' as info;
|
||||
DESCRIBE `club_team`;
|
||||
|
||||
-- 6. Prüfe die Struktur der season-Tabelle
|
||||
SELECT '=== SEASON TABELLENSTRUKTUR ===' as info;
|
||||
DESCRIBE `season`;
|
||||
|
||||
-- 7. Prüfe Foreign Key Constraints
|
||||
SELECT '=== FOREIGN KEYS ===' as info;
|
||||
SELECT
|
||||
CONSTRAINT_NAME,
|
||||
TABLE_NAME,
|
||||
COLUMN_NAME,
|
||||
REFERENCED_TABLE_NAME,
|
||||
REFERENCED_COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND (TABLE_NAME = 'club_team' OR TABLE_NAME = 'season')
|
||||
AND REFERENCED_TABLE_NAME IS NOT NULL;
|
||||
|
||||
30
backend/migrations/check_seasons_and_teams_simple.sql
Normal file
30
backend/migrations/check_seasons_and_teams_simple.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- Vereinfachtes Diagnose-Skript: Prüfe nur die wichtigsten Punkte
|
||||
|
||||
-- 1. Gibt es Seasons in der Datenbank?
|
||||
SELECT 'SEASONS:' as check_type, COUNT(*) as count FROM `season`;
|
||||
SELECT * FROM `season` ORDER BY `id` DESC;
|
||||
|
||||
-- 2. Gibt es Teams in der Datenbank?
|
||||
SELECT 'CLUB_TEAMS:' as check_type, COUNT(*) as count FROM `club_team`;
|
||||
SELECT id, name, club_id, season_id, league_id FROM `club_team` ORDER BY `id`;
|
||||
|
||||
-- 3. Haben alle Teams eine season_id?
|
||||
SELECT 'TEAMS OHNE SEASON_ID:' as check_type, COUNT(*) as count
|
||||
FROM `club_team` WHERE season_id IS NULL;
|
||||
|
||||
-- 4. Verweisen alle Teams auf existierende Seasons?
|
||||
SELECT 'TEAMS MIT FEHLENDEN SEASONS:' as check_type, COUNT(*) as count
|
||||
FROM `club_team` ct
|
||||
LEFT JOIN `season` s ON ct.season_id = s.id
|
||||
WHERE s.id IS NULL;
|
||||
|
||||
-- 5. Welche season_id verwenden die Teams?
|
||||
SELECT 'SEASON_ID VERWENDUNG:' as check_type, season_id, COUNT(*) as team_count
|
||||
FROM `club_team`
|
||||
GROUP BY season_id;
|
||||
|
||||
-- 6. Welche Seasons existieren?
|
||||
SELECT 'EXISTIERENDE SEASONS:' as check_type, id, season
|
||||
FROM `season`
|
||||
ORDER BY id;
|
||||
|
||||
17
backend/migrations/create_club_disabled_preset_groups.sql
Normal file
17
backend/migrations/create_club_disabled_preset_groups.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Migration: Create club_disabled_preset_groups table
|
||||
-- Date: 2025-01-16
|
||||
-- For MariaDB/MySQL
|
||||
-- Stores which preset groups are disabled for each club
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `club_disabled_preset_groups` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`club_id` INT(11) NOT NULL,
|
||||
`preset_type` ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe') NOT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_club_preset_type` (`club_id`, `preset_type`),
|
||||
KEY `club_id` (`club_id`),
|
||||
CONSTRAINT `club_disabled_preset_groups_ibfk_1` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Migration: Create external_tournament_participant table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `external_tournament_participant` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`tournament_id` INT(11) NOT NULL,
|
||||
`group_id` INT(11) NULL,
|
||||
`first_name` VARCHAR(255) NOT NULL,
|
||||
`last_name` VARCHAR(255) NOT NULL,
|
||||
`club` VARCHAR(255) NULL,
|
||||
`birth_date` VARCHAR(255) NULL,
|
||||
`seeded` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_tournament_id` (`tournament_id`),
|
||||
INDEX `idx_group_id` (`group_id`),
|
||||
CONSTRAINT `fk_external_participant_tournament` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `fk_external_participant_group` FOREIGN KEY (`group_id`) REFERENCES `tournament_group` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
16
backend/migrations/create_tournament_class_table.sql
Normal file
16
backend/migrations/create_tournament_class_table.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- Migration: Create tournament_class table
|
||||
-- Date: 2025-01-15
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `tournament_class` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`tournament_id` INT(11) NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `tournament_id` (`tournament_id`),
|
||||
CONSTRAINT `tournament_class_ibfk_1` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
33
backend/migrations/create_tournament_pairing_table.sql
Normal file
33
backend/migrations/create_tournament_pairing_table.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Migration: Create tournament_pairing table
|
||||
-- Date: 2025-01-23
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `tournament_pairing` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`tournament_id` INT(11) NOT NULL,
|
||||
`class_id` INT(11) NOT NULL,
|
||||
`group_id` INT(11) NULL,
|
||||
`member1_id` INT(11) NULL,
|
||||
`external1_id` INT(11) NULL,
|
||||
`member2_id` INT(11) NULL,
|
||||
`external2_id` INT(11) NULL,
|
||||
`seeded` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `tournament_id` (`tournament_id`),
|
||||
KEY `class_id` (`class_id`),
|
||||
KEY `group_id` (`group_id`),
|
||||
KEY `member1_id` (`member1_id`),
|
||||
KEY `member2_id` (`member2_id`),
|
||||
KEY `external1_id` (`external1_id`),
|
||||
KEY `external2_id` (`external2_id`),
|
||||
CONSTRAINT `tournament_pairing_ibfk_1` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `tournament_pairing_ibfk_2` FOREIGN KEY (`class_id`) REFERENCES `tournament_class` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `tournament_pairing_ibfk_3` FOREIGN KEY (`group_id`) REFERENCES `tournament_group` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
36
backend/migrations/create_training_group_tables.sql
Normal file
36
backend/migrations/create_training_group_tables.sql
Normal file
@@ -0,0 +1,36 @@
|
||||
-- Migration: Create training_group and member_training_group tables
|
||||
-- Date: 2025-01-16
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
-- Create training_group table
|
||||
CREATE TABLE IF NOT EXISTS `training_group` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`club_id` INT(11) NOT NULL,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`is_preset` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`preset_type` ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe') NULL,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `club_id` (`club_id`),
|
||||
CONSTRAINT `training_group_ibfk_1` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- Create member_training_group junction table
|
||||
CREATE TABLE IF NOT EXISTS `member_training_group` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`member_id` INT(11) NOT NULL,
|
||||
`training_group_id` INT(11) NOT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_member_group` (`member_id`, `training_group_id`),
|
||||
KEY `member_id` (`member_id`),
|
||||
KEY `training_group_id` (`training_group_id`),
|
||||
CONSTRAINT `member_training_group_ibfk_1` FOREIGN KEY (`member_id`) REFERENCES `member` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT `member_training_group_ibfk_2` FOREIGN KEY (`training_group_id`) REFERENCES `training_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
|
||||
19
backend/migrations/create_training_times_table.sql
Normal file
19
backend/migrations/create_training_times_table.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- Migration: Create training_times table
|
||||
-- Date: 2025-01-16
|
||||
-- For MariaDB/MySQL
|
||||
-- Stores training times for training groups
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `training_times` (
|
||||
`id` INT(11) NOT NULL AUTO_INCREMENT,
|
||||
`training_group_id` INT(11) NOT NULL,
|
||||
`weekday` TINYINT(1) NOT NULL COMMENT '0 = Sunday, 1 = Monday, ..., 6 = Saturday',
|
||||
`start_time` TIME NOT NULL,
|
||||
`end_time` TIME NOT NULL,
|
||||
`sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT 'Order for displaying multiple times on the same weekday',
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `training_group_id` (`training_group_id`),
|
||||
CONSTRAINT `training_times_ibfk_1` FOREIGN KEY (`training_group_id`) REFERENCES `training_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
92
backend/migrations/fix_seasons_and_teams.sql
Normal file
92
backend/migrations/fix_seasons_and_teams.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
-- Fix-Skript: Behebt häufige Probleme mit Seasons und Teams
|
||||
-- Führe dieses Skript auf dem Server aus, wenn die Diagnose Probleme zeigt
|
||||
|
||||
-- 1. Stelle sicher, dass die season-Tabelle existiert und die richtige Struktur hat
|
||||
-- (Falls die Tabelle nicht existiert, wird sie erstellt)
|
||||
CREATE TABLE IF NOT EXISTS `season` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`season` VARCHAR(255) NOT NULL UNIQUE,
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 2. Stelle sicher, dass die club_team-Tabelle die season_id-Spalte hat
|
||||
-- (Falls die Spalte nicht existiert, wird sie hinzugefügt)
|
||||
ALTER TABLE `club_team`
|
||||
ADD COLUMN IF NOT EXISTS `season_id` INT NULL;
|
||||
|
||||
-- 3. Erstelle die Seasons, falls sie fehlen
|
||||
INSERT IGNORE INTO `season` (`season`) VALUES ('2024/2025');
|
||||
INSERT IGNORE INTO `season` (`season`) VALUES ('2025/2026');
|
||||
|
||||
-- 4. Aktualisiere Teams ohne season_id auf die aktuelle Saison
|
||||
-- (Verwendet die neueste Saison basierend auf dem aktuellen Datum)
|
||||
UPDATE `club_team`
|
||||
SET `season_id` = (
|
||||
SELECT `id` FROM `season`
|
||||
WHERE `season` = (
|
||||
CASE
|
||||
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
|
||||
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
|
||||
END
|
||||
)
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE `season_id` IS NULL;
|
||||
|
||||
-- 5. Falls keine aktuelle Saison existiert, erstelle sie
|
||||
INSERT IGNORE INTO `season` (`season`) VALUES (
|
||||
CASE
|
||||
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
|
||||
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
|
||||
END
|
||||
);
|
||||
|
||||
-- 6. Aktualisiere Teams mit ungültigen season_id auf die aktuelle Saison
|
||||
UPDATE `club_team` ct
|
||||
LEFT JOIN `season` s ON ct.season_id = s.id
|
||||
SET ct.season_id = (
|
||||
SELECT `id` FROM `season`
|
||||
WHERE `season` = (
|
||||
CASE
|
||||
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
|
||||
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
|
||||
END
|
||||
)
|
||||
LIMIT 1
|
||||
)
|
||||
WHERE s.id IS NULL;
|
||||
|
||||
-- 7. Füge Foreign Key Constraint hinzu, falls er fehlt
|
||||
-- (Hinweis: MySQL/MariaDB unterstützt "IF NOT EXISTS" nicht für Constraints,
|
||||
-- daher müssen wir prüfen, ob der Constraint bereits existiert)
|
||||
SET @constraint_exists = (
|
||||
SELECT COUNT(*)
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'club_team'
|
||||
AND CONSTRAINT_NAME = 'club_team_season_id_foreign_idx'
|
||||
AND REFERENCED_TABLE_NAME = 'season'
|
||||
);
|
||||
|
||||
SET @sql = IF(@constraint_exists = 0,
|
||||
'ALTER TABLE `club_team` ADD CONSTRAINT `club_team_season_id_foreign_idx` FOREIGN KEY (`season_id`) REFERENCES `season` (`id`) ON DELETE CASCADE ON UPDATE CASCADE',
|
||||
'SELECT "Foreign key constraint already exists" as message'
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 8. Zeige die Ergebnisse
|
||||
SELECT '=== ERGEBNIS ===' as info;
|
||||
SELECT
|
||||
ct.id,
|
||||
ct.name,
|
||||
ct.season_id,
|
||||
s.season
|
||||
FROM `club_team` ct
|
||||
LEFT JOIN `season` s ON ct.season_id = s.id
|
||||
ORDER BY ct.id;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
-- Migration: Umbenennen von max_birth_year zu min_birth_year
|
||||
-- Datum: 2025-01-XX
|
||||
-- Beschreibung: Ändert die Logik von "geboren <= X" zu "geboren >= X"
|
||||
-- For MariaDB/MySQL
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'tournament_class';
|
||||
SET @oldcolumnname = 'max_birth_year';
|
||||
SET @newcolumnname = 'min_birth_year';
|
||||
|
||||
-- Check if old column exists
|
||||
SET @old_column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @oldcolumnname)
|
||||
);
|
||||
|
||||
-- Check if new column already exists
|
||||
SET @new_column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(TABLE_SCHEMA = @dbname)
|
||||
AND (TABLE_NAME = @tablename)
|
||||
AND (COLUMN_NAME = @newcolumnname)
|
||||
);
|
||||
|
||||
-- Rename column if old exists and new doesn't
|
||||
SET @sql = IF(@old_column_exists > 0 AND @new_column_exists = 0,
|
||||
CONCAT('ALTER TABLE `', @tablename, '` CHANGE COLUMN `', @oldcolumnname, '` `', @newcolumnname, '` INT(11) NULL DEFAULT NULL AFTER `gender`'),
|
||||
IF(@new_column_exists > 0,
|
||||
'SELECT 1 AS column_already_renamed',
|
||||
'SELECT 1 AS old_column_not_found'
|
||||
)
|
||||
);
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
-- Migration: Update my_tischtennis table TEXT fields to LONGTEXT for encrypted data
|
||||
-- Date: 2025-11-21
|
||||
-- For MariaDB/MySQL
|
||||
--
|
||||
-- Problem: Encrypted data can be very long, and TEXT fields (max 65KB) are too small
|
||||
-- Solution: Change to LONGTEXT (max 4GB) for all encrypted fields
|
||||
|
||||
-- Update user_data to LONGTEXT
|
||||
ALTER TABLE `my_tischtennis`
|
||||
MODIFY COLUMN `user_data` LONGTEXT NULL;
|
||||
|
||||
-- Update access_token to LONGTEXT
|
||||
ALTER TABLE `my_tischtennis`
|
||||
MODIFY COLUMN `access_token` LONGTEXT NULL;
|
||||
|
||||
-- Update refresh_token to LONGTEXT
|
||||
ALTER TABLE `my_tischtennis`
|
||||
MODIFY COLUMN `refresh_token` LONGTEXT NULL;
|
||||
|
||||
-- Update cookie to LONGTEXT
|
||||
ALTER TABLE `my_tischtennis`
|
||||
MODIFY COLUMN `cookie` LONGTEXT NULL;
|
||||
|
||||
-- Update encrypted_password to LONGTEXT
|
||||
ALTER TABLE `my_tischtennis`
|
||||
MODIFY COLUMN `encrypted_password` LONGTEXT NULL;
|
||||
|
||||
-- Update club_id to LONGTEXT (was VARCHAR, but encrypted data can be longer)
|
||||
ALTER TABLE `my_tischtennis`
|
||||
MODIFY COLUMN `club_id` LONGTEXT NULL;
|
||||
|
||||
-- Update club_name to LONGTEXT (was VARCHAR, but encrypted data can be longer)
|
||||
ALTER TABLE `my_tischtennis`
|
||||
MODIFY COLUMN `club_name` LONGTEXT NULL;
|
||||
|
||||
-- Update fed_nickname to LONGTEXT (was VARCHAR, but encrypted data can be longer)
|
||||
ALTER TABLE `my_tischtennis`
|
||||
MODIFY COLUMN `fed_nickname` LONGTEXT NULL;
|
||||
|
||||
33
backend/models/ClubDisabledPresetGroup.js
Normal file
33
backend/models/ClubDisabledPresetGroup.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Club from './Club.js';
|
||||
|
||||
const ClubDisabledPresetGroup = sequelize.define('ClubDisabledPresetGroup', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Club,
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
presetType: {
|
||||
type: DataTypes.ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe'),
|
||||
allowNull: false,
|
||||
comment: 'Type of preset group that is disabled for this club'
|
||||
}
|
||||
}, {
|
||||
tableName: 'club_disabled_preset_groups',
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default ClubDisabledPresetGroup;
|
||||
|
||||
94
backend/models/ExternalTournamentParticipant.js
Normal file
94
backend/models/ExternalTournamentParticipant.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import { encryptData, decryptData } from '../utils/encrypt.js';
|
||||
|
||||
const ExternalTournamentParticipant = sequelize.define('ExternalTournamentParticipant', {
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
groupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: false,
|
||||
allowNull: true
|
||||
},
|
||||
firstName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
set(value) {
|
||||
const encryptedValue = encryptData(value);
|
||||
this.setDataValue('firstName', encryptedValue);
|
||||
},
|
||||
get() {
|
||||
const encryptedValue = this.getDataValue('firstName');
|
||||
return decryptData(encryptedValue);
|
||||
}
|
||||
},
|
||||
lastName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
set(value) {
|
||||
const encryptedValue = encryptData(value);
|
||||
this.setDataValue('lastName', encryptedValue);
|
||||
},
|
||||
get() {
|
||||
const encryptedValue = this.getDataValue('lastName');
|
||||
return decryptData(encryptedValue);
|
||||
}
|
||||
},
|
||||
club: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
set(value) {
|
||||
if (!value) {
|
||||
this.setDataValue('club', null);
|
||||
return;
|
||||
}
|
||||
const encryptedValue = encryptData(value);
|
||||
this.setDataValue('club', encryptedValue);
|
||||
},
|
||||
get() {
|
||||
const encryptedValue = this.getDataValue('club');
|
||||
if (!encryptedValue) return null;
|
||||
return decryptData(encryptedValue);
|
||||
}
|
||||
},
|
||||
birthDate: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
set(value) {
|
||||
if (!value) {
|
||||
this.setDataValue('birthDate', null);
|
||||
return;
|
||||
}
|
||||
const encryptedValue = encryptData(value || '');
|
||||
this.setDataValue('birthDate', encryptedValue);
|
||||
},
|
||||
get() {
|
||||
const encryptedValue = this.getDataValue('birthDate');
|
||||
if (!encryptedValue) return null;
|
||||
return decryptData(encryptedValue);
|
||||
}
|
||||
},
|
||||
gender: {
|
||||
type: DataTypes.ENUM('male', 'female', 'diverse', 'unknown'),
|
||||
allowNull: true,
|
||||
defaultValue: 'unknown'
|
||||
},
|
||||
seeded: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'external_tournament_participant',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default ExternalTournamentParticipant;
|
||||
|
||||
40
backend/models/MemberTrainingGroup.js
Normal file
40
backend/models/MemberTrainingGroup.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Member from './Member.js';
|
||||
import TrainingGroup from './TrainingGroup.js';
|
||||
|
||||
const MemberTrainingGroup = sequelize.define('MemberTrainingGroup', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
memberId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Member,
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
trainingGroupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: TrainingGroup,
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
}
|
||||
}, {
|
||||
tableName: 'member_training_group',
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default MemberTrainingGroup;
|
||||
|
||||
|
||||
|
||||
@@ -22,9 +22,17 @@ 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,
|
||||
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data
|
||||
allowNull: true,
|
||||
field: 'encrypted_password'
|
||||
},
|
||||
@@ -41,14 +49,40 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
|
||||
field: 'auto_update_ratings'
|
||||
},
|
||||
accessToken: {
|
||||
type: DataTypes.TEXT,
|
||||
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data
|
||||
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,
|
||||
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data
|
||||
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,
|
||||
@@ -56,28 +90,100 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
|
||||
field: 'expires_at'
|
||||
},
|
||||
cookie: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data
|
||||
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('long'), // Use LONGTEXT to support very long encrypted strings
|
||||
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,
|
||||
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data (can be longer than VARCHAR)
|
||||
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,
|
||||
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data (can be longer than VARCHAR)
|
||||
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,
|
||||
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data (can be longer than VARCHAR)
|
||||
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,
|
||||
|
||||
@@ -17,6 +17,7 @@ const Tournament = sequelize.define('Tournament', {
|
||||
advancingPerGroup: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
},
|
||||
numberOfGroups: {
|
||||
type: DataTypes.INTEGER,
|
||||
@@ -28,7 +29,16 @@ const Tournament = sequelize.define('Tournament', {
|
||||
allowNull: false,
|
||||
defaultValue: 1
|
||||
},
|
||||
advancingPerGroup: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 },
|
||||
winningSets: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 3,
|
||||
},
|
||||
allowsExternal: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament',
|
||||
|
||||
55
backend/models/TournamentClass.js
Normal file
55
backend/models/TournamentClass.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Tournament from './Tournament.js';
|
||||
|
||||
const TournamentClass = sequelize.define('TournamentClass', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Tournament,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
sortOrder: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0
|
||||
},
|
||||
isDoubles: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
gender: {
|
||||
type: DataTypes.ENUM('male', 'female', 'mixed'),
|
||||
allowNull: true,
|
||||
defaultValue: null
|
||||
},
|
||||
minBirthYear: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
field: 'min_birth_year',
|
||||
comment: 'Geboren im Jahr X oder später (>=)'
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_class',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
export default TournamentClass;
|
||||
|
||||
@@ -8,10 +8,18 @@ const TournamentGroup = sequelize.define('TournamentGroup', {
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
stageId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
tournamentId : {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_group',
|
||||
|
||||
@@ -5,6 +5,10 @@ import Tournament from './Tournament.js';
|
||||
import TournamentGroup from './TournamentGroup.js';
|
||||
|
||||
const TournamentMatch = sequelize.define('TournamentMatch', {
|
||||
stageId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
@@ -25,6 +29,10 @@ const TournamentMatch = sequelize.define('TournamentMatch', {
|
||||
onDelete: 'SET NULL',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
groupRound: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
@@ -35,17 +43,22 @@ const TournamentMatch = sequelize.define('TournamentMatch', {
|
||||
},
|
||||
player1Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
},
|
||||
player2Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
allowNull: true,
|
||||
},
|
||||
isFinished: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
result: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
|
||||
@@ -16,6 +16,15 @@ const TournamentMember = sequelize.define('TournamentMember', {
|
||||
type: DataTypes.INTEGER,
|
||||
autoIncrement: false,
|
||||
allowNull: false
|
||||
},
|
||||
seeded: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
|
||||
71
backend/models/TournamentPairing.js
Normal file
71
backend/models/TournamentPairing.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Tournament from './Tournament.js';
|
||||
import TournamentClass from './TournamentClass.js';
|
||||
|
||||
const TournamentPairing = sequelize.define('TournamentPairing', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Tournament,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
classId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: TournamentClass,
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
onUpdate: 'CASCADE'
|
||||
},
|
||||
groupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
// Player 1: entweder Mitglied oder externer Teilnehmer
|
||||
member1Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
external1Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
// Player 2: entweder Mitglied oder externer Teilnehmer
|
||||
member2Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
external2Id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
seeded: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_pairing',
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
export default TournamentPairing;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
46
backend/models/TournamentStage.js
Normal file
46
backend/models/TournamentStage.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const TournamentStage = sequelize.define('TournamentStage', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
index: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
field: 'stage_index',
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false, // 'groups' | 'knockout'
|
||||
},
|
||||
numberOfGroups: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
advancingPerGroup: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
maxGroupSize: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_stage',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TournamentStage;
|
||||
40
backend/models/TournamentStageAdvancement.js
Normal file
40
backend/models/TournamentStageAdvancement.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
|
||||
const TournamentStageAdvancement = sequelize.define('TournamentStageAdvancement', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
tournamentId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
fromStageId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
toStageId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
mode: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'pools',
|
||||
},
|
||||
config: {
|
||||
// JSON: { pools: [{ fromPlaces:[1,2], target:{ type:'groups', groupCount:2 }}, ...] }
|
||||
type: DataTypes.JSON,
|
||||
allowNull: false,
|
||||
defaultValue: {},
|
||||
},
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'tournament_stage_advancement',
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TournamentStageAdvancement;
|
||||
51
backend/models/TrainingGroup.js
Normal file
51
backend/models/TrainingGroup.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import Club from './Club.js';
|
||||
|
||||
const TrainingGroup = sequelize.define('TrainingGroup', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: Club,
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
isPreset: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
comment: 'True if this is a preset group (Anfänger, Fortgeschrittene, etc.)'
|
||||
},
|
||||
presetType: {
|
||||
type: DataTypes.ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe'),
|
||||
allowNull: true,
|
||||
comment: 'Type of preset group'
|
||||
},
|
||||
sortOrder: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: 'Order for displaying groups'
|
||||
}
|
||||
}, {
|
||||
tableName: 'training_group',
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TrainingGroup;
|
||||
|
||||
|
||||
|
||||
47
backend/models/TrainingTime.js
Normal file
47
backend/models/TrainingTime.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import TrainingGroup from './TrainingGroup.js';
|
||||
|
||||
const TrainingTime = sequelize.define('TrainingTime', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
trainingGroupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: TrainingGroup,
|
||||
key: 'id',
|
||||
},
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
weekday: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: '0 = Sunday, 1 = Monday, ..., 6 = Saturday'
|
||||
},
|
||||
startTime: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: false,
|
||||
},
|
||||
endTime: {
|
||||
type: DataTypes.TIME,
|
||||
allowNull: false,
|
||||
},
|
||||
sortOrder: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: 'Order for displaying multiple times on the same weekday'
|
||||
}
|
||||
}, {
|
||||
tableName: 'training_times',
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
});
|
||||
|
||||
export default TrainingTime;
|
||||
|
||||
@@ -27,9 +27,14 @@ import Group from './Group.js';
|
||||
import GroupActivity from './GroupActivity.js';
|
||||
import Tournament from './Tournament.js';
|
||||
import TournamentGroup from './TournamentGroup.js';
|
||||
import TournamentClass from './TournamentClass.js';
|
||||
import TournamentMember from './TournamentMember.js';
|
||||
import TournamentMatch from './TournamentMatch.js';
|
||||
import TournamentResult from './TournamentResult.js';
|
||||
import ExternalTournamentParticipant from './ExternalTournamentParticipant.js';
|
||||
import TournamentPairing from './TournamentPairing.js';
|
||||
import TournamentStage from './TournamentStage.js';
|
||||
import TournamentStageAdvancement from './TournamentStageAdvancement.js';
|
||||
import Accident from './Accident.js';
|
||||
import UserToken from './UserToken.js';
|
||||
import OfficialTournament from './OfficialTournament.js';
|
||||
@@ -42,6 +47,10 @@ import ApiLog from './ApiLog.js';
|
||||
import MemberTransferConfig from './MemberTransferConfig.js';
|
||||
import MemberContact from './MemberContact.js';
|
||||
import MemberImage from './MemberImage.js';
|
||||
import TrainingGroup from './TrainingGroup.js';
|
||||
import MemberTrainingGroup from './MemberTrainingGroup.js';
|
||||
import ClubDisabledPresetGroup from './ClubDisabledPresetGroup.js';
|
||||
import TrainingTime from './TrainingTime.js';
|
||||
// Official tournaments relations
|
||||
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
|
||||
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
@@ -185,6 +194,13 @@ Club.hasMany(Tournament, { foreignKey: 'clubId', as: 'tournaments' });
|
||||
TournamentGroup.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournaments' });
|
||||
Tournament.hasMany(TournamentGroup, { foreignKey: 'tournamentId', as: 'tournamentGroups' });
|
||||
|
||||
// Tournament Stages
|
||||
TournamentStage.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentStage, { foreignKey: 'tournamentId', as: 'stages' });
|
||||
|
||||
TournamentStageAdvancement.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentStageAdvancement, { foreignKey: 'tournamentId', as: 'stageAdvancements' });
|
||||
|
||||
TournamentMember.belongsTo(TournamentGroup, {
|
||||
foreignKey: 'groupId',
|
||||
targetKey: 'id',
|
||||
@@ -201,6 +217,15 @@ Member.hasMany(TournamentMember, { foreignKey: 'clubMemberId', as: 'tournamentGr
|
||||
|
||||
TournamentMember.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentMember, { foreignKey: 'tournamentId', as: 'tournamentMembers' });
|
||||
TournamentMember.belongsTo(TournamentClass, {
|
||||
foreignKey: 'classId',
|
||||
as: 'class',
|
||||
constraints: false
|
||||
});
|
||||
TournamentClass.hasMany(TournamentMember, {
|
||||
foreignKey: 'classId',
|
||||
as: 'members'
|
||||
});
|
||||
|
||||
TournamentMatch.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentMatch, { foreignKey: 'tournamentId', as: 'tournamentMatches' });
|
||||
@@ -227,6 +252,68 @@ TournamentMatch.belongsTo(TournamentMember, { foreignKey: 'player2Id', as: 'play
|
||||
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player1Id', as: 'player1Matches' });
|
||||
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player2Id', as: 'player2Matches' });
|
||||
|
||||
// Tournament Classes
|
||||
TournamentClass.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentClass, { foreignKey: 'tournamentId', as: 'classes' });
|
||||
|
||||
// External Tournament Participants
|
||||
ExternalTournamentParticipant.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(ExternalTournamentParticipant, { foreignKey: 'tournamentId', as: 'externalParticipants' });
|
||||
ExternalTournamentParticipant.belongsTo(TournamentGroup, {
|
||||
foreignKey: 'groupId',
|
||||
targetKey: 'id',
|
||||
as: 'group',
|
||||
constraints: false
|
||||
});
|
||||
TournamentGroup.hasMany(ExternalTournamentParticipant, {
|
||||
foreignKey: 'groupId',
|
||||
as: 'externalGroupMembers'
|
||||
});
|
||||
ExternalTournamentParticipant.belongsTo(TournamentClass, {
|
||||
foreignKey: 'classId',
|
||||
as: 'class',
|
||||
constraints: false
|
||||
});
|
||||
TournamentClass.hasMany(ExternalTournamentParticipant, {
|
||||
foreignKey: 'classId',
|
||||
as: 'externalParticipants'
|
||||
});
|
||||
|
||||
// Tournament Pairings
|
||||
TournamentPairing.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
Tournament.hasMany(TournamentPairing, { foreignKey: 'tournamentId', as: 'pairings' });
|
||||
TournamentPairing.belongsTo(TournamentClass, { foreignKey: 'classId', as: 'class' });
|
||||
TournamentClass.hasMany(TournamentPairing, { foreignKey: 'classId', as: 'pairings' });
|
||||
TournamentPairing.belongsTo(TournamentGroup, {
|
||||
foreignKey: 'groupId',
|
||||
as: 'group',
|
||||
constraints: false
|
||||
});
|
||||
TournamentGroup.hasMany(TournamentPairing, {
|
||||
foreignKey: 'groupId',
|
||||
as: 'pairings'
|
||||
});
|
||||
TournamentPairing.belongsTo(TournamentMember, {
|
||||
foreignKey: 'member1Id',
|
||||
as: 'member1',
|
||||
constraints: false
|
||||
});
|
||||
TournamentPairing.belongsTo(TournamentMember, {
|
||||
foreignKey: 'member2Id',
|
||||
as: 'member2',
|
||||
constraints: false
|
||||
});
|
||||
TournamentPairing.belongsTo(ExternalTournamentParticipant, {
|
||||
foreignKey: 'external1Id',
|
||||
as: 'external1',
|
||||
constraints: false
|
||||
});
|
||||
TournamentPairing.belongsTo(ExternalTournamentParticipant, {
|
||||
foreignKey: 'external2Id',
|
||||
as: 'external2',
|
||||
constraints: false
|
||||
});
|
||||
|
||||
Accident.belongsTo(Member, { foreignKey: 'memberId', as: 'members' });
|
||||
Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
|
||||
|
||||
@@ -254,6 +341,31 @@ MemberContact.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
|
||||
Member.hasMany(MemberImage, { foreignKey: 'memberId', as: 'images' });
|
||||
MemberImage.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
|
||||
|
||||
// Training Groups
|
||||
Club.hasMany(TrainingGroup, { foreignKey: 'clubId', as: 'trainingGroups' });
|
||||
TrainingGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
|
||||
Member.belongsToMany(TrainingGroup, {
|
||||
through: MemberTrainingGroup,
|
||||
foreignKey: 'memberId',
|
||||
otherKey: 'trainingGroupId',
|
||||
as: 'trainingGroups'
|
||||
});
|
||||
TrainingGroup.belongsToMany(Member, {
|
||||
through: MemberTrainingGroup,
|
||||
foreignKey: 'trainingGroupId',
|
||||
otherKey: 'memberId',
|
||||
as: 'members'
|
||||
});
|
||||
|
||||
// Club Disabled Preset Groups
|
||||
Club.hasMany(ClubDisabledPresetGroup, { foreignKey: 'clubId', as: 'disabledPresetGroups' });
|
||||
ClubDisabledPresetGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
|
||||
|
||||
// Training Times
|
||||
TrainingGroup.hasMany(TrainingTime, { foreignKey: 'trainingGroupId', as: 'trainingTimes' });
|
||||
TrainingTime.belongsTo(TrainingGroup, { foreignKey: 'trainingGroupId', as: 'trainingGroup' });
|
||||
|
||||
export {
|
||||
User,
|
||||
Log,
|
||||
@@ -283,9 +395,12 @@ export {
|
||||
GroupActivity,
|
||||
Tournament,
|
||||
TournamentGroup,
|
||||
TournamentClass,
|
||||
TournamentMember,
|
||||
TournamentMatch,
|
||||
TournamentResult,
|
||||
ExternalTournamentParticipant,
|
||||
TournamentPairing,
|
||||
Accident,
|
||||
UserToken,
|
||||
OfficialTournament,
|
||||
@@ -298,4 +413,8 @@ export {
|
||||
MemberTransferConfig,
|
||||
MemberContact,
|
||||
MemberImage,
|
||||
TrainingGroup,
|
||||
MemberTrainingGroup,
|
||||
ClubDisabledPresetGroup,
|
||||
TrainingTime,
|
||||
};
|
||||
|
||||
220
backend/node_modules/.package-lock.json
generated
vendored
220
backend/node_modules/.package-lock.json
generated
vendored
@@ -432,6 +432,12 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
@@ -455,6 +461,15 @@
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"license": "MIT",
|
||||
@@ -807,6 +822,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.1.1",
|
||||
"hasInstallScript": true,
|
||||
@@ -1490,6 +1514,67 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"dev": true,
|
||||
@@ -2585,7 +2670,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -4305,6 +4392,116 @@
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.7",
|
||||
"dev": true,
|
||||
@@ -5191,6 +5388,27 @@
|
||||
"version": "1.0.2",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"license": "MIT",
|
||||
|
||||
223
backend/package-lock.json
generated
223
backend/package-lock.json
generated
@@ -28,7 +28,8 @@
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pdfjs-dist": "^5.4.394",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.5"
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -467,6 +468,12 @@
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
@@ -490,6 +497,15 @@
|
||||
"assertion-error": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.12",
|
||||
"license": "MIT",
|
||||
@@ -842,6 +858,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64id": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^4.5.0 || >= 5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.1.1",
|
||||
"hasInstallScript": true,
|
||||
@@ -1525,6 +1550,67 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.7.2",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"dev": true,
|
||||
@@ -2620,7 +2706,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -4340,6 +4428,116 @@
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "~4.3.4",
|
||||
"ws": "~8.17.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-adapter/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socket.io/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.8.7",
|
||||
"dev": true,
|
||||
@@ -5226,6 +5424,27 @@
|
||||
"version": "1.0.2",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
"pdf-parse": "^1.1.1",
|
||||
"pdfjs-dist": "^5.4.394",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.5"
|
||||
"sharp": "^0.33.5",
|
||||
"socket.io": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
updateDiaryDateActivityOrder,
|
||||
getDiaryDateActivities,
|
||||
addGroupActivity,
|
||||
updateGroupActivity,
|
||||
deleteGroupActivity,
|
||||
} from '../controllers/diaryDateActivityController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
@@ -15,6 +16,7 @@ const router = express.Router();
|
||||
router.use(authenticate);
|
||||
|
||||
router.post('/group', addGroupActivity);
|
||||
router.put('/group/:clubId/:groupActivityId', updateGroupActivity);
|
||||
router.delete('/group/:clubId/:groupActivityId', deleteGroupActivity);
|
||||
router.post('/:clubId/', createDiaryDateActivity);
|
||||
router.put('/:clubId/:id/order', updateDiaryDateActivityOrder);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { addGroup, getGroups, changeGroup } from '../controllers/groupController.js';
|
||||
import { addGroup, getGroups, changeGroup, deleteGroup } from '../controllers/groupController.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -9,5 +9,6 @@ router.use(authenticate);
|
||||
router.post('/', addGroup);
|
||||
router.get('/:clubId/:dateId', getGroups);
|
||||
router.put('/:groupId', changeGroup);
|
||||
router.delete('/:groupId', deleteGroup);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -6,7 +6,17 @@ import { authorize } from '../middleware/authorizationMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication
|
||||
// Login-Page und Login-Submit müssen VOR authenticate stehen, da iframe keinen Token mitsenden kann
|
||||
// GET /api/mytischtennis/login-page - Proxy für Login-Seite (für iframe)
|
||||
router.get('/login-page', myTischtennisController.getLoginPage);
|
||||
|
||||
// POST /api/mytischtennis/login-submit - Proxy für Login-Form-Submission
|
||||
router.post('/login-submit', myTischtennisController.submitLogin);
|
||||
|
||||
// POST /api/mytischtennis/extract-session - Extrahiere Session nach Login im iframe
|
||||
router.post('/extract-session', myTischtennisController.extractSession);
|
||||
|
||||
// All other routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// GET /api/mytischtennis/account - Get account (alle dürfen lesen)
|
||||
|
||||
@@ -2,11 +2,13 @@ import express from 'express';
|
||||
import {
|
||||
getTournaments,
|
||||
addTournament,
|
||||
updateTournament,
|
||||
addParticipant,
|
||||
getParticipants,
|
||||
setModus,
|
||||
createGroups,
|
||||
fillGroups,
|
||||
createGroupMatches,
|
||||
getGroups,
|
||||
getTournament,
|
||||
getTournamentMatches,
|
||||
@@ -17,10 +19,32 @@ import {
|
||||
resetGroups,
|
||||
resetMatches,
|
||||
removeParticipant,
|
||||
updateParticipantSeeded,
|
||||
deleteMatchResult,
|
||||
reopenMatch,
|
||||
deleteKnockoutMatches,
|
||||
setMatchActive,
|
||||
addExternalParticipant,
|
||||
getExternalParticipants,
|
||||
removeExternalParticipant,
|
||||
updateExternalParticipantSeeded,
|
||||
getTournamentClasses,
|
||||
addTournamentClass,
|
||||
updateTournamentClass,
|
||||
deleteTournamentClass,
|
||||
updateParticipantClass,
|
||||
createGroupsPerClass,
|
||||
assignParticipantToGroup,
|
||||
getPairings,
|
||||
createPairing,
|
||||
updatePairing,
|
||||
deletePairing,
|
||||
} from '../controllers/tournamentController.js';
|
||||
import {
|
||||
getStages,
|
||||
upsertStages,
|
||||
advanceStage,
|
||||
} from '../controllers/tournamentStagesController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -28,22 +52,53 @@ const router = express.Router();
|
||||
router.post('/participant', authenticate, addParticipant);
|
||||
router.post('/participants', authenticate, getParticipants);
|
||||
router.delete('/participant', authenticate, removeParticipant);
|
||||
router.put('/participant/:clubId/:tournamentId/:participantId/seeded', authenticate, updateParticipantSeeded);
|
||||
router.post('/modus', authenticate, setModus);
|
||||
router.post('/groups/reset', authenticate, resetGroups);
|
||||
router.post('/matches/reset', authenticate, resetMatches);
|
||||
router.put('/groups', authenticate, createGroups);
|
||||
router.post('/groups/create', authenticate, createGroupsPerClass);
|
||||
router.post('/groups', authenticate, fillGroups);
|
||||
router.post('/matches/create', authenticate, createGroupMatches);
|
||||
router.get('/groups', authenticate, getGroups);
|
||||
router.post('/match/result', authenticate, addMatchResult);
|
||||
router.delete('/match/result', authenticate, deleteMatchResult);
|
||||
router.post("/match/reopen", reopenMatch);
|
||||
router.post("/match/reopen", authenticate, reopenMatch);
|
||||
router.post('/match/finish', authenticate, finishMatch);
|
||||
router.put('/match/:clubId/:tournamentId/:matchId/active', authenticate, setMatchActive);
|
||||
router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches);
|
||||
router.get('/:clubId/:tournamentId', authenticate, getTournament);
|
||||
router.get('/:clubId', authenticate, getTournaments);
|
||||
router.post('/knockout', authenticate, startKnockout);
|
||||
router.delete("/matches/knockout", deleteKnockoutMatches);
|
||||
router.delete("/matches/knockout", authenticate, deleteKnockoutMatches);
|
||||
router.post('/groups/manual', authenticate, manualAssignGroups);
|
||||
router.put('/participant/group', authenticate, assignParticipantToGroup); // Muss VOR /:clubId/:tournamentId stehen!
|
||||
router.put('/:clubId/:tournamentId', authenticate, updateTournament);
|
||||
router.get('/:clubId/:tournamentId', authenticate, getTournament);
|
||||
// Externe Teilnehmer
|
||||
router.post('/external-participant', authenticate, addExternalParticipant);
|
||||
router.post('/external-participants', authenticate, getExternalParticipants);
|
||||
router.delete('/external-participant', authenticate, removeExternalParticipant);
|
||||
router.put('/external-participant/:clubId/:tournamentId/:participantId/seeded', authenticate, updateExternalParticipantSeeded);
|
||||
|
||||
// Tournament Classes
|
||||
router.get('/classes/:clubId/:tournamentId', authenticate, getTournamentClasses);
|
||||
router.post('/class/:clubId/:tournamentId', authenticate, addTournamentClass);
|
||||
router.put('/class/:clubId/:tournamentId/:classId', authenticate, updateTournamentClass);
|
||||
router.delete('/class/:clubId/:tournamentId/:classId', authenticate, deleteTournamentClass);
|
||||
router.put('/participant/:clubId/:tournamentId/:participantId/class', authenticate, updateParticipantClass);
|
||||
|
||||
// Tournament Pairings
|
||||
router.get('/pairings/:clubId/:tournamentId/:classId', authenticate, getPairings);
|
||||
router.post('/pairing/:clubId/:tournamentId/:classId', authenticate, createPairing);
|
||||
router.put('/pairing/:clubId/:tournamentId/:pairingId', authenticate, updatePairing);
|
||||
router.delete('/pairing/:clubId/:tournamentId/:pairingId', authenticate, deletePairing);
|
||||
|
||||
// Tournament Stages (mehrere Runden)
|
||||
router.get('/stages', authenticate, getStages);
|
||||
router.put('/stages', authenticate, upsertStages);
|
||||
router.post('/stages/advance', authenticate, advanceStage);
|
||||
|
||||
// Muss NACH allen festen Pfaden stehen, sonst matcht z.B. '/stages' als clubId='stages'
|
||||
router.get('/:clubId', authenticate, getTournaments);
|
||||
router.post('/', authenticate, addTournament);
|
||||
|
||||
export default router;
|
||||
|
||||
33
backend/routes/trainingGroupRoutes.js
Normal file
33
backend/routes/trainingGroupRoutes.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import {
|
||||
getTrainingGroups,
|
||||
createTrainingGroup,
|
||||
updateTrainingGroup,
|
||||
deleteTrainingGroup,
|
||||
addMemberToGroup,
|
||||
removeMemberFromGroup,
|
||||
getMemberGroups,
|
||||
ensurePresetGroups,
|
||||
enablePresetGroup,
|
||||
} from '../controllers/trainingGroupController.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
|
||||
// Spezifischere Routen zuerst (mit /member/ im Pfad)
|
||||
router.get('/:clubId/member/:memberId', getMemberGroups);
|
||||
router.post('/:clubId/:groupId/member/:memberId', addMemberToGroup);
|
||||
router.delete('/:clubId/:groupId/member/:memberId', removeMemberFromGroup);
|
||||
|
||||
// Allgemeinere Routen danach
|
||||
router.post('/:clubId/ensure-preset-groups', ensurePresetGroups);
|
||||
router.post('/:clubId/enable-preset-group/:presetType', enablePresetGroup);
|
||||
router.get('/:clubId', getTrainingGroups);
|
||||
router.post('/:clubId', createTrainingGroup);
|
||||
router.put('/:clubId/:groupId', updateTrainingGroup);
|
||||
router.delete('/:clubId/:groupId', deleteTrainingGroup);
|
||||
|
||||
export default router;
|
||||
|
||||
19
backend/routes/trainingTimeRoutes.js
Normal file
19
backend/routes/trainingTimeRoutes.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import {
|
||||
getTrainingTimes,
|
||||
createTrainingTime,
|
||||
updateTrainingTime,
|
||||
deleteTrainingTime
|
||||
} from '../controllers/trainingTimeController.js';
|
||||
|
||||
const router = express.Router();
|
||||
router.use(authenticate);
|
||||
|
||||
router.get('/:clubId', getTrainingTimes);
|
||||
router.post('/:clubId', createTrainingTime);
|
||||
router.put('/:clubId/:timeId', updateTrainingTime);
|
||||
router.delete('/:clubId/:timeId', deleteTrainingTime);
|
||||
|
||||
export default router;
|
||||
|
||||
107
backend/scripts/checkDatabase.js
Normal file
107
backend/scripts/checkDatabase.js
Normal file
@@ -0,0 +1,107 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { development } from '../config.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load .env from backend directory
|
||||
dotenv.config({ path: join(__dirname, '..', '.env') });
|
||||
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || development.host || 'localhost',
|
||||
user: process.env.DB_USER || development.username || 'root',
|
||||
password: process.env.DB_PASSWORD || development.password || '',
|
||||
database: process.env.DB_NAME || development.database || 'trainingdiary',
|
||||
};
|
||||
|
||||
async function checkDatabase() {
|
||||
let connection;
|
||||
try {
|
||||
console.log('=== Datenbank-Konfiguration ===');
|
||||
console.log('Host:', dbConfig.host);
|
||||
console.log('User:', dbConfig.user);
|
||||
console.log('Database:', dbConfig.database);
|
||||
console.log('Password:', dbConfig.password ? '***' : '(leer)');
|
||||
console.log('');
|
||||
|
||||
connection = await mysql.createConnection({
|
||||
host: dbConfig.host,
|
||||
user: dbConfig.user,
|
||||
password: dbConfig.password,
|
||||
});
|
||||
|
||||
// Zeige alle verfügbaren Datenbanken
|
||||
const [databases] = await connection.execute('SHOW DATABASES');
|
||||
console.log('=== Verfügbare Datenbanken ===');
|
||||
databases.forEach(db => {
|
||||
const dbName = db.Database;
|
||||
const isCurrent = dbName === dbConfig.database;
|
||||
console.log(`${isCurrent ? '→' : ' '} ${dbName}${isCurrent ? ' (VERWENDET)' : ''}`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
// Verbinde mit der konfigurierten Datenbank
|
||||
await connection.end();
|
||||
connection = await mysql.createConnection(dbConfig);
|
||||
|
||||
// Prüfe, ob die Tabellen existieren
|
||||
const [tables] = await connection.execute('SHOW TABLES');
|
||||
console.log(`=== Tabellen in "${dbConfig.database}" ===`);
|
||||
console.log(`Anzahl: ${tables.length}`);
|
||||
console.log('');
|
||||
|
||||
// Prüfe spezifische Tabellen
|
||||
const tableNames = tables.map(t => Object.values(t)[0]);
|
||||
const requiredTables = [
|
||||
'training_group',
|
||||
'member_training_group',
|
||||
'club_disabled_preset_groups',
|
||||
'training_times',
|
||||
'official_tournaments',
|
||||
];
|
||||
|
||||
console.log('=== Prüfung wichtiger Tabellen ===');
|
||||
for (const table of requiredTables) {
|
||||
const exists = tableNames.includes(table);
|
||||
console.log(`${exists ? '✓' : '✗'} ${table}`);
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Prüfe official_tournaments Daten
|
||||
if (tableNames.includes('official_tournaments')) {
|
||||
const [count] = await connection.execute('SELECT COUNT(*) as count FROM official_tournaments');
|
||||
console.log(`=== official_tournaments Daten ===`);
|
||||
console.log(`Anzahl Einträge: ${count[0].count}`);
|
||||
|
||||
if (count[0].count > 0) {
|
||||
// Prüfe zuerst die Spaltennamen
|
||||
const [columns] = await connection.execute('SHOW COLUMNS FROM official_tournaments');
|
||||
console.log('Spalten in official_tournaments:');
|
||||
columns.forEach(col => {
|
||||
console.log(` - ${col.Field} (${col.Type})`);
|
||||
});
|
||||
console.log('');
|
||||
|
||||
const [rows] = await connection.execute('SELECT * FROM official_tournaments LIMIT 5');
|
||||
console.log('Erste Einträge:');
|
||||
rows.forEach(row => {
|
||||
console.log(` - ID: ${row.id}, Daten:`, JSON.stringify(row, null, 2));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler:', error.message);
|
||||
if (error.code) {
|
||||
console.error('Error Code:', error.code);
|
||||
}
|
||||
} finally {
|
||||
if (connection) await connection.end();
|
||||
}
|
||||
}
|
||||
|
||||
checkDatabase();
|
||||
|
||||
59
backend/scripts/checkPermissions.js
Normal file
59
backend/scripts/checkPermissions.js
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Prüft die Berechtigungen für SSL-Zertifikate
|
||||
*/
|
||||
|
||||
import { readFileSync, statSync } from 'fs';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const certPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
|
||||
|
||||
console.log('🔍 Prüfe SSL-Zertifikat-Berechtigungen...\n');
|
||||
|
||||
// Prüfe, welcher Benutzer den Service ausführt
|
||||
try {
|
||||
const serviceUser = execSync('systemctl show -p User tt-tagebuch.service 2>/dev/null | cut -d= -f2', { encoding: 'utf-8' }).trim();
|
||||
console.log(`📋 Service-Benutzer: ${serviceUser}`);
|
||||
|
||||
// Prüfe Gruppen des Service-Benutzers
|
||||
const groups = execSync(`groups ${serviceUser} 2>/dev/null || id -Gn ${serviceUser} 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
||||
console.log(`📋 Gruppen von ${serviceUser}: ${groups}`);
|
||||
|
||||
if (groups.includes('ssl-cert')) {
|
||||
console.log('✅ Service-Benutzer ist in der ssl-cert-Gruppe');
|
||||
} else {
|
||||
console.log('❌ Service-Benutzer ist NICHT in der ssl-cert-Gruppe!');
|
||||
console.log(' → Führe aus: sudo ./scripts/fixCertPermissions.sh');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('⚠️ Konnte Service-Benutzer nicht ermitteln:', err.message);
|
||||
}
|
||||
|
||||
// Prüfe Dateiberechtigungen
|
||||
try {
|
||||
const stats = statSync(certPath);
|
||||
console.log(`\n📜 Dateiberechtigungen für ${certPath}:`);
|
||||
console.log(` Owner UID: ${stats.uid}`);
|
||||
console.log(` Group GID: ${stats.gid}`);
|
||||
console.log(` Mode: ${stats.mode.toString(8)}`);
|
||||
|
||||
// Prüfe, ob Datei lesbar ist
|
||||
try {
|
||||
readFileSync(certPath);
|
||||
console.log('✅ Datei ist lesbar');
|
||||
} catch (err) {
|
||||
console.error('❌ Datei ist NICHT lesbar:', err.message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`❌ Konnte Datei nicht prüfen: ${err.message}`);
|
||||
}
|
||||
|
||||
// Prüfe Gruppen-Berechtigungen
|
||||
try {
|
||||
const groupInfo = execSync('getent group ssl-cert 2>/dev/null', { encoding: 'utf-8' }).trim();
|
||||
console.log(`\n👥 ssl-cert-Gruppe: ${groupInfo}`);
|
||||
} catch (err) {
|
||||
console.error('❌ ssl-cert-Gruppe existiert nicht!');
|
||||
}
|
||||
|
||||
81
backend/scripts/checkServerStatus.js
Normal file
81
backend/scripts/checkServerStatus.js
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Prüft den Status des Backend-Servers
|
||||
*/
|
||||
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
console.log('🔍 Prüfe Backend-Server-Status...\n');
|
||||
|
||||
// Prüfe, ob Port 3005 (HTTP) läuft
|
||||
try {
|
||||
const { stdout } = await execAsync(`netstat -tlnp 2>/dev/null | grep :3005 || ss -tlnp 2>/dev/null | grep :3005 || echo "Port 3005 nicht gefunden"`);
|
||||
if (stdout.includes(':3005')) {
|
||||
console.log('✅ HTTP-Server (Port 3005) läuft');
|
||||
console.log(` ${stdout.trim()}`);
|
||||
} else {
|
||||
console.log('❌ HTTP-Server (Port 3005) läuft nicht');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('⚠️ Konnte Port 3005 nicht prüfen:', err.message);
|
||||
}
|
||||
|
||||
// Prüfe, ob Port 3051 (HTTPS) läuft
|
||||
try {
|
||||
const { stdout } = await execAsync(`netstat -tlnp 2>/dev/null | grep :3051 || ss -tlnp 2>/dev/null | grep :3051 || echo "Port 3051 nicht gefunden"`);
|
||||
if (stdout.includes(':3051')) {
|
||||
console.log('\n✅ HTTPS-Server (Port 3051) läuft');
|
||||
console.log(` ${stdout.trim()}`);
|
||||
} else {
|
||||
console.log('\n❌ HTTPS-Server (Port 3051) läuft NICHT');
|
||||
console.log(' → Prüfe Backend-Logs auf Fehler');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('⚠️ Konnte Port 3051 nicht prüfen:', err.message);
|
||||
}
|
||||
|
||||
// Prüfe SSL-Zertifikate
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
|
||||
const keyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
|
||||
const certPath = '/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem';
|
||||
|
||||
console.log('\n📜 Prüfe SSL-Zertifikate...');
|
||||
if (existsSync(keyPath) && existsSync(certPath)) {
|
||||
console.log('✅ SSL-Zertifikate gefunden');
|
||||
try {
|
||||
readFileSync(keyPath);
|
||||
readFileSync(certPath);
|
||||
console.log('✅ SSL-Zertifikate sind lesbar');
|
||||
} catch (err) {
|
||||
console.error('❌ SSL-Zertifikate können nicht gelesen werden:', err.message);
|
||||
console.error(' → Prüfe Dateiberechtigungen');
|
||||
}
|
||||
} else {
|
||||
console.error('❌ SSL-Zertifikate nicht gefunden');
|
||||
console.error(` Erwartete Pfade:`);
|
||||
console.error(` - ${keyPath}`);
|
||||
console.error(` - ${certPath}`);
|
||||
}
|
||||
|
||||
// Prüfe systemd-Service-Status
|
||||
try {
|
||||
const { stdout } = await execAsync('systemctl is-active tt-tagebuch 2>/dev/null || echo "inactive"');
|
||||
if (stdout.trim() === 'active') {
|
||||
console.log('\n✅ systemd-Service "tt-tagebuch" ist aktiv');
|
||||
} else {
|
||||
console.log('\n⚠️ systemd-Service "tt-tagebuch" ist nicht aktiv');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('\n⚠️ Konnte systemd-Service-Status nicht prüfen');
|
||||
}
|
||||
|
||||
console.log('\n📋 Nächste Schritte:');
|
||||
console.log(' 1. Prüfe Backend-Logs: sudo journalctl -u tt-tagebuch -n 50');
|
||||
console.log(' 2. Prüfe, ob HTTPS-Server gestartet wurde (suche nach "HTTPS-Server für Socket.IO")');
|
||||
console.log(' 3. Prüfe auf Fehler (suche nach "HTTPS-Server konnte nicht gestartet werden")');
|
||||
|
||||
84
backend/scripts/checkSocketIOServer.js
Normal file
84
backend/scripts/checkSocketIOServer.js
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Prüft, ob der Socket.IO HTTPS-Server auf Port 3051 läuft
|
||||
*/
|
||||
|
||||
import https from 'https';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const httpsPort = process.env.HTTPS_PORT || 3051;
|
||||
|
||||
console.log(`🔍 Prüfe Socket.IO HTTPS-Server auf Port ${httpsPort}...\n`);
|
||||
|
||||
// Prüfe, ob Zertifikate existieren
|
||||
try {
|
||||
const keyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
|
||||
const certPath = '/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem';
|
||||
|
||||
readFileSync(keyPath);
|
||||
readFileSync(certPath);
|
||||
console.log('✅ SSL-Zertifikate gefunden');
|
||||
} catch (err) {
|
||||
console.error('❌ SSL-Zertifikate nicht gefunden:', err.message);
|
||||
console.error(' Erwartete Pfade:');
|
||||
console.error(' - /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem');
|
||||
console.error(' - /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Prüfe, ob Port geöffnet ist
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(`netstat -tlnp 2>/dev/null | grep :${httpsPort} || ss -tlnp 2>/dev/null | grep :${httpsPort} || echo "Port nicht gefunden"`);
|
||||
if (stdout.includes(':' + httpsPort)) {
|
||||
console.log(`✅ Port ${httpsPort} ist geöffnet und lauscht`);
|
||||
console.log(` ${stdout.trim()}`);
|
||||
} else {
|
||||
console.log(`❌ Port ${httpsPort} ist nicht geöffnet oder lauscht nicht`);
|
||||
console.log(' → Backend-Server muss neu gestartet werden');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('⚠️ Konnte Port-Status nicht prüfen:', err.message);
|
||||
}
|
||||
|
||||
// Versuche Verbindung zum Server
|
||||
console.log(`\n🔌 Versuche Verbindung zu https://localhost:${httpsPort}/socket.io/...`);
|
||||
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: httpsPort,
|
||||
path: '/socket.io/?EIO=4&transport=polling',
|
||||
method: 'GET',
|
||||
rejectUnauthorized: false
|
||||
};
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
console.log(`✅ Verbindung erfolgreich! Status: ${res.statusCode}`);
|
||||
res.on('data', (chunk) => {
|
||||
console.log(` Response: ${chunk.toString().substring(0, 100)}...`);
|
||||
});
|
||||
res.on('end', () => {
|
||||
console.log('\n✅ Socket.IO HTTPS-Server läuft korrekt!');
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
console.error(`❌ Verbindung fehlgeschlagen: ${err.message}`);
|
||||
console.error(' → Backend-Server läuft möglicherweise nicht');
|
||||
console.error(' → Oder HTTPS-Server wurde nicht gestartet');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.setTimeout(5000, () => {
|
||||
console.error('❌ Timeout beim Verbindungsversuch');
|
||||
req.destroy();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
req.end();
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import mysql from 'mysql2/promise';
|
||||
import dotenv from 'dotenv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
import { development } from '../config.js';
|
||||
|
||||
dotenv.config();
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load .env from backend directory
|
||||
dotenv.config({ path: join(__dirname, '..', '.env') });
|
||||
|
||||
const report = [];
|
||||
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'trainingdiary',
|
||||
host: process.env.DB_HOST || development.host || 'localhost',
|
||||
user: process.env.DB_USER || development.username || 'root',
|
||||
password: process.env.DB_PASSWORD || development.password || '',
|
||||
database: process.env.DB_NAME || development.database || 'trainingdiary',
|
||||
};
|
||||
|
||||
async function getTables(connection) {
|
||||
|
||||
97
backend/scripts/fixCertPermissions.sh
Executable file
97
backend/scripts/fixCertPermissions.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Skript zum Fixen der SSL-Zertifikat-Berechtigungen für Node.js
|
||||
|
||||
CERT_DIR="/etc/letsencrypt/live/tt-tagebuch.de"
|
||||
CERT_GROUP="ssl-cert" # Standard-Gruppe für SSL-Zertifikate
|
||||
|
||||
# Prüfe, ob Zertifikate existieren (mit sudo, da normaler Benutzer keinen Zugriff hat)
|
||||
if ! sudo test -d "$CERT_DIR"; then
|
||||
echo "❌ Zertifikat-Verzeichnis nicht gefunden: $CERT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prüfe, ob ssl-cert-Gruppe existiert
|
||||
if ! sudo getent group "$CERT_GROUP" > /dev/null 2>&1; then
|
||||
echo "⚠️ Gruppe '$CERT_GROUP' existiert nicht. Erstelle sie..."
|
||||
sudo groupadd "$CERT_GROUP"
|
||||
fi
|
||||
|
||||
# Prüfe, welcher Benutzer den systemd-Service ausführt
|
||||
SERVICE_USER=$(sudo systemctl show -p User tt-tagebuch.service 2>/dev/null | cut -d= -f2)
|
||||
|
||||
# Wenn kein User definiert ist oder "nobody", verwende www-data
|
||||
if [ -z "$SERVICE_USER" ] || [ "$SERVICE_USER" = "nobody" ]; then
|
||||
echo "⚠️ Service-Benutzer ist '$SERVICE_USER' oder nicht definiert."
|
||||
echo " Verwende 'www-data' als Standard (empfohlen für Webserver-Services)."
|
||||
SERVICE_USER="www-data"
|
||||
|
||||
# Prüfe, ob www-data existiert
|
||||
if ! id "$SERVICE_USER" &>/dev/null; then
|
||||
echo "❌ Benutzer '$SERVICE_USER' existiert nicht!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "🔧 Konfiguriere SSL-Zertifikat-Berechtigungen..."
|
||||
echo " Service-Benutzer: $SERVICE_USER"
|
||||
echo " Zertifikat-Verzeichnis: $CERT_DIR"
|
||||
|
||||
# Füge Service-Benutzer zur ssl-cert-Gruppe hinzu
|
||||
sudo usermod -a -G "$CERT_GROUP" "$SERVICE_USER"
|
||||
|
||||
# Setze Gruppen-Berechtigungen für Zertifikate
|
||||
echo "📜 Setze Berechtigungen für Zertifikate..."
|
||||
|
||||
# Setze Gruppe für das Verzeichnis
|
||||
sudo chgrp -R "$CERT_GROUP" "$CERT_DIR"
|
||||
|
||||
# Setze Berechtigungen: Owner (root) kann lesen/schreiben, Gruppe kann lesen
|
||||
# WICHTIG: Verwende find, um auch die Symlinks zu behandeln
|
||||
sudo find "$CERT_DIR" -name "privkey.pem" -exec chmod 640 {} \;
|
||||
sudo find "$CERT_DIR" -name "fullchain.pem" -exec chmod 644 {} \;
|
||||
sudo find "$CERT_DIR" -name "cert.pem" -exec chmod 644 {} \;
|
||||
sudo find "$CERT_DIR" -name "chain.pem" -exec chmod 644 {} \;
|
||||
|
||||
# Setze auch für das archive-Verzeichnis (wo die Symlinks hinzeigen)
|
||||
ARCHIVE_DIR="/etc/letsencrypt/archive/tt-tagebuch.de"
|
||||
if sudo test -d "$ARCHIVE_DIR"; then
|
||||
echo "📜 Setze Berechtigungen für archive-Verzeichnis..."
|
||||
|
||||
# WICHTIG: Setze Verzeichnis-Berechtigungen, damit Gruppe in Verzeichnis navigieren kann
|
||||
# Setze execute-Bit für Gruppe auf allen Verzeichnissen im Pfad
|
||||
sudo chgrp -R "$CERT_GROUP" "/etc/letsencrypt/archive"
|
||||
sudo chmod 750 "/etc/letsencrypt/archive"
|
||||
sudo chmod 750 "/etc/letsencrypt/archive/tt-tagebuch.de"
|
||||
|
||||
# Setze Gruppe für alle Dateien
|
||||
sudo chgrp -R "$CERT_GROUP" "$ARCHIVE_DIR"
|
||||
|
||||
# Setze Berechtigungen für alle privkey-Dateien (privkey.pem, privkey1.pem, privkey8.pem, etc.)
|
||||
sudo find "$ARCHIVE_DIR" -type f -name "privkey*.pem" -exec chmod 640 {} \;
|
||||
sudo find "$ARCHIVE_DIR" -type f -name "fullchain*.pem" -exec chmod 644 {} \;
|
||||
sudo find "$ARCHIVE_DIR" -type f -name "cert*.pem" -exec chmod 644 {} \;
|
||||
sudo find "$ARCHIVE_DIR" -type f -name "chain*.pem" -exec chmod 644 {} \;
|
||||
|
||||
# Zeige, welche Dateien gefunden wurden
|
||||
echo " Gefundene privkey-Dateien:"
|
||||
sudo find "$ARCHIVE_DIR" -type f -name "privkey*.pem" -exec ls -la {} \;
|
||||
fi
|
||||
|
||||
# WICHTIG: Setze auch Verzeichnis-Berechtigungen für live-Verzeichnis
|
||||
LIVE_DIR="/etc/letsencrypt/live"
|
||||
if sudo test -d "$LIVE_DIR"; then
|
||||
echo "📜 Setze Verzeichnis-Berechtigungen für live-Verzeichnis..."
|
||||
sudo chgrp -R "$CERT_GROUP" "$LIVE_DIR"
|
||||
sudo chmod 750 "$LIVE_DIR"
|
||||
sudo chmod 750 "$LIVE_DIR/tt-tagebuch.de"
|
||||
fi
|
||||
|
||||
echo "✅ Berechtigungen gesetzt!"
|
||||
echo ""
|
||||
echo "📋 Prüfe Berechtigungen:"
|
||||
sudo ls -la "$CERT_DIR/privkey.pem"
|
||||
sudo ls -la "$CERT_DIR/fullchain.pem"
|
||||
echo ""
|
||||
echo "⚠️ WICHTIG: Der Service muss neu gestartet werden, damit die Gruppenänderung wirksam wird:"
|
||||
echo " sudo systemctl restart tt-tagebuch"
|
||||
171
backend/scripts/migrateMyTischtennisEncryption.js
Normal file
171
backend/scripts/migrateMyTischtennisEncryption.js
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
|
||||
|
||||
|
||||
50
backend/scripts/testCertAccess.sh
Normal file
50
backend/scripts/testCertAccess.sh
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Testet, ob www-data Zugriff auf die SSL-Zertifikate hat
|
||||
|
||||
CERT_PATH="/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem"
|
||||
|
||||
echo "🔍 Teste Zugriff auf SSL-Zertifikate als www-data...\n"
|
||||
|
||||
# Prüfe, ob die Datei existiert
|
||||
if ! sudo test -f "$CERT_PATH"; then
|
||||
echo "❌ Datei nicht gefunden: $CERT_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prüfe Berechtigungen
|
||||
echo "📋 Dateiberechtigungen:"
|
||||
sudo ls -la "$CERT_PATH"
|
||||
|
||||
# Prüfe, auf welche Datei der Symlink zeigt
|
||||
REAL_PATH=$(sudo readlink -f "$CERT_PATH")
|
||||
echo "\n📋 Symlink zeigt auf: $REAL_PATH"
|
||||
echo "📋 Berechtigungen der echten Datei:"
|
||||
sudo ls -la "$REAL_PATH"
|
||||
|
||||
# Teste, ob www-data die Datei lesen kann
|
||||
echo "\n🔍 Teste Lesezugriff als www-data..."
|
||||
if sudo -u www-data test -r "$CERT_PATH"; then
|
||||
echo "✅ www-data kann die Datei lesen!"
|
||||
else
|
||||
echo "❌ www-data kann die Datei NICHT lesen!"
|
||||
echo "\n📋 Prüfe Gruppen-Berechtigungen:"
|
||||
sudo stat -c "%A %U:%G" "$REAL_PATH"
|
||||
echo "\n📋 Prüfe, ob www-data in ssl-cert-Gruppe ist:"
|
||||
sudo groups www-data
|
||||
fi
|
||||
|
||||
# Versuche, die Datei als www-data zu lesen
|
||||
echo "\n🔍 Versuche, Datei als www-data zu öffnen..."
|
||||
if sudo -u www-data cat "$CERT_PATH" > /dev/null 2>&1; then
|
||||
echo "✅ www-data kann die Datei erfolgreich öffnen!"
|
||||
else
|
||||
echo "❌ www-data kann die Datei NICHT öffnen!"
|
||||
echo "\n💡 Mögliche Lösungen:"
|
||||
echo " 1. Prüfe, ob die Datei im archive-Verzeichnis die richtige Gruppe hat:"
|
||||
echo " sudo ls -la $REAL_PATH"
|
||||
echo " 2. Setze Gruppe und Berechtigungen erneut:"
|
||||
echo " sudo chgrp ssl-cert $REAL_PATH"
|
||||
echo " sudo chmod 640 $REAL_PATH"
|
||||
fi
|
||||
|
||||
155
backend/scripts/testWebSocket.js
Normal file
155
backend/scripts/testWebSocket.js
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test-Script zum Prüfen, ob der WebSocket-Server lokal erreichbar ist
|
||||
*
|
||||
* Verwendung:
|
||||
* node scripts/testWebSocket.js
|
||||
* node scripts/testWebSocket.js localhost 3050
|
||||
*/
|
||||
|
||||
import http from 'http';
|
||||
|
||||
const host = process.argv[2] || 'localhost';
|
||||
const port = process.argv[3] || '3050';
|
||||
const url = `http://${host}:${port}`;
|
||||
|
||||
console.log(`🔌 Teste Socket.IO-Server auf ${url}...\n`);
|
||||
|
||||
// Test 1: HTTP-Polling (Socket.IO Handshake)
|
||||
console.log('1️⃣ Teste HTTP-Polling (Socket.IO Handshake)...');
|
||||
const pollingUrl = `${url}/socket.io/?EIO=4&transport=polling`;
|
||||
|
||||
const pollingReq = http.get(pollingUrl, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log(' ✅ HTTP-Polling erfolgreich!');
|
||||
console.log(` Status: ${res.statusCode}`);
|
||||
console.log(` Response: ${data.substring(0, 200)}...`);
|
||||
|
||||
// Versuche Session-ID zu extrahieren
|
||||
let sessionId = null;
|
||||
try {
|
||||
const jsonMatch = data.match(/\{.*\}/);
|
||||
if (jsonMatch) {
|
||||
const json = JSON.parse(jsonMatch[0]);
|
||||
if (json.sid) {
|
||||
sessionId = json.sid;
|
||||
console.log(` Session ID: ${sessionId}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignoriere JSON-Parse-Fehler
|
||||
}
|
||||
|
||||
// Test 2: WebSocket-Upgrade
|
||||
console.log('\n2️⃣ Teste WebSocket-Upgrade...');
|
||||
testWebSocketUpgrade(host, port, sessionId);
|
||||
} else {
|
||||
console.error(` ❌ HTTP-Polling fehlgeschlagen: Status ${res.statusCode}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
pollingReq.on('error', (error) => {
|
||||
console.error(' ❌ HTTP-Polling Fehler:');
|
||||
console.error(` ${error.message}`);
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
console.error(' → Server läuft möglicherweise nicht oder ist nicht erreichbar');
|
||||
console.error(` → Prüfe: netstat -tlnp | grep ${port}`);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
pollingReq.setTimeout(5000, () => {
|
||||
pollingReq.destroy();
|
||||
console.error(' ❌ Timeout: Keine Antwort innerhalb von 5 Sekunden');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function testWebSocketUpgrade(host, port, sessionId) {
|
||||
// WebSocket-Upgrade-Request
|
||||
// Sec-WebSocket-Key muss 16 Bytes (128 Bits) sein, base64-encoded
|
||||
const wsKey = Buffer.allocUnsafe(16);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
wsKey[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
const wsKeyBase64 = wsKey.toString('base64');
|
||||
const path = sessionId
|
||||
? `/socket.io/?EIO=4&transport=websocket&sid=${sessionId}`
|
||||
: '/socket.io/?EIO=4&transport=websocket';
|
||||
|
||||
const options = {
|
||||
hostname: host,
|
||||
port: port,
|
||||
path: path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'Upgrade',
|
||||
'Sec-WebSocket-Key': wsKeyBase64,
|
||||
'Sec-WebSocket-Version': '13',
|
||||
'Sec-WebSocket-Protocol': 'chat, superchat'
|
||||
}
|
||||
};
|
||||
|
||||
console.log(` Request-Path: ${path}`);
|
||||
console.log(` Sec-WebSocket-Key: ${wsKeyBase64.substring(0, 20)}...`);
|
||||
|
||||
const wsReq = http.request(options, (res) => {
|
||||
console.log(` Response Status: ${res.statusCode}`);
|
||||
console.log(` Response Headers:`, JSON.stringify(res.headers, null, 2));
|
||||
|
||||
if (res.statusCode === 101) {
|
||||
console.log(' ✅ WebSocket-Upgrade erfolgreich!');
|
||||
console.log(` Status: ${res.statusCode} (Switching Protocols)`);
|
||||
console.log(` Upgrade Header: ${res.headers.upgrade}`);
|
||||
console.log(` Connection Header: ${res.headers.connection}`);
|
||||
console.log('\n✅ Socket.IO-Server ist erreichbar und unterstützt WebSockets!');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(` ⚠️ WebSocket-Upgrade: Status ${res.statusCode} (erwartet: 101)`);
|
||||
console.log(` → Server antwortet, aber Upgrade nicht erfolgreich`);
|
||||
|
||||
// Lese Response-Body für weitere Informationen
|
||||
let body = '';
|
||||
res.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
if (body) {
|
||||
console.log(` Response Body: ${body.substring(0, 200)}`);
|
||||
}
|
||||
if (res.statusCode === 400) {
|
||||
console.log(` → Socket.IO lehnt den Request ab (Bad Request)`);
|
||||
console.log(` → Möglicherweise fehlt die Session-ID oder sie ist ungültig`);
|
||||
}
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wsReq.on('error', (error) => {
|
||||
console.error(' ❌ WebSocket-Upgrade Fehler:');
|
||||
console.error(` ${error.message}`);
|
||||
console.error(` Error Code: ${error.code}`);
|
||||
console.log('\n⚠️ HTTP-Polling funktioniert, aber WebSocket-Upgrade schlägt fehl.');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
wsReq.setTimeout(5000, () => {
|
||||
wsReq.destroy();
|
||||
console.error(' ❌ Timeout: Keine Antwort innerhalb von 5 Sekunden');
|
||||
console.error(' → Möglicherweise hängt der Server oder die Verbindung wird nicht akzeptiert');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
wsReq.end();
|
||||
}
|
||||
|
||||
163
backend/scripts/testWebSocketApache.js
Executable file
163
backend/scripts/testWebSocketApache.js
Executable file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Test-Script zum Prüfen, ob WebSocket über Apache erreichbar ist
|
||||
*
|
||||
* Verwendung:
|
||||
* node scripts/testWebSocketApache.js
|
||||
* node scripts/testWebSocketApache.js https://tt-tagebuch.de
|
||||
*/
|
||||
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
|
||||
const url = process.argv[2] || 'https://tt-tagebuch.de';
|
||||
const isHttps = url.startsWith('https://');
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
console.log(`🔌 Teste Socket.IO-Server über ${isHttps ? 'Apache (HTTPS)' : 'HTTP'} auf ${url}...\n`);
|
||||
|
||||
// Test 1: HTTP-Polling (Socket.IO Handshake)
|
||||
console.log('1️⃣ Teste HTTP-Polling (Socket.IO Handshake)...');
|
||||
const pollingUrl = `${url}/socket.io/?EIO=4&transport=polling`;
|
||||
|
||||
const pollingReq = client.get(pollingUrl, {
|
||||
rejectUnauthorized: false // Für selbst-signierte Zertifikate
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log(' ✅ HTTP-Polling erfolgreich!');
|
||||
console.log(` Status: ${res.statusCode}`);
|
||||
console.log(` Response: ${data.substring(0, 200)}...`);
|
||||
|
||||
// Versuche Session-ID zu extrahieren
|
||||
try {
|
||||
const jsonMatch = data.match(/\{.*\}/);
|
||||
if (jsonMatch) {
|
||||
const json = JSON.parse(jsonMatch[0]);
|
||||
if (json.sid) {
|
||||
console.log(` Session ID: ${json.sid}`);
|
||||
|
||||
// Test 2: WebSocket-Upgrade
|
||||
console.log('\n2️⃣ Teste WebSocket-Upgrade...');
|
||||
testWebSocketUpgrade(url, json.sid, isHttps);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(' ⚠️ Konnte Session-ID nicht extrahieren');
|
||||
console.log('\n2️⃣ Teste WebSocket-Upgrade ohne Session-ID...');
|
||||
testWebSocketUpgrade(url, null, isHttps);
|
||||
}
|
||||
} else {
|
||||
console.error(` ❌ HTTP-Polling fehlgeschlagen: Status ${res.statusCode}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
pollingReq.on('error', (error) => {
|
||||
console.error(' ❌ HTTP-Polling Fehler:');
|
||||
console.error(` ${error.message}`);
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
console.error(' → Server läuft möglicherweise nicht oder ist nicht erreichbar');
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
pollingReq.setTimeout(10000, () => {
|
||||
pollingReq.destroy();
|
||||
console.error(' ❌ Timeout: Keine Antwort innerhalb von 10 Sekunden');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function testWebSocketUpgrade(baseUrl, sessionId, useHttps) {
|
||||
// WebSocket-Upgrade-Request
|
||||
// Sec-WebSocket-Key muss 16 Bytes (128 Bits) sein, base64-encoded
|
||||
const wsKey = Buffer.allocUnsafe(16);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
wsKey[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
const wsKeyBase64 = wsKey.toString('base64');
|
||||
const path = sessionId
|
||||
? `/socket.io/?EIO=4&transport=websocket&sid=${sessionId}`
|
||||
: `/socket.io/?EIO=4&transport=websocket`;
|
||||
|
||||
const urlObj = new URL(baseUrl);
|
||||
const options = {
|
||||
hostname: urlObj.hostname,
|
||||
port: urlObj.port || (useHttps ? 443 : 80),
|
||||
path: path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'Upgrade',
|
||||
'Sec-WebSocket-Key': wsKeyBase64,
|
||||
'Sec-WebSocket-Version': '13',
|
||||
'Sec-WebSocket-Protocol': 'chat, superchat',
|
||||
'Origin': baseUrl
|
||||
},
|
||||
rejectUnauthorized: false // Für selbst-signierte Zertifikate
|
||||
};
|
||||
|
||||
const client = useHttps ? https : http;
|
||||
const wsReq = client.request(options, (res) => {
|
||||
console.log(` Status: ${res.statusCode}`);
|
||||
console.log(` Upgrade Header: ${res.headers.upgrade || 'nicht gesetzt'}`);
|
||||
console.log(` Connection Header: ${res.headers.connection || 'nicht gesetzt'}`);
|
||||
|
||||
if (res.statusCode === 101) {
|
||||
console.log(' ✅ WebSocket-Upgrade erfolgreich!');
|
||||
console.log(` Status: ${res.statusCode} (Switching Protocols)`);
|
||||
console.log('\n✅ Socket.IO-Server ist über Apache erreichbar und unterstützt WebSockets!');
|
||||
process.exit(0);
|
||||
} else if (res.statusCode === 400) {
|
||||
console.log(` ⚠️ WebSocket-Upgrade: Status ${res.statusCode} (Bad Request)`);
|
||||
console.log(` → Apache leitet weiter, aber Backend akzeptiert den Request nicht`);
|
||||
console.log(` → Möglicherweise fehlen Header oder die Session-ID ist ungültig`);
|
||||
} else {
|
||||
console.log(` ⚠️ WebSocket-Upgrade: Status ${res.statusCode} (erwartet: 101)`);
|
||||
console.log(` → Server antwortet, aber Upgrade nicht erfolgreich`);
|
||||
}
|
||||
|
||||
// Lese Response-Body für weitere Informationen
|
||||
let body = '';
|
||||
res.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
if (body) {
|
||||
console.log(` Response Body: ${body.substring(0, 200)}`);
|
||||
}
|
||||
console.log('\n⚠️ HTTP-Polling funktioniert, aber WebSocket-Upgrade schlägt fehl.');
|
||||
console.log(' → Das könnte ein Problem mit der Apache-Konfiguration sein.');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
wsReq.on('error', (error) => {
|
||||
console.error(' ❌ WebSocket-Upgrade Fehler:');
|
||||
console.error(` ${error.message}`);
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
console.error(' → Verbindung wurde abgelehnt');
|
||||
} else if (error.code === 'ENOTFOUND') {
|
||||
console.error(' → Hostname nicht gefunden');
|
||||
}
|
||||
console.log('\n⚠️ HTTP-Polling funktioniert, aber WebSocket-Upgrade schlägt fehl.');
|
||||
console.log(' → Das könnte ein Problem mit der Apache-Konfiguration sein.');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
wsReq.setTimeout(10000, () => {
|
||||
wsReq.destroy();
|
||||
console.error(' ❌ Timeout: Keine Antwort innerhalb von 10 Sekunden');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
wsReq.end();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { createServer } from 'http';
|
||||
import https from 'https';
|
||||
import fs from 'fs';
|
||||
import { exec } from 'child_process';
|
||||
import sequelize from './database.js';
|
||||
import cors from 'cors';
|
||||
import { initializeSocketIO } from './services/socketService.js';
|
||||
import {
|
||||
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
|
||||
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
|
||||
@@ -44,8 +49,11 @@ import memberActivityRoutes from './routes/memberActivityRoutes.js';
|
||||
import permissionRoutes from './routes/permissionRoutes.js';
|
||||
import apiLogRoutes from './routes/apiLogRoutes.js';
|
||||
import memberTransferConfigRoutes from './routes/memberTransferConfigRoutes.js';
|
||||
import trainingGroupRoutes from './routes/trainingGroupRoutes.js';
|
||||
import trainingTimeRoutes from './routes/trainingTimeRoutes.js';
|
||||
import schedulerService from './services/schedulerService.js';
|
||||
import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.js';
|
||||
import HttpError from './exceptions/HttpError.js';
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3005;
|
||||
@@ -53,12 +61,14 @@ const port = process.env.PORT || 3005;
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// CORS-Konfiguration - Socket.IO hat seine eigene CORS-Konfiguration
|
||||
app.use(cors({
|
||||
origin: true,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid']
|
||||
}));
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// Request Logging Middleware - loggt alle API-Requests
|
||||
@@ -73,31 +83,6 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[unhandledRejection]', reason);
|
||||
});
|
||||
|
||||
// Globale Fehlerbehandlung für API-Routen
|
||||
app.use((err, req, res, next) => {
|
||||
if (res.headersSent) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const status = err?.statusCode || err?.status || 500;
|
||||
const message = err?.message || 'Interner Serverfehler';
|
||||
|
||||
const response = {
|
||||
success: false,
|
||||
message,
|
||||
error: message
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') {
|
||||
response.debug = {
|
||||
stack: err?.stack || null
|
||||
};
|
||||
}
|
||||
|
||||
console.error('[ExpressError]', err);
|
||||
res.status(status).json(response);
|
||||
});
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/clubs', clubRoutes);
|
||||
app.use('/api/clubmembers', memberRoutes);
|
||||
@@ -130,12 +115,93 @@ app.use('/api/member-activities', memberActivityRoutes);
|
||||
app.use('/api/permissions', permissionRoutes);
|
||||
app.use('/api/logs', apiLogRoutes);
|
||||
app.use('/api/member-transfer-config', memberTransferConfigRoutes);
|
||||
app.use('/api/training-groups', trainingGroupRoutes);
|
||||
app.use('/api/training-times', trainingTimeRoutes);
|
||||
|
||||
// Middleware für dynamischen kanonischen Tag (vor express.static)
|
||||
const setCanonicalTag = (req, res, next) => {
|
||||
// Socket.IO-Requests komplett ignorieren
|
||||
if (req.path.startsWith('/socket.io/')) {
|
||||
return 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();
|
||||
}
|
||||
|
||||
// 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')));
|
||||
|
||||
// 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'));
|
||||
// Globale Fehlerbehandlung für API-Routen (MUSS nach allen Routes sein!)
|
||||
app.use((err, req, res, next) => {
|
||||
if (res.headersSent) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const status = err?.statusCode || err?.status || 500;
|
||||
|
||||
// Unterstützung für Fehlercodes
|
||||
let errorResponse;
|
||||
if (err instanceof HttpError && err.errorCode) {
|
||||
// Neues Format mit Fehlercode
|
||||
errorResponse = err.toJSON();
|
||||
} else {
|
||||
// Legacy-Format: String-Nachricht
|
||||
const message = err?.message || 'Interner Serverfehler';
|
||||
errorResponse = {
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
const response = {
|
||||
success: false,
|
||||
...errorResponse,
|
||||
// Für Rückwärtskompatibilität: error-Feld mit Nachricht
|
||||
error: errorResponse.message || errorResponse.code || 'Interner Serverfehler'
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') {
|
||||
response.debug = {
|
||||
stack: err?.stack || null
|
||||
};
|
||||
}
|
||||
|
||||
console.error('[ExpressError]', err);
|
||||
res.status(status).json(response);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
@@ -256,7 +322,99 @@ app.get('*', (req, res) => {
|
||||
// Start scheduler service
|
||||
schedulerService.start();
|
||||
|
||||
app.listen(port);
|
||||
// Erstelle HTTP-Server für API
|
||||
const httpServer = createServer(app);
|
||||
|
||||
// WICHTIG: Socket.IO muss VOR dem Server-Start initialisiert werden
|
||||
// damit es Upgrade-Requests abfangen kann
|
||||
let socketIOInitialized = false;
|
||||
|
||||
// Erstelle HTTPS-Server für Socket.IO (direkt mit SSL)
|
||||
const httpsPort = process.env.HTTPS_PORT || 3051;
|
||||
|
||||
// Prüfe, ob SSL-Zertifikate vorhanden sind
|
||||
const sslKeyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
|
||||
const sslCertPath = '/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem';
|
||||
|
||||
if (fs.existsSync(sslKeyPath) && fs.existsSync(sslCertPath)) {
|
||||
try {
|
||||
console.log('📜 Lade SSL-Zertifikate...');
|
||||
const httpsOptions = {
|
||||
key: fs.readFileSync(sslKeyPath),
|
||||
cert: fs.readFileSync(sslCertPath)
|
||||
};
|
||||
console.log('✅ SSL-Zertifikate erfolgreich geladen');
|
||||
|
||||
// Erstelle HTTPS-Server mit Express-App
|
||||
const httpsServer = https.createServer(httpsOptions, app);
|
||||
|
||||
// Initialisiere Socket.IO auf HTTPS-Server VOR dem Listen
|
||||
initializeSocketIO(httpsServer);
|
||||
socketIOInitialized = true;
|
||||
|
||||
// Prüfe, ob Port bereits belegt ist
|
||||
httpsServer.on('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`❌ Port ${httpsPort} ist bereits belegt!`);
|
||||
console.error(' → Möglicherweise läuft bereits ein anderer Server auf diesem Port');
|
||||
console.error(' → Prüfe mit: lsof -i :3051 oder netstat -tlnp | grep 3051');
|
||||
socketIOInitialized = false;
|
||||
} else {
|
||||
console.error('❌ HTTPS-Server Error:', err.message);
|
||||
console.error(' Code:', err.code);
|
||||
socketIOInitialized = false;
|
||||
}
|
||||
});
|
||||
|
||||
httpsServer.on('clientError', (err, socket) => {
|
||||
if (socket && !socket.destroyed) {
|
||||
console.error('❌ HTTPS-Server Client Error:', err.message);
|
||||
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
||||
}
|
||||
});
|
||||
|
||||
// Starte HTTPS-Server
|
||||
httpsServer.listen(httpsPort, '0.0.0.0', () => {
|
||||
console.log(`🚀 HTTPS-Server für Socket.IO läuft auf Port ${httpsPort}`);
|
||||
console.log(` Socket.IO Endpoint: https://tt-tagebuch.de:${httpsPort}/socket.io/`);
|
||||
console.log(` Prüfe mit: lsof -i :${httpsPort} oder netstat -tlnp | grep ${httpsPort}`);
|
||||
});
|
||||
|
||||
// Prüfe nach kurzer Verzögerung, ob Server wirklich läuft
|
||||
setTimeout(() => {
|
||||
if (socketIOInitialized) {
|
||||
exec(`lsof -i :${httpsPort} || netstat -tlnp 2>/dev/null | grep :${httpsPort} || echo "Port nicht gefunden"`, (error, stdout) => {
|
||||
if (stdout && !stdout.includes('Port nicht gefunden')) {
|
||||
console.log(`✅ Port ${httpsPort} ist aktiv und erreichbar`);
|
||||
} else {
|
||||
console.warn(`⚠️ Port ${httpsPort} scheint nicht aktiv zu sein - prüfe Server-Logs`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 2000);
|
||||
} catch (err) {
|
||||
console.error('⚠️ HTTPS-Server konnte nicht gestartet werden:', err.message);
|
||||
console.error(' Stack:', err.stack);
|
||||
console.log(' → Socket.IO läuft auf HTTP-Server (nur für Entwicklung)');
|
||||
socketIOInitialized = false;
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ SSL-Zertifikate nicht gefunden - Socket.IO läuft auf HTTP-Server (nur für Entwicklung)');
|
||||
console.log(` Erwartete Pfade: ${sslKeyPath}, ${sslCertPath}`);
|
||||
console.log(` Prüfe mit: ls -la ${sslKeyPath} ${sslCertPath}`);
|
||||
}
|
||||
|
||||
// Fallback: Socket.IO auf HTTP-Server (wenn noch nicht initialisiert)
|
||||
// WICHTIG: VOR dem httpServer.listen() initialisieren
|
||||
if (!socketIOInitialized) {
|
||||
initializeSocketIO(httpServer);
|
||||
console.log(' ✅ Socket.IO erfolgreich auf HTTP-Server initialisiert');
|
||||
}
|
||||
|
||||
// HTTP-Server starten NACH Socket.IO-Initialisierung
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`🚀 HTTP-Server läuft auf Port ${port}`);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Unable to synchronize the database:', err);
|
||||
}
|
||||
|
||||
@@ -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,53 @@ 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;
|
||||
};
|
||||
// Wenn kein Statuscode übergeben wurde, behandeln wir den Logeintrag als Fehler,
|
||||
// damit Request-/Response-Bodies für Debugging/Testzwecke gespeichert werden.
|
||||
// (Historisch haben Tests/Callsites logRequest ohne statusCode genutzt.)
|
||||
const isError = statusCode === null || statusCode === undefined ? true : 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);
|
||||
// Für Diagnosezwecke etwas großzügiger als 2000 Zeichen, aber weiterhin begrenzt.
|
||||
sanitizedRequestBody = truncateString(requestBodyStr, 64020);
|
||||
}
|
||||
|
||||
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, 64020);
|
||||
}
|
||||
}
|
||||
// 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 +188,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
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
@@ -78,6 +78,9 @@ class AutoFetchMatchResultsService {
|
||||
|
||||
const loginResult = await myTischtennisClient.login(account.email, password);
|
||||
if (!loginResult.success) {
|
||||
if (loginResult.requiresCaptcha) {
|
||||
throw new Error(`Re-login failed: CAPTCHA erforderlich. Bitte loggen Sie sich einmal direkt auf mytischtennis.de ein, um das CAPTCHA zu lösen.`);
|
||||
}
|
||||
throw new Error(`Re-login failed: ${loginResult.error}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,9 @@ class AutoUpdateRatingsService {
|
||||
|
||||
const loginResult = await myTischtennisClient.login(account.email, password);
|
||||
if (!loginResult.success) {
|
||||
if (loginResult.requiresCaptcha) {
|
||||
throw new Error(`Re-login failed: CAPTCHA erforderlich. Bitte loggen Sie sich einmal direkt auf mytischtennis.de ein, um das CAPTCHA zu lösen.`);
|
||||
}
|
||||
throw new Error(`Re-login failed: ${loginResult.error}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import Member from '../models/Member.js';
|
||||
import { Op, fn, where, col } from 'sequelize';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import permissionService from './permissionService.js';
|
||||
import trainingGroupService from './trainingGroupService.js';
|
||||
|
||||
class ClubService {
|
||||
async getAllClubs() {
|
||||
@@ -18,7 +19,10 @@ class ClubService {
|
||||
}
|
||||
|
||||
async createClub(clubName) {
|
||||
return await Club.create({ name: clubName });
|
||||
const club = await Club.create({ name: clubName });
|
||||
// Erstelle automatisch die Vorgaben-Gruppen
|
||||
await trainingGroupService.createPresetGroups(club.id);
|
||||
return club;
|
||||
}
|
||||
|
||||
async addUserToClub(userId, clubId, isOwner = false) {
|
||||
|
||||
@@ -166,6 +166,12 @@ class DiaryDateActivityService {
|
||||
{
|
||||
model: PredefinedActivity,
|
||||
as: 'groupPredefinedActivity',
|
||||
include: [
|
||||
{
|
||||
model: PredefinedActivityImage,
|
||||
as: 'images'
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -178,20 +184,46 @@ class DiaryDateActivityService {
|
||||
const activityData = activity.toJSON();
|
||||
|
||||
if (activityData.predefinedActivity) {
|
||||
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
|
||||
// Hole alle Images aus der Datenbank
|
||||
const allImages = await PredefinedActivityImage.findAll({
|
||||
where: { predefinedActivityId: activityData.predefinedActivity.id },
|
||||
order: [['createdAt', 'ASC']]
|
||||
});
|
||||
|
||||
// Konvertiere Images zu JSON und parse drawingData falls vorhanden
|
||||
const imagesWithParsedData = allImages.map(img => {
|
||||
const imgData = img.toJSON();
|
||||
if (imgData.drawingData) {
|
||||
try {
|
||||
imgData.drawingData = JSON.parse(imgData.drawingData);
|
||||
} catch (error) {
|
||||
console.error(`Image ${imgData.id}: Error parsing drawingData:`, error);
|
||||
}
|
||||
}
|
||||
return imgData;
|
||||
});
|
||||
|
||||
// Setze images Array
|
||||
activityData.predefinedActivity.images = imagesWithParsedData;
|
||||
|
||||
const firstImage = allImages.length > 0 ? allImages[0] : null;
|
||||
|
||||
// Füge Zeichnungsdaten hinzu, falls vorhanden
|
||||
if (firstImage && firstImage.drawingData) {
|
||||
// Priorität: 1. drawingData direkt auf PredefinedActivity, 2. drawingData aus firstImage
|
||||
if (activityData.predefinedActivity.drawingData) {
|
||||
// drawingData ist bereits vorhanden (aus dem Model)
|
||||
try {
|
||||
if (typeof activityData.predefinedActivity.drawingData === 'string') {
|
||||
activityData.predefinedActivity.drawingData = JSON.parse(activityData.predefinedActivity.drawingData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
|
||||
}
|
||||
} else if (firstImage && firstImage.drawingData) {
|
||||
try {
|
||||
activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
|
||||
} catch (error) {
|
||||
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
|
||||
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData from image:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,12 +253,48 @@ class DiaryDateActivityService {
|
||||
});
|
||||
for (const groupActivity of activityData.groupActivities) {
|
||||
if (groupActivity.groupPredefinedActivity) {
|
||||
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
|
||||
const firstImage = await PredefinedActivityImage.findOne({
|
||||
// Hole alle Images aus der Datenbank
|
||||
const allImages = await PredefinedActivityImage.findAll({
|
||||
where: { predefinedActivityId: groupActivity.groupPredefinedActivity.id },
|
||||
order: [['createdAt', 'ASC']]
|
||||
});
|
||||
|
||||
// Konvertiere Images zu JSON und parse drawingData falls vorhanden
|
||||
const imagesWithParsedData = allImages.map(img => {
|
||||
const imgData = img.toJSON();
|
||||
if (imgData.drawingData) {
|
||||
try {
|
||||
imgData.drawingData = JSON.parse(imgData.drawingData);
|
||||
} catch (error) {
|
||||
console.error(`Image ${imgData.id}: Error parsing drawingData:`, error);
|
||||
}
|
||||
}
|
||||
return imgData;
|
||||
});
|
||||
|
||||
// Setze images Array
|
||||
groupActivity.groupPredefinedActivity.images = imagesWithParsedData;
|
||||
|
||||
const firstImage = allImages.length > 0 ? allImages[0] : null;
|
||||
|
||||
// Füge Zeichnungsdaten hinzu, falls vorhanden
|
||||
// Priorität: 1. drawingData direkt auf PredefinedActivity, 2. drawingData aus firstImage
|
||||
if (groupActivity.groupPredefinedActivity.drawingData) {
|
||||
try {
|
||||
if (typeof groupActivity.groupPredefinedActivity.drawingData === 'string') {
|
||||
groupActivity.groupPredefinedActivity.drawingData = JSON.parse(groupActivity.groupPredefinedActivity.drawingData);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`GroupActivity ${groupActivity.groupPredefinedActivity.id}: Error parsing drawingData:`, error);
|
||||
}
|
||||
} else if (firstImage && firstImage.drawingData) {
|
||||
try {
|
||||
groupActivity.groupPredefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
|
||||
} catch (error) {
|
||||
console.error(`GroupActivity ${groupActivity.groupPredefinedActivity.id}: Error parsing drawingData from image:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (firstImage) {
|
||||
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
|
||||
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
|
||||
@@ -242,7 +310,7 @@ class DiaryDateActivityService {
|
||||
return activitiesWithImages;
|
||||
}
|
||||
|
||||
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, timeblockId = null) {
|
||||
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, predefinedActivityId = null, timeblockId = null) {
|
||||
await checkAccess(userToken, clubId);
|
||||
let diaryDateActivity;
|
||||
|
||||
@@ -282,11 +350,37 @@ class DiaryDateActivityService {
|
||||
console.error('Group diaryDateId:', group.diaryDateId, 'Activity diaryDateId:', diaryDateActivity.diaryDateId);
|
||||
throw new Error('Group isn\'t related to date');
|
||||
}
|
||||
const [predefinedActivity, created] = await PredefinedActivity.findOrCreate({
|
||||
where: {
|
||||
name: activity
|
||||
|
||||
let predefinedActivity = null;
|
||||
|
||||
// 1. Versuche zuerst, die PredefinedActivity per ID zu finden
|
||||
if (predefinedActivityId) {
|
||||
predefinedActivity = await PredefinedActivity.findByPk(predefinedActivityId);
|
||||
}
|
||||
|
||||
// 2. Falls nicht gefunden, suche nach Name oder Code
|
||||
if (!predefinedActivity) {
|
||||
const normalized = (activity || '').trim();
|
||||
if (normalized) {
|
||||
predefinedActivity = await PredefinedActivity.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ name: normalized },
|
||||
{ code: normalized }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Falls immer noch nicht gefunden, erstelle eine neue
|
||||
if (!predefinedActivity) {
|
||||
predefinedActivity = await PredefinedActivity.create({
|
||||
name: activity || '',
|
||||
code: activity || '',
|
||||
});
|
||||
}
|
||||
|
||||
devLog(predefinedActivity);
|
||||
const activityData = {
|
||||
diaryDateActivity: diaryDateActivity.id,
|
||||
@@ -297,6 +391,24 @@ class DiaryDateActivityService {
|
||||
return await GroupActivity.create(activityData);
|
||||
}
|
||||
|
||||
async updateGroupActivity(userToken, clubId, groupActivityId, predefinedActivityId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const groupActivity = await GroupActivity.findByPk(groupActivityId);
|
||||
if (!groupActivity) {
|
||||
throw new Error('Group activity not found');
|
||||
}
|
||||
|
||||
// Prüfe, ob die PredefinedActivity existiert
|
||||
const predefinedActivity = await PredefinedActivity.findByPk(predefinedActivityId);
|
||||
if (!predefinedActivity) {
|
||||
throw new Error('Predefined activity not found');
|
||||
}
|
||||
|
||||
// Aktualisiere die customActivity (die auf die PredefinedActivity verweist)
|
||||
groupActivity.customActivity = predefinedActivityId;
|
||||
return await groupActivity.save();
|
||||
}
|
||||
|
||||
async deleteGroupActivity(userToken, clubId, groupActivityId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const groupActivity = await GroupActivity.findByPk(groupActivityId);
|
||||
|
||||
@@ -58,6 +58,22 @@ class GroupService {
|
||||
await group.save();
|
||||
return group;
|
||||
}
|
||||
|
||||
async deleteGroup(userToken, groupId, clubId, dateId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
await this.checkDiaryDateToClub(clubId, dateId);
|
||||
const group = await Group.findOne({
|
||||
where: {
|
||||
id: groupId,
|
||||
diaryDateId: dateId
|
||||
}
|
||||
});
|
||||
if (!group) {
|
||||
throw new HttpError('Gruppe nicht gefunden oder passt nicht zum angegebenen Datum und Verein', 404);
|
||||
}
|
||||
await group.destroy();
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
export default new GroupService();
|
||||
@@ -1086,7 +1086,7 @@ class MemberService {
|
||||
}
|
||||
}
|
||||
|
||||
async generateMemberGallery(userToken, clubId, size = 200) {
|
||||
async generateMemberGallery(userToken, clubId, size = 200, createImage = true) {
|
||||
try {
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
@@ -1153,6 +1153,15 @@ class MemberService {
|
||||
error: 'Keine aktiven Mitglieder mit Bildern gefunden'
|
||||
};
|
||||
}
|
||||
|
||||
// Wenn kein Bild erstellt werden soll (z.B. bei format=json), nur die Liste zurückgeben
|
||||
if (!createImage) {
|
||||
return {
|
||||
status: 200,
|
||||
galleryEntries
|
||||
};
|
||||
}
|
||||
|
||||
// Maximale Breite für die Galerie (Dialog-Breite 900px - 32px Padding = 868px)
|
||||
const maxGalleryWidth = 868;
|
||||
// Berechne maximale Anzahl Spalten, die in die Breite passen
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user