72 Commits

Author SHA1 Message Date
Torsten Schulz (local)
7be98ffeeb Entfernt die myTischtennis-Integration aus dem Backend und Frontend. Löscht Controller, Routen und Service für myTischtennis. Aktualisiert die Datenmodelle, um die neue ExternalServiceAccount-Integration zu unterstützen. Ändert die API-Routen und Frontend-Komponenten, um die neuen Endpunkte zu verwenden. 2025-10-01 21:01:09 +02:00
Torsten Schulz (local)
1ef1711eea Merge branch 'main' into httv 2025-10-01 13:49:26 +02:00
Torsten Schulz (local)
85981a880d Ändert die Navigationsstruktur in App.vue, indem die Klasse der Fußzeile von "nav-menu" zu "sidebar-footer" geändert wird. Fügt eine neue CSS-Klasse "nav-menu-no-flex" hinzu, um das Layout der Navigation zu optimieren und die Flexbox-Eigenschaften anzupassen. 2025-10-01 13:49:13 +02:00
Torsten Schulz (local)
84503b6404 Merge branch 'httv' 2025-10-01 13:31:41 +02:00
Torsten Schulz (local)
bcc3ce036d Ersetzt Konsolenausgaben durch eine bedingte Entwicklungsprotokollierungsfunktion in mehreren Controllern und Services. Dies verbessert die Protokollierung und Fehlerverfolgung im gesamten Code. Aktualisiert die Benutzer-Utils, um die neue Protokollierungsfunktion zu verwenden. 2025-10-01 13:29:49 +02:00
Torsten Schulz (local)
0fe0514660 Verbessert die Protokollierung in den Club-Controller- und Benutzer-Utils-Dateien, indem die Konsolenausgaben durch eine bedingte Entwicklungsprotokollierungsfunktion ersetzt werden. Aktualisiert die Fehlerbehandlung, um detailliertere Fehlermeldungen auszugeben. 2025-10-01 13:24:16 +02:00
Torsten Schulz (local)
431ec861ba Erweitert die Trainingsstatistik-Funktionalität im TrainingStatsController um die Abfrage und Formatierung von Trainingstagen der letzten 12 Monate. Aktualisiert die Benutzeroberfläche in TrainingStatsView.vue zur Anzeige dieser Trainingstage in einer aufklappbaren Tabelle. Fügt Funktionen zum Umschalten der Sichtbarkeit von Trainingstagen und Mitgliedern hinzu. 2025-10-01 13:20:36 +02:00
Torsten Schulz (local)
648b608036 Erweitert die Trainingsstatistik-Funktionalität im TrainingStatsController, um die Anzahl der Trainings in den letzten 12 und 3 Monaten zu berechnen und zurückzugeben. Aktualisiert die Benutzeroberfläche in TrainingStatsView.vue zur Anzeige dieser neuen Daten. Ändert die Navigation in App.vue, um direkt zu den Trainingsstatistiken zu führen. 2025-10-01 13:01:54 +02:00
Torsten Schulz (local)
4ac71d967f Fügt Unterstützung für myTischtennis-Integration hinzu. Aktualisiert die Mitglieder-Controller und -Routen, um die Aktualisierung von TTR/QTTR-Werten zu ermöglichen. Ergänzt die Benutzeroberfläche in MembersView.vue zur Aktualisierung der Bewertungen und fügt neue Routen für die myTischtennis-Daten hinzu. Aktualisiert die Datenmodelle, um die neuen Felder für TTR und QTTR zu integrieren. 2025-10-01 12:09:55 +02:00
Torsten Schulz (local)
75d304ec6d Merge branch 'activitypainter' 2025-10-01 09:42:33 +02:00
Torsten Schulz (local)
afd96f5df1 Optimiert die Berechnung der Startposition im CourtDrawingRender.vue, indem die Offset-Logik für vertikale und horizontale Zeichnungen präzisiert wird. Kommentiert den Code zur besseren Verständlichkeit der Offset-Berechnungen. 2025-10-01 09:42:15 +02:00
Torsten Schulz (local)
4bfa6a5889 Erweitert die Funktionalität in CourtDrawingRender.vue und CourtDrawingTool.vue zur Verbesserung der Zeichnungslogik. Fügt neue Offset-Parameter für Zielkreise hinzu und optimiert die Berechnung der Zielpositionen. Entfernt die Schaltflächen für das manuelle Speichern in CourtDrawingTool.vue zugunsten einer automatischen Speicherung. Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue zur Unterstützung der neuen Zeichnungsdaten-Logik. 2025-10-01 09:41:07 +02:00
Torsten Schulz (local)
144034a305 Verbessert die Benutzeroberfläche in ScheduleView.vue durch Hinzufügen von Funktionen zum Laden von Gesamt- und Erwachsenenspielplänen. Implementiert eine Hover-Info für Spiele mit Fallback-Werten und optimiert die Anzeige von Spielinformationen. Fügt neue CSS-Klassen für die Hervorhebung von Spielen heute und in der nächsten Woche hinzu. 2025-09-25 19:45:20 +02:00
Torsten Schulz (local)
f4187512ba Erweitert die Funktionalität zur Erstellung und Aktualisierung von vordefinierten Aktivitäten, indem das Feld für Zeichnungsdaten in den entsprechenden Controllern, Modellen und Services hinzugefügt wird. Aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue und PredefinedActivities.vue, um die Handhabung von Zeichnungsdaten zu verbessern und die Logik für das Laden und Speichern von Zeichnungen zu optimieren. 2025-09-25 19:35:13 +02:00
Torsten Schulz (local)
b557297bf0 Verbessert die Logik zur Erstellung von Aktivitäten im DiaryDateActivityService, um PredefinedActivities robuster zu finden. Fügt Unterstützung für die Suche nach Aktivitäten per ID, Name oder Code hinzu. Aktualisiert die Benutzeroberfläche in DiaryView.vue zur Anzeige von Zeichnungsdaten und integriert ein neues Rendering-Modal für Zeichnungen. Optimiert die Bildanzeige in CourtDrawingTool.vue und implementiert eine verbesserte Fehlerbehandlung beim Laden von Bildern. 2025-09-23 14:40:41 +02:00
Torsten Schulz (local)
eb2273e28c Aktualisiert die Token-Lebensdauer im Authentifizierungsdienst auf 3 Stunden und verbessert die Logik zur Auswahl der Startposition im CourtDrawingTool.vue, um eine Standard-Startposition festzulegen, wenn keine ausgewählt ist. 2025-09-23 09:13:51 +02:00
Torsten Schulz (local)
091599b745 Erweitert die Funktionalität in PredefinedActivityImageController.js, um Zeichnungsdaten aus dem Request zu extrahieren und in der Datenbank zu speichern. Aktualisiert das Datenmodell in PredefinedActivityImage.js, um ein neues Feld für Zeichnungsdaten hinzuzufügen. Passt die Routen in predefinedActivityRoutes.js an, um die neue PUT-Methode für das Hochladen von Bildern zu unterstützen. Integriert die Zeichnungsdaten in die Aktivitätenlogik in diaryDateActivityService.js und aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue zur Unterstützung von Zeichnungsdaten. Verbessert die Handhabung von Bild-Uploads in PredefinedActivities.vue und implementiert die Logik zum Laden von Zeichnungsdaten beim Bearbeiten von Aktivitäten. 2025-09-23 08:39:13 +02:00
Torsten Schulz (local)
d70a5ca63e Erweitert die Funktionalität in PredefinedActivities.vue um die Möglichkeit, eine Übungszeichnung zu erstellen. Fügt ein Zeichen-Tool hinzu, das die Zeichnungsdaten speichert und automatisch als Bild-Link verwendet, wenn kein Bild-Link vorhanden ist. Aktualisiert die Benutzeroberfläche zur Bild- und Zeichnungshinzufügung. 2025-09-22 12:23:39 +02:00
Torsten Schulz (local)
09ffd1db3d Fügt die Funktionalität zum schnellen Hinzufügen von Mitgliedern in DiaryView.vue hinzu. Implementiert einen Dialog zur Eingabe von Mitgliedsdaten, einschließlich Vorname, Nachname, Geburtsdatum und Geschlecht. Aktualisiert die Logik zur Validierung neuer Mitglieder und zur Integration in die Mitgliederliste. 2025-09-21 19:25:30 +02:00
Torsten Schulz (local)
d90acf43e1 Verbessert die Benutzeroberfläche in OfficialTournaments.vue durch Anpassung der PDF-Generierungsfunktion. Die Schaltfläche zum Erzeugen von PDFs schließt nun das Dialogfeld automatisch. Aktualisiert die Logik zur Auswahl von Mitgliedern, um die ausgewählte ID beim Ändern des Status zu aktualisieren. 2025-09-21 18:49:10 +02:00
Torsten Schulz (local)
adb93af906 Fügt die Unterstützung für Teilnahmegebühren in officialTournamentController.js hinzu, einschließlich der Extraktion von Gebühren aus dem Turniertext. Aktualisiert das Datenmodell in OfficialTournament.js, um die Teilnahmegebühren zu speichern. Passt die Benutzeroberfläche in OfficialTournaments.vue an, um die Teilnahmegebühren anzuzeigen, und aktualisiert PDFGenerator.js, um die Gebühren im PDF-Dokument darzustellen. 2025-09-21 18:39:25 +02:00
Torsten Schulz (local)
a36f0ea446 Aktualisiert die Benutzeroberfläche in OfficialTournaments.vue zur Anzeige des Teilnehmerstatus mit neuen Status-Badges und Aktionsbuttons. Implementiert die Logik zur Aktualisierung des Status eines Teilnehmers, einschließlich der Optionen Anmelden, Teilnehmen und Zurücksetzen. Verbessert die Darstellung der Platzierungseingabe und optimiert das Styling für eine bessere Benutzererfahrung. 2025-09-21 18:11:16 +02:00
Torsten Schulz (local)
e4fcf2eca2 Fügt die Funktionalität zur Aktualisierung des Teilnehmerstatus in officialTournamentController.js hinzu. Implementiert die Route zum Aktualisieren des Status eines Teilnehmers in officialTournamentRoutes.js und passt die Benutzeroberfläche in OfficialTournaments.vue an, um den neuen Status anzuzeigen und Aktionen wie Anmelden, Teilnehmen und Zurücksetzen zu ermöglichen. 2025-09-21 18:05:50 +02:00
Torsten Schulz (local)
0ee16c7766 Fügt detaillierte Konsolenausgaben in TournamentService.js hinzu, um den Prozess der Match-Erstellung für Gruppen zu verfolgen. Aktualisiert das Styling in main.scss, um die Schriftartgewichtung auf 700 zu erhöhen und die Texttransformation zu entfernen. 2025-09-21 17:49:41 +02:00
Torsten Schulz (local)
21c19298da Fügt die Möglichkeit hinzu, Teilnehmer aus dem heutigen Trainingstag zu laden, einschließlich der Logik zur Überprüfung, ob heute ein Trainingstag stattfindet. Implementiert die Methode zum Laden der Teilnehmer und optimiert die Benutzeroberfläche mit einem neuen Button für diese Funktion. 2025-09-21 17:44:39 +02:00
Torsten Schulz (local)
3c65fed994 Fügt die Funktion zum Laden von Turnieren beim Start hinzu und optimiert die Turniererstellung, um die Turnierliste nach der Erstellung eines neuen Turniers automatisch zu aktualisieren. Verbessert die Anzeige von Turnierdaten, indem der Turniername priorisiert wird, und behandelt Fehler beim Laden und Erstellen von Turnieren. 2025-09-21 17:31:12 +02:00
Torsten Schulz (local)
66046ddccd Aktualisiert die Punktevergabe in TournamentService.js und TournamentsView.vue, sodass der Sieger +1 Punkt erhält und der Verlierer -1 Punkt. Fügt eine neue Methode getLivePosition hinzu, um die Live-Punkte und -Sätze der Spieler in der Gruppe zu berechnen und anzuzeigen. Optimiert die Darstellung der Platzierung in der Tabelle. 2025-09-21 17:25:23 +02:00
Torsten Schulz (local)
561d8186d3 Verbessert die Logik zur Zuordnung von Teilnehmern in TournamentService.js, indem manuelle Zuordnungen berücksichtigt werden. Implementiert eine zufällige Verteilung der Teilnehmer nur, wenn keine manuelle Zuordnung vorhanden ist. Aktualisiert die Erstellung von Matches, um sicherzustellen, dass nur Spieler aus derselben Gruppe gegeneinander antreten. In TournamentsView.vue wird die Teilnehmerliste jetzt kollabierbar, und es werden neue Funktionen zur Anzeige von Spielergebnissen und zur Hervorhebung von Matches hinzugefügt. 2025-09-21 17:16:47 +02:00
Torsten Schulz (local)
312f8f24ab Optimiert das Styling in DiaryView.vue, indem die Bildgrößenanpassung auf viewport-basierte Einheiten umgestellt wird. Entfernt Margen und Polsterungen für eine bessere Darstellung der Bilder im Overlay. 2025-09-16 00:16:54 +02:00
Torsten Schulz (local)
ba4b56360d Optimiert die Darstellung von vordefinierten Aktivitäten in DiaryView.vue, indem die Logik zur Anzeige von Aktivitätsnamen und -codes verbessert wird. Aktualisiert das Styling der Bildsymbole und passt die Positionierung der Bilder an, um eine bessere Benutzererfahrung zu gewährleisten. 2025-09-16 00:14:30 +02:00
Torsten Schulz (local)
02732a01da Fügt die Möglichkeit hinzu, Bilder vordefinierter Aktivitäten in DiaryView.vue anzuzeigen. Implementiert die Methode showActivityImage zur Anzeige des Bildes und aktualisiert das Styling für die Bildsymbole. 2025-09-15 23:58:48 +02:00
Torsten Schulz (local)
4307fa7d82 Entfernt die Authentifizierung von der Route zum Abrufen von vordefinierten Aktivitätsbildern in predefinedActivityRoutes.js. Aktualisiert den Alt-Text für Bilder in PredefinedActivities.vue von "Activity Image" auf "Predefined Activity Image". 2025-09-15 23:56:18 +02:00
Torsten Schulz (local)
a1dc6afb2c Ändert die Zugriffskontrolle in predefinedActivityImageController.js von checkAccess zu checkGlobalAccess, um die globale Authentifizierung für vordefinierte Aktivitäten zu ermöglichen. Fügt die Funktion checkGlobalAccess in userUtils.js hinzu, die die Benutzerinformationen basierend auf dem Token zurückgibt. 2025-09-15 23:53:49 +02:00
Torsten Schulz (local)
92ce64b807 Fügt die Funktion zum Löschen von vordefinierten Aktivitätsbildern hinzu. Implementiert die Logik in der Datei predefinedActivityImageController.js und aktualisiert die Routen in predefinedActivityRoutes.js. Ergänzt die Benutzeroberfläche in PredefinedActivities.vue um die Möglichkeit, hochgeladene Bilder anzuzeigen und zu löschen. 2025-09-15 23:46:59 +02:00
Torsten Schulz (local)
296939d1a0 Entfernt die Deaktivierung des "Teilnehmer-PDF"-Buttons in OfficialTournaments.vue, um die Benutzerfreundlichkeit zu verbessern und die PDF-Generierung jederzeit zu ermöglichen. 2025-09-12 14:30:53 +02:00
Torsten Schulz (local)
dc8a5778d6 Implementiert die Funktion zur Generierung eines Teilnehmer-PDFs in OfficialTournaments.vue. Fügt die Methode addParticipantsSummary in PDFGenerator.js hinzu, um eine Zusammenfassung der Teilnehmerdaten in einem PDF-Dokument darzustellen. Integriert die Logik zur Gruppierung und Formatierung der Teilnehmerinformationen basierend auf ihrem Anmeldestatus und der Teilnahme. 2025-09-12 14:23:47 +02:00
Torsten Schulz (local)
cf04e5bfe8 Erweitert die Benutzeroberfläche in OfficialTournaments.vue um einen neuen Tab für Teilnehmer, einschließlich Filteroptionen zur Anzeige von Anmeldestatus und Teilnahme. Implementiert die Logik zur Gruppierung und Anzeige der Teilnehmerdaten in einer Tabelle. 2025-09-12 13:58:04 +02:00
Torsten Schulz (local)
ace15ae1d3 Aktualisiert die index.html zur Unterstützung der deutschen Sprache und verbessert die SEO durch Hinzufügen von Meta-Tags. Modifiziert App.vue, um das Logo in der Kopfzeile anzuzeigen und fügt einen Footer mit Links zu Impressum und Datenschutzerklärung hinzu. Überarbeitet Home.vue mit neuen Marketing- und Funktionsabschnitten sowie einer FAQ-Sektion zur Benutzerinformation. Ergänzt Router-Konfiguration um Impressum- und Datenschutzseiten. 2025-09-11 15:32:49 +02:00
Torsten Schulz (local)
d4b82a3a6f Erweitert die Methode eligibleMembers in OfficialTournaments.vue, um nur aktive Mitglieder zu filtern, die für Wettbewerbe berechtigt sind. Dies verbessert die Genauigkeit der angezeigten Teilnehmerliste. 2025-09-11 14:58:33 +02:00
Torsten Schulz (local)
48cd0921df Fügt die Methode listClubParticipations im OfficialTournamentController hinzu, um die Teilnahme von Mitgliedern an offiziellen Turnieren zu listen. Aktualisiert die Routen, um diese neue Funktionalität zu integrieren. Verbessert die Benutzeroberfläche in OfficialTournaments.vue mit Tabs zur Anzeige von Veranstaltungen und Turnierbeteiligungen sowie einer Filteroption für den Zeitraum der Beteiligungen. 2025-09-11 14:11:19 +02:00
Torsten Schulz (local)
df02e48cfd Fügt das Modell OfficialCompetitionMember hinzu und implementiert die Logik zur Verwaltung der Teilnahme von Mitgliedern an offiziellen Wettbewerben. Aktualisiert die Routen und Controller, um die Teilnahmeinformationen zu speichern und abzurufen. Ergänzt die Benutzeroberfläche in OfficialTournaments.vue zur Anzeige und Bearbeitung der Teilnahmeoptionen für Mitglieder. 2025-09-11 12:58:56 +02:00
Torsten Schulz (local)
4a6d868820 Ändert die Schriftgröße der Navigationslinks in App.vue von 0.75rem auf 1rem, um die Lesbarkeit zu verbessern. 2025-09-01 11:38:44 +02:00
Torsten Schulz (local)
52556a4292 Fügt ein neues Skript zur Bereinigung aller Indizes in package.json hinzu und entfernt überflüssige Leerzeichen in diaryDateActivityService.js. 2025-09-01 11:27:09 +02:00
Torsten Schulz (server)
3a02ffb3e3 Merge branch 'main' of ssh://tsschulz.de:/home/git/trainingstagebuch 2025-09-01 09:23:26 +00:00
Torsten Schulz (local)
c4b9a7d782 Verbessert die Benutzeroberfläche in DiaryView.vue, indem die Struktur des Unfallformulars optimiert und die Audioinitialisierung an die Benutzerinteraktion angepasst wird. Fügt Logik zur Überprüfung von Aktivitätszeiten hinzu und stellt sicher, dass Audio nur bei aktivierter Funktion abgespielt wird. 2025-09-01 11:23:02 +02:00
Torsten Schulz (server)
5e8b221541 Merge branch 'main' of ssh://tsschulz.de:/home/git/trainingstagebuch 2025-09-01 07:38:48 +00:00
Torsten Schulz (server)
26720c8df3 updated package.json 2025-09-01 07:38:43 +00:00
Torsten Schulz (local)
a1ab742126 Optimiert das Styling in DiaryView.vue, indem die Überlauf-Eigenschaften des Trainingsplan-Div-Elements angepasst werden. Entfernt die horizontale Überlauf-Einstellung, um die Benutzeroberfläche zu verbessern. 2025-09-01 09:37:43 +02:00
Torsten Schulz (local)
f21ad3d8a3 Fügt eine neue Skriptfunktion zum Bereinigen von Benutzertoken hinzu und aktualisiert die Logik zum Synchronisieren des UserToken-Modells. Implementiert eine neue Controller-Methode zum Löschen von Datumsangaben für Clubs und passt die Routen entsprechend an. Ergänzt die Benutzeroberfläche in DiaryView.vue um die Möglichkeit, ein Datum zu löschen, und aktualisiert die Logik zur Überprüfung der Datumsaktualität. 2025-09-01 09:33:54 +02:00
Torsten Schulz (local)
51d3087006 Fügt die Anzeige des letzten Trainingsdatums und einen Sortiermechanismus in der Mitgliederstatistik hinzu. Aktualisiert die Backend-Logik zur Berechnung des letzten Trainings und passt die Benutzeroberfläche in TrainingStatsView.vue entsprechend an. 2025-08-31 21:32:03 +02:00
Torsten Schulz (local)
a08588a075 Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue, um die Auswahlmöglichkeiten für das Zusammenführen von Aktivitäten zu verbessern. Sortiert die Aktivitäten in den Dropdown-Listen nach Namen, um die Benutzerfreundlichkeit zu erhöhen. 2025-08-31 21:19:51 +02:00
Torsten Schulz (local)
5d67a52b45 Verbessert das Styling in PredefinedActivities.vue, um die Benutzeroberfläche zu optimieren. Fügt eine Höhe für das Hauptlayout hinzu und ermöglicht das Scrollen in der Liste sowie eine sticky Positionierung für die Toolbar, um die Benutzerfreundlichkeit zu erhöhen. 2025-08-31 21:14:30 +02:00
Torsten Schulz (local)
f29425c987 Fügt Funktionen zum Zusammenführen und Entfernen von Duplikaten vordefinierter Aktivitäten hinzu. Implementiert die entsprechenden Controller-Methoden und Routen. Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue, um die neuen Funktionen zur Verfügung zu stellen und die Aktivitäten nach Namen und Code zu sortieren. 2025-08-31 21:09:48 +02:00
Torsten Schulz (local)
e3b8488d2b Erweitert die PDF-Generierung in PDFGenerator.js, um empfohlene und andere Wettbewerbe für Mitglieder zu unterscheiden. Fügt eine neue Struktur für die Anzeige von Empfehlungen und Hinweisen hinzu. Aktualisiert OfficialTournaments.vue, um die Auswahl von Mitgliedern und deren Wettbewerben zu verbessern, einschließlich einer neuen Dialogstruktur und der Verwaltung von Empfehlungen. 2025-08-31 15:55:49 +02:00
Torsten Schulz (local)
f49e1896b9 Fügt eine Funktion zur PDF-Generierung für ausgewählte Mitglieder in OfficialTournaments.vue hinzu. Implementiert ein Dialogfeld zur Auswahl von Mitgliedern und ermöglicht die Erstellung eines PDFs mit Wettbewerbsinformationen. Aktualisiert das Styling für die Benutzeroberfläche und die Modal-Komponenten. 2025-08-31 15:28:46 +02:00
Torsten Schulz (local)
2092473cf3 Verbessert die Mitgliederansicht in MembersView.vue, indem inaktive Mitglieder visuell hervorgehoben werden. Fügt CSS-Klassen hinzu, um die Darstellung inaktiver Mitglieder zu optimieren, einschließlich einer inaktiven Auszeichnung und einer durchgestrichenen Schriftart für inaktive Geschlechtssymbole und -bezeichnungen. 2025-08-31 15:11:01 +02:00
Torsten Schulz (local)
c00849a154 Verbessert die Mitgliederansicht in ClubView.vue, indem aktive Mitglieder nach Nachnamen und Vornamen sortiert angezeigt werden. Fügt Geschlechtssymbole und -bezeichnungen hinzu, um die Darstellung zu optimieren. Implementiert neue Methoden zur Geschlechtslabelierung und -symbolisierung sowie entsprechende CSS-Klassen für eine ansprechendere Benutzeroberfläche. 2025-08-31 15:02:57 +02:00
Torsten Schulz (local)
8069946154 Aktualisiert die Mitgliederansicht in MembersView.vue, um Geschlechtssymbole und -bezeichnungen anzuzeigen. Entfernt die Geschlechtsspalte und implementiert neue Methoden zur Darstellung von Geschlecht. Fügt CSS-Klassen für die Geschlechtsdarstellung hinzu, um die Benutzeroberfläche zu verbessern. 2025-08-30 23:39:28 +02:00
Torsten Schulz (local)
975800c1ab Fügt Unterstützung für offizielle Turniere und Wettbewerbe hinzu. Aktualisiert die Datenbankmodelle, um Geschlecht für Mitglieder zu erfassen, und implementiert neue Routen sowie Frontend-Komponenten zur Anzeige und Verwaltung dieser Daten. Verbessert die Benutzeroberfläche zur Eingabe von Mitgliederdaten und aktualisiert die Abhängigkeiten im Projekt. 2025-08-30 23:16:39 +02:00
Torsten Schulz (local)
b82a80a11d Fügt Unterstützung für Aktivitätenmitglieder in DiaryView.vue hinzu. Ermöglicht das Zuordnen von Teilnehmern zu Aktivitäten, einschließlich der Verwaltung von Teilnehmern über das Backend. Aktualisiert die Datenbankmodelle und -routen, um die neuen Funktionen zu unterstützen. 2025-08-28 14:43:04 +02:00
Torsten Schulz (local)
244b61c901 Fügt Unterstützung für vordefinierte Aktivitäten hinzu, einschließlich der Möglichkeit, Bilder hochzuladen und zu suchen. Aktualisiert die Datenbankmodelle und -routen entsprechend. Verbessert die Benutzeroberfläche zur Anzeige und Bearbeitung von Aktivitäten in DiaryView.vue. 2025-08-28 14:11:29 +02:00
Torsten Schulz (local)
c7325ac982 Erweitert die updateActivity-Methode in DiaryDateActivityService, um benutzerdefinierte Aktivitäten zu verarbeiten und neue vordefinierte Aktivitäten zu erstellen. Fügt eine Methode loadTrainingPlan in DiaryView.vue hinzu, um die Anzeige nach Änderungen zu aktualisieren. 2025-08-28 13:14:06 +02:00
Torsten Schulz (local)
8fbdc68016 Implementiert die Bearbeitung von Aktivitäten in DiaryView.vue. Fügt Eingabefelder und Schaltflächen zum Speichern oder Abbrechen von Änderungen hinzu. Aktualisiert das Styling für klickbare Elemente. 2025-08-28 12:04:33 +02:00
Torsten Schulz (local)
455b2c94cd Aktualisiert die Anzeige der Teilnehmeranzahl in DiaryView.vue, indem die Variable von 'members' auf 'participants' geändert wurde. 2025-08-28 09:31:24 +02:00
Torsten Schulz (local)
c9a1026b50 Aktualisiert die Anzeige der Teilnehmeranzahl in DiaryView.vue, indem die Anzahl der Mitglieder in der Überschrift angezeigt wird. 2025-08-28 09:28:00 +02:00
Torsten Schulz (local)
f6f1ea0403 Erhöht das Padding am unteren Rand der Spalte in DiaryView.vue von 3em auf 4em, um das Layout weiter zu optimieren. 2025-08-28 09:25:54 +02:00
Torsten Schulz (local)
a636b32510 Fügt Padding zum unteren Rand der Spalte in DiaryView.vue hinzu, um das Layout zu verbessern. 2025-08-28 09:22:26 +02:00
Torsten Schulz (local)
8ee1203ec6 Implementiert Benutzer-Authentifizierung und Datenladung bei Login. Fügt Links für Registrierung und Login in den entsprechenden Komponenten hinzu. Aktualisiert das Styling für Login- und Registrierungslinks. 2025-08-27 09:10:53 +02:00
Torsten Schulz (local)
bce5150757 Aktualisiert die Datenbankkonfiguration und ändert den Import von Komponenten in SCSS auf die neue Syntax. 2025-08-23 22:02:20 +02:00
Torsten Schulz (local)
117f6b4c93 Fügt Sortierfunktion für Ligen hinzu und entfernt die automatische Neuladung bei Logout 2025-08-23 21:57:55 +02:00
Torsten Schulz (local)
6a8b0e35d7 Fügt Sortierfunktionalität für die Mitgliederstatistik hinzu. Die Tabellenüberschriften sind jetzt klickbar und ermöglichen eine Sortierung nach Name und Teilnahmezahlen. Zudem wurde die Sortierreihenfolge implementiert und visuell durch Icons angezeigt. 2025-08-22 16:06:56 +02:00
Torsten Schulz (local)
ed96fc5f27 Aktualisiert den TrainingStatsController, um nur aktive Mitglieder eines spezifischen Vereins zu laden, indem die Abfrage um die clubId erweitert wurde. 2025-08-22 15:53:50 +02:00
128 changed files with 13591 additions and 1131 deletions

View File

@@ -0,0 +1,603 @@
import axios from 'axios';
import fs from 'fs';
import path from 'path';
const BASE_URL = 'https://ttde-id.liga.nu';
const CLICK_TT_BASE = 'https://httv.click-tt.de';
class HettvClient {
constructor() {
this.baseURL = BASE_URL;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 15000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
},
maxRedirects: 5, // Folge den OAuth2-Redirects
validateStatus: (status) => status >= 200 && status < 400
});
// Einfache Cookie-Jar nach Host -> { name: value }
this.cookieJar = new Map();
this.defaultHeaders = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
};
}
/**
* Login to HeTTV via OAuth2
* @param {string} username - HeTTV username (email)
* @param {string} password - HeTTV password
* @returns {Promise<Object>} Login response with session data
*/
async login(username, password) {
try {
console.log('[HettvClient] - Starting login for:', username);
// Schritt 1: OAuth2-Authorization-Endpoint aufrufen - das sollte zur Login-Seite weiterleiten
const oauthParams = new URLSearchParams({
'scope': 'nuLiga',
'response_type': 'code',
'redirect_uri': 'https://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/oAuthLogin',
'state': 'nonce=' + Math.random().toString(36).substring(2, 15),
'client_id': 'XtVpGjXKAhz3BZuu'
});
// OAuth2 Start
// Der OAuth2-Endpoint sollte direkt zur Login-Seite weiterleiten
const loginPageResponse = await this.client.get(`/oauth2/authz/ttde?${oauthParams.toString()}`, {
maxRedirects: 5, // Folge den Redirects zur Login-Seite
validateStatus: (status) => status >= 200 && status < 400,
headers: {
...this.defaultHeaders
}
});
// Login-Seite erreicht
// Session-Cookie aus der Login-Seite extrahieren
const setCookies = loginPageResponse.headers['set-cookie'];
if (!setCookies || !Array.isArray(setCookies)) {
console.error('[HettvClient] - No cookies from login page');
return {
success: false,
error: 'Keine Session-Cookie von Login-Seite erhalten'
};
}
const sessionCookie = setCookies.find(cookie => cookie.startsWith('nusportingress='));
if (!sessionCookie) {
console.error('[HettvClient] - No nusportingress cookie from login page');
return {
success: false,
error: 'Keine nusportingress Session von Login-Seite erhalten'
};
}
// Extrahiere t:formdata aus dem HTML der Login-Seite
const htmlContent = loginPageResponse.data;
// HTML erhalten
// Suche nach t:formdata im HTML - verschiedene mögliche Formate
let formDataMatch = htmlContent.match(/name="t:formdata"\s+value="([^"]+)"/);
if (!formDataMatch) {
// Versuche andere Formate
formDataMatch = htmlContent.match(/name='t:formdata'\s+value='([^']+)'/);
}
if (!formDataMatch) {
// Suche nach hidden input mit t:formdata (value vor name)
formDataMatch = htmlContent.match(/<input[^>]*value="([^"]+)"[^>]*name="t:formdata"/);
}
if (!formDataMatch) {
// Suche nach hidden input mit t:formdata (name vor value)
formDataMatch = htmlContent.match(/<input[^>]*name="t:formdata"[^>]*value="([^"]+)"/);
}
if (!formDataMatch) {
// Suche nach t:formdata ohne Anführungszeichen
formDataMatch = htmlContent.match(/name=t:formdata\s+value=([^\s>]+)/);
}
if (!formDataMatch) {
console.error('[HettvClient] - No t:formdata found in login page');
console.log('[HettvClient] - HTML snippet:', htmlContent.substring(0, 2000));
// Debug: Suche nach allen hidden inputs
const hiddenInputs = htmlContent.match(/<input[^>]*type="hidden"[^>]*>/g);
console.log('[HettvClient] - Hidden inputs found:', hiddenInputs);
return {
success: false,
error: 'Keine t:formdata von Login-Seite erhalten'
};
}
const tFormData = formDataMatch[1];
// CSRF-Token gefunden
// Schritt 2: Login mit den korrekten Daten durchführen
// Verwende die Session-Cookie für den Login-Request
const formData = new URLSearchParams();
formData.append('t:submit', '["submit_0","submit_0"]');
formData.append('t:ac', 'ttde');
formData.append('t:formdata', tFormData);
formData.append('username', username);
formData.append('password', password);
const loginResponse = await this.client.post('/oauth2/login.loginform', formData.toString(), {
headers: {
'Cookie': sessionCookie.split(';')[0],
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
...this.defaultHeaders,
'Referer': `${BASE_URL}/oauth2/login.loginform`
},
maxRedirects: 5,
validateStatus: (status) => status >= 200 && status < 400
});
// Login-Antwort erhalten
// Prüfe ob wir erfolgreich eingeloggt sind
// Login-Response geprüft
// Prüfe den Response-Inhalt um zu sehen ob wir noch auf der Login-Seite sind
const responseContent = loginResponse.data;
const isLoginPage = responseContent.includes('click-TT ID') &&
responseContent.includes('Username') &&
responseContent.includes('Password');
// Login-Page-Erkennung durchgeführt
if (isLoginPage) {
console.log('[HettvClient] - Still on login page, login failed');
console.log('[HettvClient] - Response snippet:', responseContent.substring(0, 500));
return {
success: false,
error: 'Login fehlgeschlagen - ungültige Zugangsdaten'
};
}
// Prüfe auf OAuth2-Redirect oder Erfolg
const hasOAuthRedirect = responseContent.includes('oauth2') ||
responseContent.includes('redirect') ||
loginResponse.status >= 300;
// OAuth Redirect erkannt
// Extrahiere die finale Session-Cookie
const finalCookies = loginResponse.headers['set-cookie'];
const finalSessionCookie = finalCookies?.find(cookie => cookie.startsWith('nusportingress='));
const sessionId = (finalSessionCookie || sessionCookie).match(/nusportingress=([^;]+)/)?.[1];
console.log('[HettvClient] - Login erfolgreich (HeTTV).');
// Versuche die finale OAuth-Weiterleitung zu httv.click-tt.de aufzurufen, um PHPSESSID zu erhalten
let finalUrl = loginResponse.request?.res?.responseUrl;
console.log('[HettvClient] - Login finalUrl:', finalUrl);
let phpSessIdCookie = null;
let finalHtml = null;
try {
if (finalUrl && finalUrl.includes('oAuthLogin')) {
const clickTTClient = axios.create({
timeout: 15000,
maxRedirects: 0,
validateStatus: (status) => status >= 200 && status < 400
});
// Folge der Redirect-Kette manuell, übernehme Cookies
let currentUrl = finalUrl;
let lastResp = null;
let hop = 0;
const maxHops = 10;
while (hop++ < maxHops && currentUrl) {
lastResp = await clickTTClient.get(currentUrl, {
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
...this.defaultHeaders,
'Referer': hop === 1 ? `${BASE_URL}/oauth2/login.loginform` : (lastResp?.request?.res?.responseUrl || currentUrl),
'Cookie': this._cookieHeaderForUrl(currentUrl)
}
});
this._ingestSetCookiesFromResponse(currentUrl, lastResp.headers['set-cookie']);
const loc = lastResp.headers['location'];
if (loc) {
// Absolut vs relativ
if (/^https?:\/\//i.test(loc)) {
currentUrl = loc;
} else {
const u = new URL(currentUrl);
currentUrl = `${u.origin}${loc}`;
}
continue;
}
break; // keine weitere Location => final
}
const clickTTResp = lastResp;
finalHtml = typeof clickTTResp.data === 'string' ? clickTTResp.data : '';
const ctSetCookies = clickTTResp.headers['set-cookie'];
if (Array.isArray(ctSetCookies)) {
phpSessIdCookie = ctSetCookies.find(c => c.startsWith('PHPSESSID='))?.split(';')[0] || null;
}
// Finale click-TT URL ermittelt
}
} catch (e) {
// Finale click-TT Seite konnte nicht geladen werden
}
// Baue kombinierte Cookie-Kette (falls PHPSESSID vorhanden)
const baseCookie = (finalSessionCookie || sessionCookie).split(';')[0];
const combinedCookie = phpSessIdCookie ? `${baseCookie}; ${phpSessIdCookie}` : baseCookie;
return {
success: true,
sessionId: sessionId,
cookie: combinedCookie,
accessToken: null,
refreshToken: null,
expiresAt: null,
user: {
finalUrl: finalUrl || null,
htmlSnippet: finalHtml ? finalHtml.substring(0, 2000) : null
}
};
} catch (error) {
console.error('HeTTV login error:', error.message);
console.error('Error details:', error.response?.status, error.response?.statusText);
return {
success: false,
error: error.response?.data?.message || 'Login fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Verify login credentials
* @param {string} username - HeTTV username
* @param {string} password - HeTTV password
* @returns {Promise<boolean>} True if credentials are valid
*/
async verifyCredentials(username, password) {
const result = await this.login(username, password);
return result.success;
}
/**
* Make an authenticated request to click-TT
* @param {string} endpoint - API endpoint
* @param {string} cookie - JSESSIONID cookie
* @param {Object} options - Additional axios options
* @returns {Promise<Object>} API response
*/
async authenticatedRequest(endpoint, cookie, options = {}, finalUrl = null) {
try {
// Bestimme Basis-URL dynamisch aus finalUrl, falls vorhanden
let baseURL = CLICK_TT_BASE;
if (finalUrl) {
try {
const url = new URL(finalUrl);
baseURL = url.origin;
} catch (_) {}
}
const isAbsolute = /^https?:\/\//i.test(endpoint);
const client = axios.create({
baseURL: isAbsolute ? undefined : baseURL,
timeout: 15000,
maxRedirects: 0,
validateStatus: (status) => status >= 200 && status < 400
});
// Manuelles Redirect-Following inkl. Cookies/Referer
let currentUrl = isAbsolute ? endpoint : `${baseURL}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
let lastResp = null;
const trace = [];
let hop = 0;
const maxHops = 10;
console.log(`[HettvClient] - Starting redirect chain from: ${currentUrl}`);
while (hop++ < maxHops && currentUrl) {
console.log(`[HettvClient] - Redirect ${hop}: GET ${currentUrl}`);
lastResp = await client.request({
method: options.method || 'GET',
url: currentUrl,
data: options.data,
headers: {
...this.defaultHeaders,
...(options.headers || {}),
'Cookie': this._mergeCookieHeader(cookie, this._cookieHeaderForUrl(currentUrl)),
'Referer': hop === 1 ? (finalUrl || baseURL) : (lastResp?.request?.res?.responseUrl || currentUrl)
}
});
this._ingestSetCookiesFromResponse(currentUrl, lastResp.headers['set-cookie']);
const loc = lastResp.headers['location'];
console.log(`[HettvClient] - Response: ${lastResp.status} ${lastResp.statusText}`);
console.log(`[HettvClient] - Location header: ${loc || 'none'}`);
console.log(`[HettvClient] - Set-Cookie header: ${lastResp.headers['set-cookie'] ? 'present' : 'none'}`);
console.log(`[HettvClient] - Content-Type: ${lastResp.headers['content-type'] || 'none'}`);
// Speichere jede Seite zur Analyse
try {
const dir = path.resolve(process.cwd(), 'backend', 'uploads');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filename = `hettv_redirect_${hop}_${Date.now()}.html`;
const filePath = path.join(dir, filename);
const content = typeof lastResp.data === 'string' ? lastResp.data : JSON.stringify(lastResp.data, null, 2);
fs.writeFileSync(filePath, content, 'utf8');
console.log(`[HettvClient] - Saved page to: ${filename}`);
} catch (e) {
console.log(`[HettvClient] - Could not save page ${hop}:`, e.message);
}
trace.push({
url: currentUrl,
status: lastResp.status,
location: loc || null
});
if (loc) {
const newUrl = /^https?:\/\//i.test(loc) ? loc : `${new URL(currentUrl).origin}${loc}`;
console.log(`[HettvClient] - Following redirect to: ${newUrl}`);
currentUrl = newUrl;
continue;
}
console.log(`[HettvClient] - Final response: ${lastResp.status} (no more redirects)`);
break;
}
return {
success: true,
data: lastResp?.data,
trace
};
} catch (error) {
console.error('HeTTV API error:', error.message);
return {
success: false,
error: error.response?.data?.message || 'API-Anfrage fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Navigate to main HeTTV page and find Downloads menu
* @param {string} cookie - Session cookie
* @returns {Promise<Object>} Response with main page content and download links
*/
async getMainPageWithDownloads(cookie, finalUrl = null) {
try {
console.log('[HettvClient] - Loading main HeTTV page...');
// Kandidaten für Einstiegs-URL bestimmen
let origin = CLICK_TT_BASE;
if (finalUrl) {
try { origin = new URL(finalUrl).origin; } catch (_) {}
}
const candidates = [];
// Direkt zu HeTTV navigieren
candidates.push('http://httv.click-tt.de/');
candidates.push('http://httv.click-tt.de/wa/');
candidates.push('http://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/');
// Wenn wir eine finalUrl haben, verwende diese auch
if (finalUrl) {
candidates.push(finalUrl);
}
console.log('[HettvClient] - URL candidates:', candidates);
let mainPageResponse = null;
let mainTrace = [];
let lastError = null;
for (const candidate of candidates) {
const resp = await this.authenticatedRequest(candidate, cookie, {}, finalUrl);
if (resp.success && typeof resp.data === 'string' && resp.data.length > 0) {
mainPageResponse = resp;
mainTrace = resp.trace || [];
break;
}
lastError = resp;
}
if (!mainPageResponse) {
return lastError || { success: false, error: 'HeTTV Einstiegsseite nicht erreichbar', status: 404 };
}
const htmlContent = mainPageResponse.data;
console.log('[HettvClient] - Main page loaded, HTML length:', htmlContent.length);
// Erkenne Fehlerseite (Session ungültig)
if (/click-TT\s*-\s*Fehlerseite/i.test(htmlContent) || /ungültige oder nicht mehr gültige URL/i.test(htmlContent)) {
return {
success: false,
error: 'Session ungültig oder abgelaufen',
status: 401,
data: { htmlSnippet: htmlContent.substring(0, 1000) }
};
}
// Speichere HTML zur Analyse
let savedFile = null;
try {
const dir = path.resolve(process.cwd(), 'backend', 'uploads');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filename = `hettv_main_${Date.now()}.html`;
const filePath = path.join(dir, filename);
fs.writeFileSync(filePath, htmlContent, 'utf8');
savedFile = filePath;
} catch (e) {
// Ignoriere Speicherfehler still, nur für Debug
}
// Suche nach Downloads-Links im HTML
const downloadLinks = [];
// 1) URL-Heuristiken
const urlPatterns = [
/href="([^"]*download[^"]*)"/gi,
/href="([^"]*downloads[^"]*)"/gi,
/href="([^"]*Download[^"]*)"/gi,
/href="([^"]*Downloads[^"]*)"/gi
];
urlPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(htmlContent)) !== null) {
const link = match[1];
if (link && !downloadLinks.includes(link)) {
downloadLinks.push(link);
}
}
});
// 2) Linktext-Heuristik: <a ...>Downloads</a>
const anchorPattern = /<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
let aMatch;
while ((aMatch = anchorPattern.exec(htmlContent)) !== null) {
const href = aMatch[1];
const text = aMatch[2].replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
if (/\bdownloads?\b/i.test(text)) {
if (href && !downloadLinks.includes(href)) {
downloadLinks.push(href);
}
}
}
// 3) Fallback: Menüpunkte in Navigationen (role="navigation" etc.)
if (downloadLinks.length === 0) {
const navSectionRegex = /<nav[\s\S]*?<\/nav>/gi;
let nav;
while ((nav = navSectionRegex.exec(htmlContent)) !== null) {
const section = nav[0];
let m;
anchorPattern.lastIndex = 0;
while ((m = anchorPattern.exec(section)) !== null) {
const href = m[1];
const text = m[2].replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
if (/\bdownloads?\b/i.test(text)) {
if (href && !downloadLinks.includes(href)) {
downloadLinks.push(href);
}
}
}
}
}
console.log('[HettvClient] - Found download links:', downloadLinks);
return {
success: true,
data: {
htmlContent: htmlContent,
downloadLinks: downloadLinks,
htmlSnippet: htmlContent.substring(0, 2000), // Erste 2000 Zeichen für Analyse
savedFile,
trace: mainTrace,
lastUrl: mainTrace.length ? mainTrace[mainTrace.length - 1].url : null,
lastStatus: mainTrace.length ? mainTrace[mainTrace.length - 1].status : null
}
};
} catch (error) {
console.error('HeTTV main page error:', error.message);
return {
success: false,
error: error.message || 'Fehler beim Laden der Hauptseite',
status: 500
};
}
}
/**
* Load a specific download page
* @param {string} downloadUrl - URL to the download page
* @param {string} cookie - Session cookie
* @returns {Promise<Object>} Response with download page content
*/
async loadDownloadPage(downloadUrl, cookie, finalUrl = null) {
try {
console.log('[HettvClient] - Loading download page:', downloadUrl);
const response = await this.authenticatedRequest(downloadUrl, cookie, {}, finalUrl);
if (!response.success) {
return response;
}
const htmlContent = response.data;
console.log('[HettvClient] - Download page loaded, HTML length:', htmlContent.length);
return {
success: true,
data: {
url: downloadUrl,
htmlContent: htmlContent,
htmlSnippet: htmlContent.substring(0, 3000) // Erste 3000 Zeichen für Analyse
}
};
} catch (error) {
console.error('HeTTV download page error:', error.message);
return {
success: false,
error: error.message || 'Fehler beim Laden der Download-Seite',
status: 500
};
}
}
// --- Cookie-Helfer ---
_ingestSetCookiesFromResponse(currentUrl, setCookies) {
if (!Array.isArray(setCookies) || setCookies.length === 0) return;
const { host } = new URL(currentUrl);
if (!this.cookieJar.has(host)) this.cookieJar.set(host, new Map());
const jar = this.cookieJar.get(host);
setCookies.forEach((cookieStr) => {
const pair = cookieStr.split(';')[0];
const eq = pair.indexOf('=');
if (eq > 0) {
const name = pair.substring(0, eq).trim();
const value = pair.substring(eq + 1).trim();
jar.set(name, value);
}
});
}
_cookieHeaderForUrl(currentUrl) {
const { host } = new URL(currentUrl);
const jar = this.cookieJar.get(host);
if (!jar || jar.size === 0) return '';
return Array.from(jar.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
}
_mergeCookieHeader(primary, secondary) {
const items = [];
if (primary) items.push(primary);
if (secondary) items.push(secondary);
return items.filter(Boolean).join('; ');
}
}
export default new HettvClient();

View File

@@ -0,0 +1,284 @@
import axios from 'axios';
const BASE_URL = 'https://www.mytischtennis.de';
class MyTischtennisClient {
constructor() {
this.baseURL = BASE_URL;
this.client = axios.create({
baseURL: this.baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': '*/*'
},
maxRedirects: 0, // Don't follow redirects automatically
validateStatus: (status) => status >= 200 && status < 400 // Accept 3xx as success
});
}
/**
* Login to myTischtennis API
* @param {string} email - myTischtennis email (not username!)
* @param {string} password - myTischtennis password
* @returns {Promise<Object>} Login response with token and session data
*/
async login(email, password) {
try {
// Create form data
const formData = new URLSearchParams();
formData.append('email', email);
formData.append('password', password);
formData.append('intent', 'login');
const response = await this.client.post(
'/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
formData.toString()
);
// Extract the cookie from response headers
const setCookie = response.headers['set-cookie'];
if (!setCookie || !Array.isArray(setCookie)) {
return {
success: false,
error: 'Keine Session-Cookie erhalten'
};
}
// Find the sb-10-auth-token cookie
const authCookie = setCookie.find(cookie => cookie.startsWith('sb-10-auth-token='));
if (!authCookie) {
return {
success: false,
error: 'Kein Auth-Token in Response gefunden'
};
}
// Extract and decode the token
const tokenMatch = authCookie.match(/sb-10-auth-token=base64-([^;]+)/);
if (!tokenMatch) {
return {
success: false,
error: 'Token-Format ungültig'
};
}
const base64Token = tokenMatch[1];
let tokenData;
try {
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
tokenData = JSON.parse(decodedToken);
} catch (decodeError) {
console.error('Error decoding token:', decodeError);
return {
success: false,
error: 'Token konnte nicht dekodiert werden'
};
}
return {
success: true,
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
expiresAt: tokenData.expires_at,
expiresIn: tokenData.expires_in,
user: tokenData.user,
cookie: authCookie.split(';')[0] // Just the cookie value without attributes
};
} catch (error) {
console.error('MyTischtennis login error:', error.message);
return {
success: false,
error: error.response?.data?.message || 'Login fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Verify login credentials
* @param {string} email - myTischtennis email
* @param {string} password - myTischtennis password
* @returns {Promise<boolean>} True if credentials are valid
*/
async verifyCredentials(email, password) {
const result = await this.login(email, password);
return result.success;
}
/**
* Make an authenticated request
* @param {string} endpoint - API endpoint
* @param {string} cookie - Authentication cookie (sb-10-auth-token)
* @param {Object} options - Additional axios options
* @returns {Promise<Object>} API response
*/
async authenticatedRequest(endpoint, cookie, options = {}) {
try {
const response = await this.client.request({
url: endpoint,
...options,
headers: {
...options.headers,
'Cookie': cookie,
'Accept': '*/*',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7',
'Referer': 'https://www.mytischtennis.de/',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin'
}
});
return {
success: true,
data: response.data
};
} catch (error) {
console.error('MyTischtennis API error:', error.message);
return {
success: false,
error: error.response?.data?.message || 'API-Anfrage fehlgeschlagen',
status: error.response?.status || 500
};
}
}
/**
* Get user profile and club information
* @param {string} cookie - Authentication cookie (sb-10-auth-token)
* @returns {Promise<Object>} User profile with club info
*/
async getUserProfile(cookie) {
console.log('[getUserProfile] - Calling /?_data=root with cookie:', cookie?.substring(0, 50) + '...');
const result = await this.authenticatedRequest('/?_data=root', cookie, {
method: 'GET'
});
console.log('[getUserProfile] - Result success:', result.success);
if (result.success) {
console.log('[getUserProfile] - Response structure:', {
hasUserProfile: !!result.data?.userProfile,
hasClub: !!result.data?.userProfile?.club,
hasOrganization: !!result.data?.userProfile?.organization,
clubnr: result.data?.userProfile?.club?.clubnr,
clubName: result.data?.userProfile?.club?.name,
orgShort: result.data?.userProfile?.organization?.short,
ttr: result.data?.userProfile?.ttr,
qttr: result.data?.userProfile?.qttr
});
console.log('[getUserProfile] - Full userProfile.club:', result.data?.userProfile?.club);
console.log('[getUserProfile] - Full userProfile.organization:', result.data?.userProfile?.organization);
return {
success: true,
clubId: result.data?.userProfile?.club?.clubnr || null,
clubName: result.data?.userProfile?.club?.name || null,
fedNickname: result.data?.userProfile?.organization?.short || null,
ttr: result.data?.userProfile?.ttr || null,
qttr: result.data?.userProfile?.qttr || null,
userProfile: result.data?.userProfile || null
};
}
console.error('[getUserProfile] - Failed:', result.error);
return result;
}
/**
* Get club rankings (andro-Rangliste)
* @param {string} cookie - Authentication cookie
* @param {string} clubId - Club number (e.g., "43030")
* @param {string} fedNickname - Federation nickname (e.g., "HeTTV")
* @returns {Promise<Object>} Rankings with player entries (all pages)
*/
async getClubRankings(cookie, clubId, fedNickname) {
const allEntries = [];
let currentPage = 0;
let hasMorePages = true;
console.log('[getClubRankings] - Starting to fetch rankings for club', clubId);
while (hasMorePages) {
const endpoint = `/rankings/andro-rangliste?all-players=on&clubnr=${clubId}&fednickname=${fedNickname}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
console.log(`[getClubRankings] - Fetching page ${currentPage}...`);
const result = await this.authenticatedRequest(endpoint, cookie, {
method: 'GET'
});
if (!result.success) {
console.error(`[getClubRankings] - Failed to fetch page ${currentPage}:`, result.error);
return result;
}
// Find the dynamic key that contains entries
const blockLoaderData = result.data?.pageContent?.blockLoaderData;
if (!blockLoaderData) {
console.error('[getClubRankings] - No blockLoaderData found');
return {
success: false,
error: 'Keine blockLoaderData gefunden'
};
}
// Finde den Schlüssel, der entries enthält
let entries = null;
let rankingData = null;
for (const key in blockLoaderData) {
if (blockLoaderData[key]?.entries) {
entries = blockLoaderData[key].entries;
rankingData = blockLoaderData[key];
break;
}
}
if (!entries) {
console.error('[getClubRankings] - No entries found in blockLoaderData');
return {
success: false,
error: 'Keine entries in blockLoaderData gefunden'
};
}
console.log(`[getClubRankings] - Page ${currentPage}: Found ${entries.length} entries`);
// Füge Entries hinzu
allEntries.push(...entries);
// Prüfe ob es weitere Seiten gibt
// Wenn die aktuelle Seite weniger Einträge hat als das Limit, sind wir am Ende
// Oder wenn wir alle erwarteten Einträge haben
if (entries.length === 0) {
hasMorePages = false;
console.log('[getClubRankings] - No more entries, stopping');
} else if (rankingData.numberOfPages && currentPage >= rankingData.numberOfPages - 1) {
hasMorePages = false;
console.log(`[getClubRankings] - Reached last page (${rankingData.numberOfPages})`);
} else if (allEntries.length >= rankingData.resultLength) {
hasMorePages = false;
console.log(`[getClubRankings] - Got all entries (${allEntries.length}/${rankingData.resultLength})`);
} else {
currentPage++;
}
}
console.log(`[getClubRankings] - Total entries fetched: ${allEntries.length}`);
return {
success: true,
entries: allEntries,
metadata: {
totalEntries: allEntries.length,
pagesFetched: currentPage + 1
}
};
}
}
export default new MyTischtennisClient();

View File

@@ -1,77 +1,76 @@
import ClubService from '../services/clubService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getClubs = async (req, res) => {
try {
console.log('[getClubs] - get clubs');
devLog('[getClubs] - get clubs');
const clubs = await ClubService.getAllClubs();
console.log('[getClubs] - prepare response');
devLog('[getClubs] - prepare response');
res.status(200).json(clubs);
console.log('[getClubs] - done');
devLog('[getClubs] - done');
} catch (error) {
console.log('[getClubs] - error');
console.log(error);
console.error('[getClubs] - error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const addClub = async (req, res) => {
console.log('[addClub] - Read out parameters');
devLog('[addClub] - Read out parameters');
const { authcode: token } = req.headers;
const { name: clubName } = req.body;
try {
console.log('[addClub] - find club by name');
devLog('[addClub] - find club by name');
const club = await ClubService.findClubByName(clubName);
console.log('[addClub] - get user');
devLog('[addClub] - get user');
const user = await getUserByToken(token);
console.log('[addClub] - check if club already exists');
devLog('[addClub] - check if club already exists');
if (club) {
res.status(409).json({ error: "alreadyexists" });
return;
}
console.log('[addClub] - create club');
devLog('[addClub] - create club');
const newClub = await ClubService.createClub(clubName);
console.log('[addClub] - add user to new club');
devLog('[addClub] - add user to new club');
await ClubService.addUserToClub(user.id, newClub.id);
console.log('[addClub] - prepare response');
devLog('[addClub] - prepare response');
res.status(200).json(newClub);
console.log('[addClub] - done');
devLog('[addClub] - done');
} catch (error) {
console.log('[addClub] - error');
console.log(error);
console.error('[addClub] - error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getClub = async (req, res) => {
console.log('[getClub] - start');
devLog('[getClub] - start');
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
console.log('[getClub] - get user');
devLog('[getClub] - get user');
const user = await getUserByToken(token);
console.log('[getClub] - get users club');
devLog('[getClub] - get users club');
const access = await ClubService.getUserClubAccess(user.id, clubId);
console.log('[getClub] - check access');
devLog('[getClub] - check access');
if (access.length === 0 || !access[0].approved) {
res.status(403).json({ error: "noaccess", status: access.length === 0 ? "notrequested" : "requested" });
return;
}
console.log('[getClub] - get club');
devLog('[getClub] - get club');
const club = await ClubService.findClubById(clubId);
console.log('[getClub] - check club exists');
devLog('[getClub] - check club exists');
if (!club) {
return res.status(404).json({ message: 'Club not found' });
}
console.log('[getClub] - set response');
devLog('[getClub] - set response');
res.status(200).json(club);
console.log('[getClub] - done');
devLog('[getClub] - done');
} catch (error) {
console.log(error);
console.error('[getClub] - error:', error);
res.status(500).json({ message: 'Server error' });
}
};
@@ -82,7 +81,7 @@ export const requestClubAccess = async (req, res) => {
try {
const user = await getUserByToken(token);
console.log(user);
devLog('[requestClubAccess] - user:', user);
await ClubService.requestAccessToClub(user.id, clubId);
res.status(200).json({});
@@ -92,6 +91,7 @@ export const requestClubAccess = async (req, res) => {
} else if (error.message === 'clubnotfound') {
res.status(404).json({ err: "clubnotfound" });
} else {
console.error('[requestClubAccess] - error:', error);
res.status(500).json({ err: "internalerror" });
}
}

View File

@@ -1,6 +1,7 @@
import diaryService from '../services/diaryService.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
const getDatesForClub = async (req, res) => {
try {
const { clubId } = req.params;
@@ -38,7 +39,7 @@ const updateTrainingTimes = async (req, res) => {
const { authcode: userToken } = req.headers;
const { dateId, trainingStart, trainingEnd } = req.body;
if (!dateId || !trainingStart) {
console.log(dateId, trainingStart, trainingEnd);
devLog(dateId, trainingStart, trainingEnd);
throw new HttpError('notallfieldsfilled', 400);
}
const updatedDate = await diaryService.updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd);
@@ -116,3 +117,15 @@ const deleteTagFromDiaryDate = async (req, res) => {
export { getDatesForClub, createDateForClub, updateTrainingTimes, addDiaryNote, deleteDiaryNote, addDiaryTag,
addTagToDiaryDate, deleteTagFromDiaryDate };
export const deleteDateForClub = async (req, res) => {
try {
const { clubId, dateId } = req.params;
const { authcode: userToken } = req.headers;
const result = await diaryService.removeDateForClub(userToken, clubId, dateId);
res.status(200).json(result);
} catch (error) {
console.error('[deleteDateForClub] - Error:', error);
res.status(error.statusCode || 500).json({ error: error.message || 'systemerror' });
}
};

View File

@@ -1,5 +1,6 @@
import diaryDateActivityService from '../services/diaryDateActivityService.js';
import { devLog } from '../utils/logger.js';
export const createDiaryDateActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
@@ -15,7 +16,7 @@ export const createDiaryDateActivity = async (req, res) => {
});
res.status(201).json(activityItem);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error creating activity' });
}
};
@@ -58,7 +59,7 @@ export const updateDiaryDateActivityOrder = async (req, res) => {
const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId);
res.status(200).json(updatedActivity);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error updating activity order' });
}
};
@@ -70,7 +71,7 @@ export const getDiaryDateActivities = async (req, res) => {
const activities = await diaryDateActivityService.getActivities(userToken, clubId, diaryDateId);
res.status(200).json(activities);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error getting activities' });
}
}
@@ -82,7 +83,7 @@ export const addGroupActivity = async(req, res) => {
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity);
res.status(201).json(activityItem);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error adding group activity' });
}
}

View File

@@ -1,7 +1,8 @@
import diaryDateTagService from "../services/diaryDateTagService.js"
import { devLog } from '../utils/logger.js';
export const getDiaryDateMemberTags = async (req, res) => {
console.log("getDiaryDateMemberTags");
devLog("getDiaryDateMemberTags");
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
@@ -14,7 +15,7 @@ export const getDiaryDateMemberTags = async (req, res) => {
}
export const addDiaryDateTag = async (req, res) => {
console.log("addDiaryDateTag");
devLog("addDiaryDateTag");
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;

View File

@@ -0,0 +1,52 @@
import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
import Participant from '../models/Participant.js';
import { checkAccess } from '../utils/userUtils.js';
export const getMembersForActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId } = req.params;
await checkAccess(userToken, clubId);
const list = await DiaryMemberActivity.findAll({ where: { diaryDateActivityId } });
res.status(200).json(list);
} catch (e) {
res.status(500).json({ error: 'Error fetching members for activity' });
}
};
export const addMembersToActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId } = req.params;
const { participantIds } = req.body; // array of participant ids
await checkAccess(userToken, clubId);
const validParticipants = await Participant.findAll({ where: { id: participantIds } });
const validIds = new Set(validParticipants.map(p => p.id));
const created = [];
for (const pid of participantIds) {
if (!validIds.has(pid)) continue;
const existing = await DiaryMemberActivity.findOne({ where: { diaryDateActivityId, participantId: pid } });
if (!existing) {
const rec = await DiaryMemberActivity.create({ diaryDateActivityId, participantId: pid });
created.push(rec);
}
}
res.status(201).json(created);
} catch (e) {
res.status(500).json({ error: 'Error adding members to activity' });
}
};
export const removeMemberFromActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId, participantId } = req.params;
await checkAccess(userToken, clubId);
await DiaryMemberActivity.destroy({ where: { diaryDateActivityId, participantId } });
res.status(200).json({ ok: true });
} catch (e) {
res.status(500).json({ error: 'Error removing member from activity' });
}
};

View File

@@ -1,11 +1,12 @@
import DiaryMemberService from '../services/diaryMemberService.js';
import { devLog } from '../utils/logger.js';
const getMemberTags = async (req, res) => {
try {
const { diaryDateId, memberId } = req.query;
const { clubId } = req.params;
const { authcode: userToken } = req.headers;
console.log(diaryDateId, memberId, clubId);
devLog(diaryDateId, memberId, clubId);
const tags = await DiaryMemberService.getTagsForMemberAndDate(userToken, clubId, diaryDateId, memberId);
res.status(200).json(tags);
} catch (error) {
@@ -19,7 +20,7 @@ const getMemberNotes = async (req, res) => {
const { diaryDateId, memberId } = req.query;
const { clubId } = req.params;
const { authcode: userToken } = req.headers;
console.log('---------->', userToken, clubId);
devLog('---------->', userToken, clubId);
const notes = await DiaryMemberService.getNotesForMember(userToken, clubId, diaryDateId, memberId);
res.status(200).json(notes);
} catch (error) {

View File

@@ -1,5 +1,6 @@
import { DiaryTag, DiaryDateTag } from '../models/index.js';
import { devLog } from '../utils/logger.js';
export const getTags = async (req, res) => {
try {
const tags = await DiaryTag.findAll();
@@ -12,11 +13,11 @@ export const getTags = async (req, res) => {
export const createTag = async (req, res) => {
try {
const { name } = req.body;
console.log(name);
devLog(name);
const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
res.status(201).json(newTag);
} catch (error) {
console.log('[createTag] - Error:', error);
devLog('[createTag] - Error:', error);
res.status(500).json({ error: 'Error creating tag' });
}
};

View File

@@ -0,0 +1,172 @@
import externalServiceService from '../services/externalServiceService.js';
import HttpError from '../exceptions/HttpError.js';
class ExternalServiceController {
/**
* GET /api/mytischtennis/account?service=mytischtennis
* Get current user's external service account
*/
async getAccount(req, res, next) {
try {
const userId = req.user.id;
const service = req.query.service || 'mytischtennis';
const account = await externalServiceService.getAccount(userId, service);
if (!account) {
return res.status(200).json({ account: null });
}
res.status(200).json({ account });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/status?service=mytischtennis
* Check account configuration status
*/
async getStatus(req, res, next) {
try {
const userId = req.user.id;
const service = req.query.service || 'mytischtennis';
const status = await externalServiceService.checkAccountStatus(userId, service);
res.status(200).json(status);
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/account
* Create or update external service account
*/
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, userPassword, service = 'mytischtennis' } = req.body;
if (!email) {
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
}
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
if (password && !userPassword) {
throw new HttpError(400, 'App-Passwort erforderlich zum Setzen des myTischtennis-Passworts');
}
const account = await externalServiceService.upsertAccount(
userId,
email,
password,
savePassword || false,
userPassword,
service
);
res.status(200).json({
message: `${service}-Account erfolgreich gespeichert`,
account
});
} catch (error) {
next(error);
}
}
/**
* DELETE /api/mytischtennis/account?service=mytischtennis
* Delete external service account
*/
async deleteAccount(req, res, next) {
try {
const userId = req.user.id;
const service = req.query.service || 'mytischtennis';
const deleted = await externalServiceService.deleteAccount(userId, service);
if (!deleted) {
throw new HttpError(404, `Kein ${service}-Account gefunden`);
}
res.status(200).json({ message: `${service}-Account gelöscht` });
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/verify
* Verify login credentials
*/
async verifyLogin(req, res, next) {
try {
const userId = req.user.id;
const { password, service = 'mytischtennis' } = req.body;
const result = await externalServiceService.verifyLogin(userId, password, service);
res.status(200).json({
message: 'Login erfolgreich',
success: true,
accessToken: result.accessToken,
expiresAt: result.expiresAt,
clubId: result.clubId,
clubName: result.clubName
});
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/session?service=mytischtennis
* Get stored session data for authenticated requests
*/
async getSession(req, res, next) {
try {
const userId = req.user.id;
const service = req.query.service || 'mytischtennis';
const session = await externalServiceService.getSession(userId, service);
res.status(200).json({ session });
} catch (error) {
next(error);
}
}
/**
* GET /api/external-service/hettv/main-page
* Load HeTTV main page and find download links
*/
async loadHettvMainPage(req, res, next) {
try {
const userId = req.user.id;
const result = await externalServiceService.loadHettvMainPage(userId);
res.status(200).json(result);
} catch (error) {
next(error);
}
}
/**
* POST /api/external-service/hettv/download-page
* Load specific HeTTV download page
*/
async loadHettvDownloadPage(req, res, next) {
try {
const userId = req.user.id;
const { downloadUrl } = req.body;
if (!downloadUrl) {
throw new HttpError(400, 'Download-URL ist erforderlich');
}
const result = await externalServiceService.loadHettvDownloadPage(userId, downloadUrl);
res.status(200).json(result);
} catch (error) {
next(error);
}
}
}
export default new ExternalServiceController();

View File

@@ -1,6 +1,7 @@
import HttpError from '../exceptions/HttpError.js';
import groupService from '../services/groupService.js';
import { devLog } from '../utils/logger.js';
const addGroup = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
@@ -9,7 +10,7 @@ const addGroup = async(req, res) => {
res.status(201).json(result);
} catch (error) {
console.error('[addGroup] - Error:', error);
console.log(req.params, req.headers, req.body)
devLog(req.params, req.headers, req.body)
res.status(error.statusCode || 500).json({ error: error.message });
}
}

View File

@@ -1,6 +1,7 @@
import MatchService from '../services/matchService.js';
import fs from 'fs';
import { devLog } from '../utils/logger.js';
export const uploadCSV = async (req, res) => {
try {
const { clubId } = req.body;
@@ -21,7 +22,7 @@ export const uploadCSV = async (req, res) => {
export const getLeaguesForCurrentSeason = async (req, res) => {
try {
console.log(req.headers, req.params);
devLog(req.headers, req.params);
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId);

View File

@@ -1,5 +1,6 @@
import MemberService from "../services/memberService.js";
import { devLog } from '../utils/logger.js';
const getClubMembers = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
@@ -9,24 +10,24 @@ const getClubMembers = async(req, res) => {
}
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
} catch(error) {
console.log('[getClubMembers] - Error: ', error);
devLog('[getClubMembers] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
}
const getWaitingApprovals = async(req, res) => {
try {
console.log('[getWaitingApprovals] - Start');
devLog('[getWaitingApprovals] - Start');
const { id: clubId } = req.params;
console.log('[getWaitingApprovals] - get token');
devLog('[getWaitingApprovals] - get token');
const { authcode: userToken } = req.headers;
console.log('[getWaitingApprovals] - load for waiting approvals');
devLog('[getWaitingApprovals] - load for waiting approvals');
const waitingApprovals = await MemberService.getApprovalRequests(userToken, clubId);
console.log('[getWaitingApprovals] - set response');
devLog('[getWaitingApprovals] - set response');
res.status(200).json(waitingApprovals);
console.log('[getWaitingApprovals] - done');
devLog('[getWaitingApprovals] - done');
} catch(error) {
console.log('[getWaitingApprovals] - Error: ', error);
devLog('[getWaitingApprovals] - Error: ', error);
res.status(403).json({ error: error });
}
}
@@ -34,11 +35,11 @@ const getWaitingApprovals = async(req, res) => {
const setClubMembers = async (req, res) => {
try {
const { id: memberId, firstname: firstName, lastname: lastName, street, city, birthdate, phone, email, active,
testMembership, picsInInternetAllowed } = req.body;
testMembership, picsInInternetAllowed, gender, ttr, qttr } = req.body;
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate,
phone, email, active, testMembership, picsInInternetAllowed);
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr);
res.status(addResult.status || 500).json(addResult.response);
} catch (error) {
console.error('[setClubMembers] - Error:', error);
@@ -59,7 +60,7 @@ const uploadMemberImage = async (req, res) => {
};
const getMemberImage = async (req, res) => {
console.log('[getMemberImage]');
devLog('[getMemberImage]');
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
@@ -75,4 +76,17 @@ const getMemberImage = async (req, res) => {
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage };
const updateRatingsFromMyTischtennis = async (req, res) => {
devLog('[updateRatingsFromMyTischtennis]');
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.updateRatingsFromMyTischtennis(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
res.status(500).json({ error: 'Failed to update ratings' });
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis };

View File

@@ -1,15 +1,16 @@
import MemberNoteService from "../services/memberNoteService.js";
import { devLog } from '../utils/logger.js';
const getMemberNotes = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { memberId } = req.params;
const { clubId } = req.query;
console.log('[getMemberNotes]', userToken, memberId, clubId);
devLog('[getMemberNotes]', userToken, memberId, clubId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
console.log('[getMemberNotes] - Error: ', error);
devLog('[getMemberNotes] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -18,12 +19,12 @@ const addMemberNote = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { memberId, content, clubId } = req.body;
console.log('[addMemberNote]', userToken, memberId, content, clubId);
devLog('[addMemberNote]', userToken, memberId, content, clubId);
await MemberNoteService.addNoteToMember(userToken, clubId, memberId, content);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(201).json(notes);
} catch (error) {
console.log('[addMemberNote] - Error: ', error);
devLog('[addMemberNote] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -33,13 +34,13 @@ const deleteMemberNote = async (req, res) => {
const { authcode: userToken } = req.headers;
const { noteId } = req.params;
const { clubId } = req.body;
console.log('[deleteMemberNote]', userToken, noteId, clubId);
devLog('[deleteMemberNote]', userToken, noteId, clubId);
const memberId = await MemberNoteService.getMemberIdForNote(noteId); // Member ID ermitteln
await MemberNoteService.deleteNoteForMember(userToken, clubId, noteId);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
console.log('[deleteMemberNote] - Error: ', error);
devLog('[deleteMemberNote] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};

View File

@@ -0,0 +1,619 @@
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pdfParse = require('pdf-parse/lib/pdf-parse.js');
import { checkAccess } from '../utils/userUtils.js';
import OfficialTournament from '../models/OfficialTournament.js';
import OfficialCompetition from '../models/OfficialCompetition.js';
import OfficialCompetitionMember from '../models/OfficialCompetitionMember.js';
import Member from '../models/Member.js';
import { Op } from 'sequelize';
// In-Memory Store (einfacher Start); später DB-Modell
const parsedTournaments = new Map(); // key: id, value: { id, clubId, rawText, parsedData }
let seq = 1;
export const uploadTournamentPdf = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
await checkAccess(userToken, clubId);
if (!req.file || !req.file.buffer) return res.status(400).json({ error: 'No pdf provided' });
const data = await pdfParse(req.file.buffer);
const parsed = parseTournamentText(data.text);
const t = await OfficialTournament.create({
clubId,
title: parsed.title || null,
eventDate: parsed.termin || null,
organizer: null,
host: null,
venues: JSON.stringify(parsed.austragungsorte || []),
competitionTypes: JSON.stringify(parsed.konkurrenztypen || []),
registrationDeadlines: JSON.stringify(parsed.meldeschluesse || []),
entryFees: JSON.stringify(parsed.entryFees || {}),
});
// competitions persistieren
for (const c of parsed.competitions || []) {
// Korrigiere Fehlzuordnung: Wenn die Zeile mit "Stichtag" fälschlich in performanceClass steht
let performanceClass = c.leistungsklasse || c.performanceClass || null;
let cutoffDate = c.stichtag || c.cutoffDate || null;
if (performanceClass && /^stichtag\b/i.test(performanceClass)) {
cutoffDate = performanceClass.replace(/^stichtag\s*:?\s*/i, '').trim();
performanceClass = null;
}
await OfficialCompetition.create({
tournamentId: t.id,
ageClassCompetition: c.altersklasseWettbewerb || c.ageClassCompetition || null,
performanceClass,
startTime: c.startzeit || c.startTime || null,
registrationDeadlineDate: c.meldeschlussDatum || c.registrationDeadlineDate || null,
registrationDeadlineOnline: c.meldeschlussOnline || c.registrationDeadlineOnline || null,
cutoffDate,
ttrRelevant: c.ttrRelevant || null,
openTo: c.offenFuer || c.openTo || null,
preliminaryRound: c.vorrunde || c.preliminaryRound || null,
finalRound: c.endrunde || c.finalRound || null,
maxParticipants: c.maxTeilnehmer || c.maxParticipants || null,
entryFee: c.startgeld || c.entryFee || null,
});
}
res.status(201).json({ id: String(t.id) });
} catch (e) {
console.error('[uploadTournamentPdf] Error:', e);
res.status(500).json({ error: 'Failed to parse pdf' });
}
};
export const getParsedTournament = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, id } = req.params;
await checkAccess(userToken, clubId);
const t = await OfficialTournament.findOne({ where: { id, clubId } });
if (!t) return res.status(404).json({ error: 'not found' });
const comps = await OfficialCompetition.findAll({ where: { tournamentId: id } });
const entries = await OfficialCompetitionMember.findAll({ where: { tournamentId: id } });
const competitions = comps.map((c) => {
const j = c.toJSON();
return {
id: j.id,
tournamentId: j.tournamentId,
ageClassCompetition: j.ageClassCompetition || null,
performanceClass: j.performanceClass || null,
startTime: j.startTime || null,
registrationDeadlineDate: j.registrationDeadlineDate || null,
registrationDeadlineOnline: j.registrationDeadlineOnline || null,
cutoffDate: j.cutoffDate || null,
ttrRelevant: j.ttrRelevant || null,
openTo: j.openTo || null,
preliminaryRound: j.preliminaryRound || null,
finalRound: j.finalRound || null,
maxParticipants: j.maxParticipants || null,
entryFee: j.entryFee || null,
// Legacy Felder zusätzlich, falls Frontend sie noch nutzt
altersklasseWettbewerb: j.ageClassCompetition || null,
leistungsklasse: j.performanceClass || null,
startzeit: j.startTime || null,
meldeschlussDatum: j.registrationDeadlineDate || null,
meldeschlussOnline: j.registrationDeadlineOnline || null,
stichtag: j.cutoffDate || null,
offenFuer: j.openTo || null,
vorrunde: j.preliminaryRound || null,
endrunde: j.finalRound || null,
maxTeilnehmer: j.maxParticipants || null,
startgeld: j.entryFee || null,
};
});
res.status(200).json({
id: String(t.id),
clubId: String(t.clubId),
parsedData: {
title: t.title,
termin: t.eventDate,
austragungsorte: JSON.parse(t.venues || '[]'),
konkurrenztypen: JSON.parse(t.competitionTypes || '[]'),
meldeschluesse: JSON.parse(t.registrationDeadlines || '[]'),
entryFees: JSON.parse(t.entryFees || '{}'),
competitions,
},
participation: entries.map(e => ({
id: e.id,
tournamentId: e.tournamentId,
competitionId: e.competitionId,
memberId: e.memberId,
wants: !!e.wants,
registered: !!e.registered,
participated: !!e.participated,
placement: e.placement || null,
})),
});
} catch (e) {
res.status(500).json({ error: 'Failed to fetch parsed tournament' });
}
};
export const upsertCompetitionMember = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, id } = req.params; // id = tournamentId
await checkAccess(userToken, clubId);
const { competitionId, memberId, wants, registered, participated, placement } = req.body;
if (!competitionId || !memberId) return res.status(400).json({ error: 'competitionId and memberId required' });
const [row] = await OfficialCompetitionMember.findOrCreate({
where: { competitionId, memberId },
defaults: {
tournamentId: id,
competitionId,
memberId,
wants: !!wants,
registered: !!registered,
participated: !!participated,
placement: placement || null,
}
});
row.wants = wants !== undefined ? !!wants : row.wants;
row.registered = registered !== undefined ? !!registered : row.registered;
row.participated = participated !== undefined ? !!participated : row.participated;
if (placement !== undefined) row.placement = placement;
await row.save();
return res.status(200).json({ success: true, id: row.id });
} catch (e) {
console.error('[upsertCompetitionMember] Error:', e);
res.status(500).json({ error: 'Failed to save participation' });
}
};
export const updateParticipantStatus = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, id } = req.params; // id = tournamentId
await checkAccess(userToken, clubId);
const { competitionId, memberId, action } = req.body;
if (!competitionId || !memberId || !action) {
return res.status(400).json({ error: 'competitionId, memberId and action required' });
}
const [row] = await OfficialCompetitionMember.findOrCreate({
where: { competitionId, memberId },
defaults: {
tournamentId: id,
competitionId,
memberId,
wants: false,
registered: false,
participated: false,
placement: null,
}
});
// Status-Update basierend auf Aktion
switch (action) {
case 'register':
// Von "möchte teilnehmen" zu "angemeldet"
row.wants = true;
row.registered = true;
row.participated = false;
break;
case 'participate':
// Von "angemeldet" zu "hat gespielt"
row.wants = true;
row.registered = true;
row.participated = true;
break;
case 'reset':
// Zurück zu "möchte teilnehmen"
row.wants = true;
row.registered = false;
row.participated = false;
break;
default:
return res.status(400).json({ error: 'Invalid action. Use: register, participate, or reset' });
}
await row.save();
return res.status(200).json({
success: true,
id: row.id,
status: {
wants: row.wants,
registered: row.registered,
participated: row.participated,
placement: row.placement
}
});
} catch (e) {
console.error('[updateParticipantStatus] Error:', e);
res.status(500).json({ error: 'Failed to update participant status' });
}
};
export const listOfficialTournaments = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
await checkAccess(userToken, clubId);
const list = await OfficialTournament.findAll({ where: { clubId } });
res.status(200).json(list);
} catch (e) {
res.status(500).json({ error: 'Failed to list tournaments' });
}
};
export const listClubParticipations = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
await checkAccess(userToken, clubId);
const tournaments = await OfficialTournament.findAll({ where: { clubId } });
if (!tournaments || tournaments.length === 0) return res.status(200).json([]);
const tournamentIds = tournaments.map(t => t.id);
const rows = await OfficialCompetitionMember.findAll({
where: { tournamentId: { [Op.in]: tournamentIds }, participated: true },
include: [
{ model: OfficialCompetition, as: 'competition', attributes: ['id', 'tournamentId', 'ageClassCompetition', 'startTime'] },
{ model: OfficialTournament, as: 'tournament', attributes: ['id', 'title', 'eventDate'] },
{ model: Member, as: 'member', attributes: ['id', 'firstName', 'lastName'] },
]
});
const parseDmy = (s) => {
if (!s) return null;
const m = String(s).match(/(\d{1,2})\.(\d{1,2})\.(\d{4})/);
if (!m) return null;
const d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
return isNaN(d.getTime()) ? null : d;
};
const fmtDmy = (d) => {
const dd = String(d.getDate()).padStart(2, '0');
const mm = String(d.getMonth() + 1).padStart(2, '0');
const yyyy = d.getFullYear();
return `${dd}.${mm}.${yyyy}`;
};
const byTournament = new Map();
for (const r of rows) {
const t = r.tournament;
const c = r.competition;
const m = r.member;
if (!t || !c || !m) continue;
if (!byTournament.has(t.id)) {
byTournament.set(t.id, {
tournamentId: String(t.id),
title: t.title || null,
startDate: null,
endDate: null,
entries: [],
_dates: [],
_eventDate: t.eventDate || null,
});
}
const bucket = byTournament.get(t.id);
const compDate = parseDmy(c.startTime || '') || null;
if (compDate) bucket._dates.push(compDate);
bucket.entries.push({
memberId: m.id,
memberName: `${m.firstName || ''} ${m.lastName || ''}`.trim(),
competitionId: c.id,
competitionName: c.ageClassCompetition || '',
placement: r.placement || null,
date: compDate ? fmtDmy(compDate) : null,
});
}
const out = [];
for (const t of tournaments) {
const bucket = byTournament.get(t.id) || {
tournamentId: String(t.id),
title: t.title || null,
startDate: null,
endDate: null,
entries: [],
_dates: [],
_eventDate: t.eventDate || null,
};
// Ableiten Start/Ende
if (bucket._dates.length) {
bucket._dates.sort((a, b) => a - b);
bucket.startDate = fmtDmy(bucket._dates[0]);
bucket.endDate = fmtDmy(bucket._dates[bucket._dates.length - 1]);
} else if (bucket._eventDate) {
const all = String(bucket._eventDate).match(/(\d{1,2}\.\d{1,2}\.\d{4})/g) || [];
if (all.length >= 1) {
const d1 = parseDmy(all[0]);
const d2 = all.length >= 2 ? parseDmy(all[1]) : d1;
if (d1) bucket.startDate = fmtDmy(d1);
if (d2) bucket.endDate = fmtDmy(d2);
}
}
// Sort entries: Mitglied, dann Konkurrenz
bucket.entries.sort((a, b) => {
const mcmp = (a.memberName || '').localeCompare(b.memberName || '', 'de', { sensitivity: 'base' });
if (mcmp !== 0) return mcmp;
return (a.competitionName || '').localeCompare(b.competitionName || '', 'de', { sensitivity: 'base' });
});
delete bucket._dates;
delete bucket._eventDate;
out.push(bucket);
}
res.status(200).json(out);
} catch (e) {
res.status(500).json({ error: 'Failed to list club participations' });
}
};
export const deleteOfficialTournament = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, id } = req.params;
await checkAccess(userToken, clubId);
const t = await OfficialTournament.findOne({ where: { id, clubId } });
if (!t) return res.status(404).json({ error: 'not found' });
await OfficialCompetition.destroy({ where: { tournamentId: id } });
await OfficialTournament.destroy({ where: { id } });
res.status(204).send();
} catch (e) {
res.status(500).json({ error: 'Failed to delete tournament' });
}
};
function parseTournamentText(text) {
const lines = text.split(/\r?\n/);
const normLines = lines.map(l => l.replace(/\s+/g, ' ').trim());
const findTitle = () => {
const idx = normLines.findIndex(l => /Kreiseinzelmeisterschaften/i.test(l));
return idx >= 0 ? normLines[idx] : null;
};
// Neue Funktion: Teilnahmegebühren pro Spielklasse extrahieren
const extractEntryFees = () => {
const entryFees = {};
// Verschiedene Patterns für Teilnahmegebühren suchen
const feePatterns = [
// Pattern 1: "Startgeld: U12: 5€, U14: 7€, U16: 10€"
/startgeld\s*:?\s*(.+)/i,
// Pattern 2: "Teilnahmegebühr: U12: 5€, U14: 7€"
/teilnahmegebühr\s*:?\s*(.+)/i,
// Pattern 3: "Gebühr: U12: 5€, U14: 7€"
/gebühr\s*:?\s*(.+)/i,
// Pattern 4: "Einschreibegebühr: U12: 5€, U14: 7€"
/einschreibegebühr\s*:?\s*(.+)/i,
// Pattern 5: "Anmeldegebühr: U12: 5€, U14: 7€"
/anmeldegebühr\s*:?\s*(.+)/i
];
for (const pattern of feePatterns) {
for (let i = 0; i < normLines.length; i++) {
const line = normLines[i];
const match = line.match(pattern);
if (match) {
const feeText = match[1];
// Extrahiere Gebühren aus dem Text
// Unterstützt verschiedene Formate:
// "U12: 5€, U14: 7€, U16: 10€"
// "U12: 5 Euro, U14: 7 Euro"
// "U12 5€, U14 7€"
// "U12: 5,00€, U14: 7,00€"
const feeMatches = feeText.matchAll(/(U\d+|AK\s*\d+)\s*:?\s*(\d+(?:[,.]\d+)?)\s*(?:€|Euro|EUR)?/gi);
for (const feeMatch of feeMatches) {
const ageClass = feeMatch[1].toUpperCase().replace(/\s+/g, '');
const amount = feeMatch[2].replace(',', '.');
const numericAmount = parseFloat(amount);
if (!isNaN(numericAmount)) {
entryFees[ageClass] = {
amount: numericAmount,
currency: '€',
rawText: feeMatch[0]
};
}
}
// Wenn wir Gebühren gefunden haben, brechen wir ab
if (Object.keys(entryFees).length > 0) {
break;
}
}
}
if (Object.keys(entryFees).length > 0) {
break;
}
}
return entryFees;
};
const extractBlockAfter = (labels, multiline = false) => {
const idx = normLines.findIndex(l => labels.some(lb => l.toLowerCase().startsWith(lb)));
if (idx === -1) return multiline ? [] : null;
const line = normLines[idx];
const afterColon = line.includes(':') ? line.split(':').slice(1).join(':').trim() : '';
if (!multiline) {
if (afterColon) return afterColon;
// sonst nächste nicht-leere Zeile
for (let i = idx + 1; i < normLines.length; i++) {
if (normLines[i]) return normLines[i];
}
return null;
}
// multiline bis zur nächsten Leerzeile oder nächsten bekannten Section
const out = [];
if (afterColon) out.push(afterColon);
for (let i = idx + 1; i < normLines.length; i++) {
const ln = normLines[i];
if (!ln) break;
if (/^(termin|austragungsort|austragungsorte|konkurrenz|konkurrenzen|konkurrenztypen|meldeschluss|altersklassen|startzeiten)/i.test(ln)) break;
out.push(ln);
}
return out;
};
const extractAllMatches = (regex) => {
const results = [];
for (const l of normLines) {
const m = l.match(regex);
if (m) results.push(m);
}
return results;
};
const title = findTitle();
const termin = extractBlockAfter(['termin', 'termin '], false);
const austragungsorte = extractBlockAfter(['austragungsort', 'austragungsorte'], true);
let konkurrenzRaw = extractBlockAfter(['konkurrenz', 'konkurrenzen', 'konkurrenztypen'], true);
if (konkurrenzRaw && !Array.isArray(konkurrenzRaw)) konkurrenzRaw = [konkurrenzRaw];
const konkurrenztypen = (konkurrenzRaw || []).flatMap(l => l.split(/[;,]/)).map(s => s.trim()).filter(Boolean);
// Meldeschlüsse mit Position und Zuordnung zu AK ermitteln
const meldeschluesseRaw = [];
for (let i = 0; i < normLines.length; i++) {
const l = normLines[i];
const m = l.match(/meldeschluss\s*:?\s*(.+)$/i);
if (m) meldeschluesseRaw.push({ line: i, value: m[1].trim() });
}
let altersRaw = extractBlockAfter(['altersklassen', 'altersklasse'], true);
if (altersRaw && !Array.isArray(altersRaw)) altersRaw = [altersRaw];
const altersklassen = (altersRaw || []).flatMap(l => l.split(/[;,]/)).map(s => s.trim()).filter(Boolean);
// Wettbewerbe/Konkurrenzen parsen (Block ab "3. Konkurrenzen")
const competitions = [];
const konkIdx = normLines.findIndex(l => /^\s*3\.?\s+Konkurrenzen/i.test(l) || /^Konkurrenzen\b/i.test(l));
// Bestimme Start-Sektionsnummer (z. B. 3 bei "3. Konkurrenzen"), fallback 3
const startSectionNum = (() => {
if (konkIdx === -1) return 3;
const m = normLines[konkIdx].match(/^\s*(\d+)\./);
return m ? parseInt(m[1], 10) : 3;
})();
const nextSectionIdx = () => {
for (let i = konkIdx + 1; i < normLines.length; i++) {
const m = normLines[i].match(/^\s*(\d+)\.\s+/);
if (m) {
const num = parseInt(m[1], 10);
if (!Number.isNaN(num) && num > startSectionNum) return i;
}
// Hinweis: Seitenfußzeilen wie "nu.Dokument ..." ignorieren wir, damit mehrseitige Blöcke nicht abbrechen
}
return normLines.length;
};
if (konkIdx !== -1) {
const endIdx = nextSectionIdx();
let i = konkIdx + 1;
while (i < endIdx) {
const line = normLines[i];
if (/^Altersklasse\/Wettbewerb\s*:/i.test(line)) {
const comp = {};
comp.altersklasseWettbewerb = line.split(':').slice(1).join(':').trim();
i++;
while (i < endIdx && !/^Altersklasse\/Wettbewerb\s*:/i.test(normLines[i])) {
const ln = normLines[i];
const m = ln.match(/^([^:]+):\s*(.*)$/);
if (m) {
const key = m[1].trim().toLowerCase();
const val = m[2].trim();
if (key.startsWith('leistungsklasse')) comp.leistungsklasse = val;
else if (key === 'startzeit') {
// Erwartet: 20.09.2025 13:30 Uhr -> wir extrahieren Datum+Zeit
const sm = val.match(/(\d{2}\.\d{2}\.\d{4})\s+(\d{1,2}:\d{2})/);
comp.startzeit = sm ? `${sm[1]} ${sm[2]}` : val;
}
else if (key.startsWith('meldeschluss datum')) comp.meldeschlussDatum = val;
else if (key.startsWith('meldeschluss online')) comp.meldeschlussOnline = val;
else if (key === 'stichtag') comp.stichtag = val;
else if (key === 'ttr-relevant') comp.ttrRelevant = val;
else if (key === 'offen für') comp.offenFuer = val;
else if (key.startsWith('austragungssys. vorrunde')) comp.vorrunde = val;
else if (key.startsWith('austragungssys. endrunde')) comp.endrunde = val;
else if (key.startsWith('max. teilnehmerzahl')) comp.maxTeilnehmer = val;
else if (key === 'startgeld') {
comp.startgeld = val;
// Versuche auch spezifische Gebühren für diese Altersklasse zu extrahieren
const ageClassMatch = comp.altersklasseWettbewerb?.match(/(U\d+|AK\s*\d+)/i);
if (ageClassMatch) {
const ageClass = ageClassMatch[1].toUpperCase().replace(/\s+/g, '');
const feeMatch = val.match(/(\d+(?:[,.]\d+)?)\s*(?:€|Euro|EUR)?/);
if (feeMatch) {
const amount = feeMatch[1].replace(',', '.');
const numericAmount = parseFloat(amount);
if (!isNaN(numericAmount)) {
comp.entryFeeDetails = {
amount: numericAmount,
currency: '€',
ageClass: ageClass
};
}
}
}
}
}
i++;
}
competitions.push(comp);
continue; // schon auf nächster Zeile
}
i++;
}
}
// Altersklassen-Positionen im Text (zur Zuordnung von Meldeschlüssen)
const akPositions = [];
for (let i = 0; i < normLines.length; i++) {
const l = normLines[i];
const m = l.match(/\b(U\d+|AK\s*\d+)\b/i);
if (m) akPositions.push({ line: i, ak: m[1].toUpperCase().replace(/\s+/g, '') });
}
const meldeschluesseByAk = {};
for (const ms of meldeschluesseRaw) {
// Nächste AK im Umkreis von 3 Zeilen suchen
let best = null;
let bestDist = Infinity;
for (const ak of akPositions) {
const dist = Math.abs(ak.line - ms.line);
if (dist < bestDist && dist <= 3) { best = ak; bestDist = dist; }
}
if (best) {
if (!meldeschluesseByAk[best.ak]) meldeschluesseByAk[best.ak] = new Set();
meldeschluesseByAk[best.ak].add(ms.value);
}
}
// Dedup global
const meldeschluesse = Array.from(new Set(meldeschluesseRaw.map(x => x.value)));
// Sets zu Arrays
const meldeschluesseByAkOut = Object.fromEntries(Object.entries(meldeschluesseByAk).map(([k,v]) => [k, Array.from(v)]));
// Vorhandene einfache Personenerkennung (optional, zu Analysezwecken)
const entries = [];
for (const l of normLines) {
const m = l.match(/^([A-Za-zÄÖÜäöüß\-\s']{3,})(?:\s+\((m|w|d)\))?$/i);
if (m && /\s/.test(m[1])) {
entries.push({ name: m[1].trim(), genderHint: m[2] || null });
}
}
// Extrahiere Teilnahmegebühren
const entryFees = extractEntryFees();
return {
title,
termin,
austragungsorte,
konkurrenztypen,
meldeschluesse,
meldeschluesseByAk: meldeschluesseByAkOut,
altersklassen,
startzeiten: {},
competitions,
entries,
entryFees, // Neue: Teilnahmegebühren pro Spielklasse
debug: { normLines },
};
}

View File

@@ -1,12 +1,13 @@
import Participant from '../models/Participant.js';
import { devLog } from '../utils/logger.js';
export const getParticipants = async (req, res) => {
try {
const { dateId } = req.params;
const participants = await Participant.findAll({ where: { diaryDateId: dateId } });
res.status(200).json(participants);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Abrufen der Teilnehmer' });
}
};
@@ -17,7 +18,7 @@ export const addParticipant = async (req, res) => {
const participant = await Participant.create({ diaryDateId, memberId });
res.status(201).json(participant);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Hinzufügen des Teilnehmers' });
}
};
@@ -28,7 +29,7 @@ export const removeParticipant = async (req, res) => {
await Participant.destroy({ where: { diaryDateId, memberId } });
res.status(200).json({ message: 'Teilnehmer entfernt' });
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Entfernen des Teilnehmers' });
}
};

View File

@@ -1,9 +1,12 @@
import predefinedActivityService from '../services/predefinedActivityService.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import path from 'path';
import fs from 'fs';
export const createPredefinedActivity = async (req, res) => {
try {
const { name, description, durationText, duration } = req.body;
const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, description, durationText, duration });
const { name, code, description, durationText, duration, imageLink, drawingData } = req.body;
const predefinedActivity = await predefinedActivityService.createPredefinedActivity({ name, code, description, durationText, duration, imageLink, drawingData });
res.status(201).json(predefinedActivity);
} catch (error) {
console.error('[createPredefinedActivity] - Error:', error);
@@ -25,10 +28,11 @@ export const getPredefinedActivityById = async (req, res) => {
try {
const { id } = req.params;
const predefinedActivity = await predefinedActivityService.getPredefinedActivityById(id);
const images = await PredefinedActivityImage.findAll({ where: { predefinedActivityId: id } });
if (!predefinedActivity) {
return res.status(404).json({ error: 'Predefined activity not found' });
}
res.status(200).json(predefinedActivity);
res.status(200).json({ ...predefinedActivity.toJSON(), images });
} catch (error) {
console.error('[getPredefinedActivityById] - Error:', error);
res.status(500).json({ error: 'Error fetching predefined activity' });
@@ -38,11 +42,43 @@ export const getPredefinedActivityById = async (req, res) => {
export const updatePredefinedActivity = async (req, res) => {
try {
const { id } = req.params;
const { name, description, durationText, duration } = req.body;
const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, description, durationText, duration });
const { name, code, description, durationText, duration, imageLink, drawingData } = req.body;
const updatedActivity = await predefinedActivityService.updatePredefinedActivity(id, { name, code, description, durationText, duration, imageLink, drawingData });
res.status(200).json(updatedActivity);
} catch (error) {
console.error('[updatePredefinedActivity] - Error:', error);
res.status(500).json({ error: 'Error updating predefined activity' });
}
};
export const searchPredefinedActivities = async (req, res) => {
try {
const { q, limit } = req.query;
const result = await predefinedActivityService.searchPredefinedActivities(q, limit);
res.status(200).json(result);
} catch (error) {
console.error('[searchPredefinedActivities] - Error:', error);
res.status(500).json({ error: 'Error searching predefined activities' });
}
};
export const mergePredefinedActivities = async (req, res) => {
try {
const { sourceId, targetId } = req.body;
await predefinedActivityService.mergeActivities(sourceId, targetId);
res.status(200).json({ ok: true });
} catch (error) {
console.error('[mergePredefinedActivities] - Error:', error);
res.status(500).json({ error: 'Error merging predefined activities' });
}
};
export const deduplicatePredefinedActivities = async (req, res) => {
try {
const result = await predefinedActivityService.deduplicateActivities();
res.status(200).json(result);
} catch (error) {
console.error('[deduplicatePredefinedActivities] - Error:', error);
res.status(500).json({ error: 'Error deduplicating predefined activities' });
}
};

View File

@@ -0,0 +1,98 @@
import PredefinedActivity from '../models/PredefinedActivity.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import { checkGlobalAccess } from '../utils/userUtils.js';
import path from 'path';
import fs from 'fs';
import sharp from 'sharp';
import { devLog } from '../utils/logger.js';
export const uploadPredefinedActivityImage = async (req, res) => {
try {
const { id } = req.params; // predefinedActivityId
const { authcode: userToken } = req.headers;
await checkGlobalAccess(userToken); // Predefined Activities sind global, keine Club-Zugriffskontrolle nötig
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
return res.status(404).json({ error: 'Predefined activity not found' });
}
if (!req.file || !req.file.buffer) {
return res.status(400).json({ error: 'No image uploaded' });
}
const imagesDir = path.join('images', 'predefined');
if (!fs.existsSync(imagesDir)) {
fs.mkdirSync(imagesDir, { recursive: true });
}
const fileName = `${id}-${Date.now()}.jpg`;
const filePath = path.join(imagesDir, fileName);
await sharp(req.file.buffer)
.resize(800, 800, { fit: 'inside' })
.jpeg({ quality: 85 })
.toFile(filePath);
// Extrahiere Zeichnungsdaten aus dem Request
const drawingData = req.body.drawingData ? JSON.parse(req.body.drawingData) : null;
devLog('[uploadPredefinedActivityImage] - drawingData:', drawingData);
const imageRecord = await PredefinedActivityImage.create({
predefinedActivityId: id,
imagePath: filePath,
mimeType: 'image/jpeg',
drawingData: drawingData ? JSON.stringify(drawingData) : null,
});
// Optional: als imageLink am Activity-Datensatz setzen
activity.imageLink = `/api/predefined-activities/${id}/image/${imageRecord.id}`;
await activity.save();
res.status(201).json({ id: imageRecord.id, imageLink: activity.imageLink });
} catch (error) {
console.error('[uploadPredefinedActivityImage] - Error:', error);
res.status(500).json({ error: 'Failed to upload image' });
}
};
export const deletePredefinedActivityImage = async (req, res) => {
try {
const { id, imageId } = req.params; // predefinedActivityId, imageId
const { authcode: userToken } = req.headers;
await checkGlobalAccess(userToken);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
return res.status(404).json({ error: 'Predefined activity not found' });
}
const image = await PredefinedActivityImage.findOne({
where: { id: imageId, predefinedActivityId: id }
});
if (!image) {
return res.status(404).json({ error: 'Image not found' });
}
// Datei vom Dateisystem löschen
if (fs.existsSync(image.imagePath)) {
fs.unlinkSync(image.imagePath);
}
// Datensatz aus der Datenbank löschen
await image.destroy();
// Falls das gelöschte Bild der aktuelle imageLink war, diesen zurücksetzen
if (activity.imageLink === `/api/predefined-activities/${id}/image/${imageId}`) {
activity.imageLink = null;
await activity.save();
}
res.status(200).json({ message: 'Image deleted successfully' });
} catch (error) {
console.error('[deletePredefinedActivityImage] - Error:', error);
res.status(500).json({ error: 'Failed to delete image' });
}
};

View File

@@ -11,10 +11,30 @@ class TrainingStatsController {
const twelveMonthsAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
// Alle aktiven Mitglieder des Vereins laden
// Alle aktiven Mitglieder des spezifischen Vereins laden
const members = await Member.findAll({
where: {
active: true
active: true,
clubId: parseInt(clubId)
}
});
// Anzahl der Trainings im jeweiligen Zeitraum berechnen
const trainingsCount12Months = await DiaryDate.count({
where: {
clubId: parseInt(clubId),
date: {
[Op.gte]: twelveMonthsAgo
}
}
});
const trainingsCount3Months = await DiaryDate.count({
where: {
clubId: parseInt(clubId),
date: {
[Op.gte]: threeMonthsAgo
}
}
});
@@ -94,6 +114,10 @@ class TrainingStatsController {
endTime: '--:--'
}));
// Letztes Training
const lastTrainingDate = trainingDetails.length ? trainingDetails[0].diaryDate.date : null;
const lastTrainingTs = lastTrainingDate ? new Date(lastTrainingDate).getTime() : 0;
stats.push({
id: member.id,
firstName: member.firstName,
@@ -102,6 +126,8 @@ class TrainingStatsController {
participation12Months,
participation3Months,
participationTotal,
lastTraining: lastTrainingDate,
lastTrainingTs,
trainingDetails: formattedTrainingDetails
});
}
@@ -109,7 +135,36 @@ class TrainingStatsController {
// Nach Gesamtteilnahme absteigend sortieren
stats.sort((a, b) => b.participationTotal - a.participationTotal);
res.json(stats);
// Trainingstage mit Teilnehmerzahlen abrufen (letzte 12 Monate, absteigend sortiert)
const trainingDays = await DiaryDate.findAll({
where: {
clubId: parseInt(clubId),
date: {
[Op.gte]: twelveMonthsAgo
}
},
include: [{
model: Participant,
as: 'participantList',
attributes: ['id']
}],
order: [['date', 'DESC']]
});
// Formatiere Trainingstage mit Teilnehmerzahl
const formattedTrainingDays = trainingDays.map(day => ({
id: day.id,
date: day.date,
participantCount: day.participantList ? day.participantList.length : 0
}));
// Zusätzliche Metadaten mit Trainingsanzahl zurückgeben
res.json({
members: stats,
trainingsCount12Months,
trainingsCount3Months,
trainingDays: formattedTrainingDays
});
} catch (error) {
console.error('Fehler beim Laden der Trainings-Statistik:', error);

View File

@@ -1,14 +1,16 @@
import { Sequelize } from 'sequelize';
import { development } from './config.js';
const sequelize = new Sequelize(
development.database,
development.database,
development.username,
development.password,
{
host: development.host,
dialect: development.dialect,
define: development.define,
logging: false, // SQL-Logging deaktivieren
}
);

View File

@@ -0,0 +1,11 @@
-- Migration: Add drawing_data column to predefined_activity_images table
-- Date: 2025-09-22
-- Description: Adds drawing_data column to store Court Drawing Tool metadata
ALTER TABLE `predefined_activity_images`
ADD COLUMN `drawing_data` TEXT NULL
COMMENT 'JSON string containing drawing metadata for Court Drawing Tool'
AFTER `mime_type`;
-- Verify the column was added
DESCRIBE `predefined_activity_images`;

View File

@@ -0,0 +1,26 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const DiaryMemberActivity = sequelize.define('DiaryMemberActivity', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
diaryDateActivityId: {
type: DataTypes.INTEGER,
allowNull: false,
},
participantId: {
type: DataTypes.INTEGER,
allowNull: false,
},
}, {
tableName: 'diary_member_activities',
timestamps: true,
underscored: true,
});
export default DiaryMemberActivity;

View File

@@ -0,0 +1,132 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
service: {
type: DataTypes.STRING(50),
allowNull: false,
defaultValue: 'mytischtennis'
},
email: {
type: DataTypes.STRING,
allowNull: false,
},
encryptedPassword: {
type: DataTypes.TEXT,
allowNull: true,
field: 'encrypted_password'
},
savePassword: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
field: 'save_password'
},
accessToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'access_token'
},
refreshToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'refresh_token'
},
expiresAt: {
type: DataTypes.BIGINT,
allowNull: true,
field: 'expires_at'
},
cookie: {
type: DataTypes.TEXT,
allowNull: true
},
userData: {
type: DataTypes.JSON,
allowNull: true,
field: 'user_data'
},
clubId: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_id'
},
clubName: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_name'
},
fedNickname: {
type: DataTypes.STRING,
allowNull: true,
field: 'fed_nickname'
},
lastLoginAttempt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_attempt'
},
lastLoginSuccess: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_success'
}
}, {
underscored: true,
tableName: 'external_service_account',
timestamps: true,
indexes: [
{
unique: true,
fields: ['user_id', 'service']
}
],
hooks: {
beforeSave: async (instance) => {
// Wenn savePassword false ist, password auf null setzen
if (!instance.savePassword) {
instance.encryptedPassword = null;
}
}
}
});
// Virtuelle Felder für password handling
ExternalServiceAccount.prototype.setPassword = function(password) {
if (password && this.savePassword) {
this.encryptedPassword = encryptData(password);
} else {
this.encryptedPassword = null;
}
};
ExternalServiceAccount.prototype.getPassword = function() {
if (this.encryptedPassword) {
try {
return decryptData(this.encryptedPassword);
} catch (error) {
console.error('Error decrypting password:', error);
return null;
}
}
return null;
};
export default ExternalServiceAccount;

View File

@@ -122,6 +122,22 @@ const Member = sequelize.define('Member', {
allowNull: false,
default: false,
}
,
gender: {
type: DataTypes.ENUM('male','female','diverse','unknown'),
allowNull: true,
defaultValue: 'unknown'
},
ttr: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null
},
qttr: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null
}
}, {
underscored: true,
sequelize,

View File

@@ -0,0 +1,132 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
service: {
type: DataTypes.STRING(50),
allowNull: false,
defaultValue: 'mytischtennis'
},
email: {
type: DataTypes.STRING,
allowNull: false,
},
encryptedPassword: {
type: DataTypes.TEXT,
allowNull: true,
field: 'encrypted_password'
},
savePassword: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
field: 'save_password'
},
accessToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'access_token'
},
refreshToken: {
type: DataTypes.TEXT,
allowNull: true,
field: 'refresh_token'
},
expiresAt: {
type: DataTypes.BIGINT,
allowNull: true,
field: 'expires_at'
},
cookie: {
type: DataTypes.TEXT,
allowNull: true
},
userData: {
type: DataTypes.JSON,
allowNull: true,
field: 'user_data'
},
clubId: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_id'
},
clubName: {
type: DataTypes.STRING,
allowNull: true,
field: 'club_name'
},
fedNickname: {
type: DataTypes.STRING,
allowNull: true,
field: 'fed_nickname'
},
lastLoginAttempt: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_attempt'
},
lastLoginSuccess: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_login_success'
}
}, {
underscored: true,
tableName: 'external_service_account',
timestamps: true,
indexes: [
{
unique: true,
fields: ['user_id', 'service']
}
],
hooks: {
beforeSave: async (instance) => {
// Wenn savePassword false ist, password auf null setzen
if (!instance.savePassword) {
instance.encryptedPassword = null;
}
}
}
});
// Virtuelle Felder für password handling
ExternalServiceAccount.prototype.setPassword = function(password) {
if (password && this.savePassword) {
this.encryptedPassword = encryptData(password);
} else {
this.encryptedPassword = null;
}
};
ExternalServiceAccount.prototype.getPassword = function() {
if (this.encryptedPassword) {
try {
return decryptData(this.encryptedPassword);
} catch (error) {
console.error('Error decrypting password:', error);
return null;
}
}
return null;
};
export default ExternalServiceAccount;

View File

@@ -0,0 +1,28 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const OfficialCompetition = sequelize.define('OfficialCompetition', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
tournamentId: { type: DataTypes.INTEGER, allowNull: false },
// Englische Attributnamen, gemappt auf bestehende DB-Spalten
ageClassCompetition: { type: DataTypes.STRING, allowNull: true, field: 'age_class_competition' },
performanceClass: { type: DataTypes.STRING, allowNull: true, field: 'performance_class' },
startTime: { type: DataTypes.STRING, allowNull: true, field: 'start_time' },
registrationDeadlineDate: { type: DataTypes.STRING, allowNull: true, field: 'registration_deadline_date' },
registrationDeadlineOnline: { type: DataTypes.STRING, allowNull: true, field: 'registration_deadline_online' },
cutoffDate: { type: DataTypes.STRING, allowNull: true, field: 'cutoff_date' },
ttrRelevant: { type: DataTypes.STRING, allowNull: true },
openTo: { type: DataTypes.STRING, allowNull: true, field: 'open_to' },
preliminaryRound: { type: DataTypes.STRING, allowNull: true, field: 'preliminary_round' },
finalRound: { type: DataTypes.STRING, allowNull: true, field: 'final_round' },
maxParticipants: { type: DataTypes.STRING, allowNull: true, field: 'max_participants' },
entryFee: { type: DataTypes.STRING, allowNull: true, field: 'entry_fee' },
}, {
tableName: 'official_competitions',
timestamps: true,
underscored: true,
});
export default OfficialCompetition;

View File

@@ -0,0 +1,25 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const OfficialCompetitionMember = sequelize.define('OfficialCompetitionMember', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
tournamentId: { type: DataTypes.INTEGER, allowNull: false },
competitionId: { type: DataTypes.INTEGER, allowNull: false },
memberId: { type: DataTypes.INTEGER, allowNull: false },
wants: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
registered: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
participated: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false },
placement: { type: DataTypes.STRING, allowNull: true },
}, {
tableName: 'official_competition_members',
timestamps: true,
underscored: true,
indexes: [
{ unique: true, fields: ['competition_id', 'member_id'] },
{ fields: ['tournament_id'] },
],
});
export default OfficialCompetitionMember;

View File

@@ -0,0 +1,23 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const OfficialTournament = sequelize.define('OfficialTournament', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
clubId: { type: DataTypes.INTEGER, allowNull: false },
title: { type: DataTypes.STRING, allowNull: true },
eventDate: { type: DataTypes.STRING, allowNull: true },
organizer: { type: DataTypes.STRING, allowNull: true },
host: { type: DataTypes.STRING, allowNull: true },
venues: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
competitionTypes: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
registrationDeadlines: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Array)
entryFees: { type: DataTypes.TEXT, allowNull: true }, // JSON.stringify(Object) - Teilnahmegebühren pro Spielklasse
}, {
tableName: 'official_tournaments',
timestamps: true,
underscored: true,
});
export default OfficialTournament;

View File

@@ -11,10 +11,19 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', {
type: DataTypes.STRING,
allowNull: false,
},
code: {
type: DataTypes.STRING,
allowNull: true,
},
description: {
type: DataTypes.TEXT,
allowNull: true,
},
drawingData: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'JSON string with metadata for Court Drawing Tool'
},
durationText: {
type: DataTypes.STRING,
allowNull: true,
@@ -23,6 +32,10 @@ const PredefinedActivity = sequelize.define('PredefinedActivity', {
type: DataTypes.INTEGER,
allowNull: true,
},
imageLink: {
type: DataTypes.STRING,
allowNull: true,
},
}, {
tableName: 'predefined_activities',
timestamps: true,

View File

@@ -0,0 +1,35 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const PredefinedActivityImage = sequelize.define('PredefinedActivityImage', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
predefinedActivityId: {
type: DataTypes.INTEGER,
allowNull: false,
},
imagePath: {
type: DataTypes.STRING,
allowNull: false,
},
mimeType: {
type: DataTypes.STRING,
allowNull: true,
},
drawingData: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'JSON string containing drawing metadata for Court Drawing Tool'
},
}, {
tableName: 'predefined_activity_images',
timestamps: true,
underscored: true,
});
export default PredefinedActivityImage;

View File

@@ -13,6 +13,8 @@ import DiaryDateTag from './DiaryDateTag.js';
import DiaryMemberNote from './DiaryMemberNote.js';
import DiaryMemberTag from './DiaryMemberTag.js';
import PredefinedActivity from './PredefinedActivity.js';
import DiaryMemberActivity from './DiaryMemberActivity.js';
import PredefinedActivityImage from './PredefinedActivityImage.js';
import DiaryDateActivity from './DiaryDateActivity.js';
import Match from './Match.js';
import League from './League.js';
@@ -28,6 +30,20 @@ import TournamentMatch from './TournamentMatch.js';
import TournamentResult from './TournamentResult.js';
import Accident from './Accident.js';
import UserToken from './UserToken.js';
import OfficialTournament from './OfficialTournament.js';
import OfficialCompetition from './OfficialCompetition.js';
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
import ExternalServiceAccount from './ExternalServiceAccount.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
// Official competition participations
OfficialCompetition.hasMany(OfficialCompetitionMember, { foreignKey: 'competitionId', as: 'members' });
OfficialCompetitionMember.belongsTo(OfficialCompetition, { foreignKey: 'competitionId', as: 'competition' });
OfficialTournament.hasMany(OfficialCompetitionMember, { foreignKey: 'tournamentId', as: 'competitionMembers' });
OfficialCompetitionMember.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
Member.hasMany(OfficialCompetitionMember, { foreignKey: 'memberId', as: 'officialCompetitionEntries' });
OfficialCompetitionMember.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
User.hasMany(Log, { foreignKey: 'userId' });
Log.belongsTo(User, { foreignKey: 'userId' });
@@ -76,6 +92,14 @@ DiaryDateActivity.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDa
PredefinedActivity.hasMany(DiaryDateActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivities' });
DiaryDateActivity.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' });
// DiaryMemberActivity links a Participant to a DiaryDateActivity
DiaryMemberActivity.belongsTo(DiaryDateActivity, { foreignKey: 'diaryDateActivityId', as: 'activity' });
DiaryDateActivity.hasMany(DiaryMemberActivity, { foreignKey: 'diaryDateActivityId', as: 'activityMembers' });
DiaryMemberActivity.belongsTo(Participant, { foreignKey: 'participantId', as: 'participant' });
Participant.hasMany(DiaryMemberActivity, { foreignKey: 'participantId', as: 'memberActivities' });
// PredefinedActivity Images
PredefinedActivity.hasMany(PredefinedActivityImage, { foreignKey: 'predefinedActivityId', as: 'images' });
PredefinedActivityImage.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' });
Club.hasMany(Match, { foreignKey: 'clubId', as: 'matches' });
Match.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
@@ -181,6 +205,9 @@ Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
Accident.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' });
User.hasMany(ExternalServiceAccount, { foreignKey: 'userId', as: 'externalServiceAccounts' });
ExternalServiceAccount.belongsTo(User, { foreignKey: 'userId', as: 'user' });
export {
User,
Log,
@@ -198,6 +225,8 @@ export {
DiaryMemberNote,
DiaryMemberTag,
PredefinedActivity,
DiaryMemberActivity,
PredefinedActivityImage,
DiaryDateActivity,
Match,
League,
@@ -211,4 +240,8 @@ export {
TournamentResult,
Accident,
UserToken,
OfficialTournament,
OfficialCompetition,
OfficialCompetitionMember,
ExternalServiceAccount,
};

View File

@@ -4,6 +4,26 @@
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
"integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==",
"ideallyInert": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -180,6 +200,137 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"ideallyInert": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"ideallyInert": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"ideallyInert": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"ideallyInert": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"ideallyInert": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"ideallyInert": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"ideallyInert": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
@@ -196,6 +347,23 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"ideallyInert": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
@@ -212,6 +380,75 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"ideallyInert": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"ideallyInert": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"ideallyInert": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
@@ -234,6 +471,29 @@
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"ideallyInert": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
@@ -256,6 +516,66 @@
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"ideallyInert": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"ideallyInert": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"ideallyInert": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
@@ -499,6 +819,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz",
@@ -507,6 +833,17 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -573,9 +910,10 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -643,6 +981,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -772,6 +1123,18 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -848,9 +1211,10 @@
}
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -879,10 +1243,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"path-key": "^3.1.0",
@@ -913,6 +1278,22 @@
"node": ">= 10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -945,6 +1326,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -1001,6 +1391,20 @@
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1028,13 +1432,10 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
@@ -1048,6 +1449,33 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1265,16 +1693,17 @@
}
},
"node_modules/express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -1288,7 +1717,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -1303,6 +1732,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/encodeurl": {
@@ -1433,6 +1866,42 @@
"dev": true,
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1477,6 +1946,21 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"ideallyInert": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -1515,16 +1999,21 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1533,6 +2022,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1579,12 +2081,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1611,10 +2113,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -1623,11 +2125,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@@ -2071,6 +2576,15 @@
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -2302,6 +2816,12 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -2554,9 +3074,37 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/pdf-parse": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
"license": "MIT",
"dependencies": {
"debug": "^3.1.0",
"node-ensure": "^0.0.0"
},
"engines": {
"node": ">=6.8.1"
}
},
"node_modules/pdf-parse/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/pdf-parse/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/pg-connection-string": {
@@ -2604,6 +3152,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -3240,6 +3794,14 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"ideallyInert": true,
"license": "0BSD",
"optional": true
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -109,7 +109,7 @@ function expand(str, isTop) {
var isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) {
// {a},b}
if (m.post.match(/,.*\}/)) {
if (m.post.match(/,(?!,).*\}/)) {
str = m.pre + '{' + m.body + escClose + m.post;
return expand(str);
}

View File

@@ -1,7 +1,7 @@
{
"name": "brace-expansion",
"description": "Brace expansion as known from sh/bash",
"version": "1.1.11",
"version": "1.1.12",
"repository": {
"type": "git",
"url": "git://github.com/juliangruber/brace-expansion.git"
@@ -43,5 +43,8 @@
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
},
"publishConfig": {
"tag": "1.x"
}
}

View File

@@ -1,147 +0,0 @@
0.6.0 / 2023-11-06
==================
* Add `partitioned` option
0.5.0 / 2022-04-11
==================
* Add `priority` option
* Fix `expires` option to reject invalid dates
* perf: improve default decode speed
* perf: remove slow string split in parse
0.4.2 / 2022-02-02
==================
* perf: read value only when assigning in parse
* perf: remove unnecessary regexp in parse
0.4.1 / 2020-04-21
==================
* Fix `maxAge` option to reject invalid values
0.4.0 / 2019-05-15
==================
* Add `SameSite=None` support
0.3.1 / 2016-05-26
==================
* Fix `sameSite: true` to work with draft-7 clients
- `true` now sends `SameSite=Strict` instead of `SameSite`
0.3.0 / 2016-05-26
==================
* Add `sameSite` option
- Replaces `firstPartyOnly` option, never implemented by browsers
* Improve error message when `encode` is not a function
* Improve error message when `expires` is not a `Date`
0.2.4 / 2016-05-20
==================
* perf: enable strict mode
* perf: use for loop in parse
* perf: use string concatenation for serialization
0.2.3 / 2015-10-25
==================
* Fix cookie `Max-Age` to never be a floating point number
0.2.2 / 2015-09-17
==================
* Fix regression when setting empty cookie value
- Ease the new restriction, which is just basic header-level validation
* Fix typo in invalid value errors
0.2.1 / 2015-09-17
==================
* Throw on invalid values provided to `serialize`
- Ensures the resulting string is a valid HTTP header value
0.2.0 / 2015-08-13
==================
* Add `firstPartyOnly` option
* Throw better error for invalid argument to parse
* perf: hoist regular expression
0.1.5 / 2015-09-17
==================
* Fix regression when setting empty cookie value
- Ease the new restriction, which is just basic header-level validation
* Fix typo in invalid value errors
0.1.4 / 2015-09-17
==================
* Throw better error for invalid argument to parse
* Throw on invalid values provided to `serialize`
- Ensures the resulting string is a valid HTTP header value
0.1.3 / 2015-05-19
==================
* Reduce the scope of try-catch deopt
* Remove argument reassignments
0.1.2 / 2014-04-16
==================
* Remove unnecessary files from npm package
0.1.1 / 2014-02-23
==================
* Fix bad parse when cookie value contained a comma
* Fix support for `maxAge` of `0`
0.1.0 / 2013-05-01
==================
* Add `decode` option
* Add `encode` option
0.0.6 / 2013-04-08
==================
* Ignore cookie parts missing `=`
0.0.5 / 2012-10-29
==================
* Return raw cookie value if value unescape errors
0.0.4 / 2012-06-21
==================
* Use encode/decodeURIComponent for cookie encoding/decoding
- Improve server/client interoperability
0.0.3 / 2012-06-06
==================
* Only escape special characters per the cookie RFC
0.0.2 / 2012-06-01
==================
* Fix `maxAge` option to not throw error
0.0.1 / 2012-05-28
==================
* Add more tests
0.0.0 / 2012-05-28
==================
* Initial release

174
backend/node_modules/cookie/index.js generated vendored
View File

@@ -23,14 +23,66 @@ exports.serialize = serialize;
var __toString = Object.prototype.toString
/**
* RegExp to match field-content in RFC 7230 sec 3.2
* RegExp to match cookie-name in RFC 6265 sec 4.1.1
* This refers out to the obsoleted definition of token in RFC 2616 sec 2.2
* which has been replaced by the token definition in RFC 7230 appendix B.
*
* field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
* field-vchar = VCHAR / obs-text
* obs-text = %x80-FF
* cookie-name = token
* token = 1*tchar
* tchar = "!" / "#" / "$" / "%" / "&" / "'" /
* "*" / "+" / "-" / "." / "^" / "_" /
* "`" / "|" / "~" / DIGIT / ALPHA
*/
var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
var cookieNameRegExp = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
/**
* RegExp to match cookie-value in RFC 6265 sec 4.1.1
*
* cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
* cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
* ; US-ASCII characters excluding CTLs,
* ; whitespace DQUOTE, comma, semicolon,
* ; and backslash
*/
var cookieValueRegExp = /^("?)[\u0021\u0023-\u002B\u002D-\u003A\u003C-\u005B\u005D-\u007E]*\1$/;
/**
* RegExp to match domain-value in RFC 6265 sec 4.1.1
*
* domain-value = <subdomain>
* ; defined in [RFC1034], Section 3.5, as
* ; enhanced by [RFC1123], Section 2.1
* <subdomain> = <label> | <subdomain> "." <label>
* <label> = <let-dig> [ [ <ldh-str> ] <let-dig> ]
* Labels must be 63 characters or less.
* 'let-dig' not 'letter' in the first char, per RFC1123
* <ldh-str> = <let-dig-hyp> | <let-dig-hyp> <ldh-str>
* <let-dig-hyp> = <let-dig> | "-"
* <let-dig> = <letter> | <digit>
* <letter> = any one of the 52 alphabetic characters A through Z in
* upper case and a through z in lower case
* <digit> = any one of the ten digits 0 through 9
*
* Keep support for leading dot: https://github.com/jshttp/cookie/issues/173
*
* > (Note that a leading %x2E ("."), if present, is ignored even though that
* character is not permitted, but a trailing %x2E ("."), if present, will
* cause the user agent to ignore the attribute.)
*/
var domainValueRegExp = /^([.]?[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)([.][a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*$/i;
/**
* RegExp to match path-value in RFC 6265 sec 4.1.1
*
* path-value = <any CHAR except CTLs or ";">
* CHAR = %x01-7F
* ; defined in RFC 5234 appendix B.1
*/
var pathValueRegExp = /^[\u0020-\u003A\u003D-\u007E]*$/;
/**
* Parse a cookie header.
@@ -39,107 +91,128 @@ var fieldContentRegExp = /^[\u0009\u0020-\u007e\u0080-\u00ff]+$/;
* The object has the various cookies as keys(names) => values
*
* @param {string} str
* @param {object} [options]
* @param {object} [opt]
* @return {object}
* @public
*/
function parse(str, options) {
function parse(str, opt) {
if (typeof str !== 'string') {
throw new TypeError('argument str must be a string');
}
var obj = {}
var opt = options || {};
var dec = opt.decode || decode;
var obj = {};
var len = str.length;
// RFC 6265 sec 4.1.1, RFC 2616 2.2 defines a cookie name consists of one char minimum, plus '='.
if (len < 2) return obj;
var index = 0
while (index < str.length) {
var eqIdx = str.indexOf('=', index)
var dec = (opt && opt.decode) || decode;
var index = 0;
var eqIdx = 0;
var endIdx = 0;
// no more cookie pairs
if (eqIdx === -1) {
break
}
do {
eqIdx = str.indexOf('=', index);
if (eqIdx === -1) break; // No more cookie pairs.
var endIdx = str.indexOf(';', index)
endIdx = str.indexOf(';', index);
if (endIdx === -1) {
endIdx = str.length
} else if (endIdx < eqIdx) {
endIdx = len;
} else if (eqIdx > endIdx) {
// backtrack on prior semicolon
index = str.lastIndexOf(';', eqIdx - 1) + 1
continue
index = str.lastIndexOf(';', eqIdx - 1) + 1;
continue;
}
var key = str.slice(index, eqIdx).trim()
var keyStartIdx = startIndex(str, index, eqIdx);
var keyEndIdx = endIndex(str, eqIdx, keyStartIdx);
var key = str.slice(keyStartIdx, keyEndIdx);
// only assign once
if (undefined === obj[key]) {
var val = str.slice(eqIdx + 1, endIdx).trim()
if (!obj.hasOwnProperty(key)) {
var valStartIdx = startIndex(str, eqIdx + 1, endIdx);
var valEndIdx = endIndex(str, endIdx, valStartIdx);
// quoted values
if (val.charCodeAt(0) === 0x22) {
val = val.slice(1, -1)
if (str.charCodeAt(valStartIdx) === 0x22 /* " */ && str.charCodeAt(valEndIdx - 1) === 0x22 /* " */) {
valStartIdx++;
valEndIdx--;
}
var val = str.slice(valStartIdx, valEndIdx);
obj[key] = tryDecode(val, dec);
}
index = endIdx + 1
}
} while (index < len);
return obj;
}
function startIndex(str, index, max) {
do {
var code = str.charCodeAt(index);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index;
} while (++index < max);
return max;
}
function endIndex(str, index, min) {
while (index > min) {
var code = str.charCodeAt(--index);
if (code !== 0x20 /* */ && code !== 0x09 /* \t */) return index + 1;
}
return min;
}
/**
* Serialize data into a cookie header.
*
* Serialize the a name value pair into a cookie string suitable for
* http headers. An optional options object specified cookie parameters.
* Serialize a name value pair into a cookie string suitable for
* http headers. An optional options object specifies cookie parameters.
*
* serialize('foo', 'bar', { httpOnly: true })
* => "foo=bar; httpOnly"
*
* @param {string} name
* @param {string} val
* @param {object} [options]
* @param {object} [opt]
* @return {string}
* @public
*/
function serialize(name, val, options) {
var opt = options || {};
var enc = opt.encode || encode;
function serialize(name, val, opt) {
var enc = (opt && opt.encode) || encodeURIComponent;
if (typeof enc !== 'function') {
throw new TypeError('option encode is invalid');
}
if (!fieldContentRegExp.test(name)) {
if (!cookieNameRegExp.test(name)) {
throw new TypeError('argument name is invalid');
}
var value = enc(val);
if (value && !fieldContentRegExp.test(value)) {
if (!cookieValueRegExp.test(value)) {
throw new TypeError('argument val is invalid');
}
var str = name + '=' + value;
if (!opt) return str;
if (null != opt.maxAge) {
var maxAge = opt.maxAge - 0;
var maxAge = Math.floor(opt.maxAge);
if (isNaN(maxAge) || !isFinite(maxAge)) {
if (!isFinite(maxAge)) {
throw new TypeError('option maxAge is invalid')
}
str += '; Max-Age=' + Math.floor(maxAge);
str += '; Max-Age=' + maxAge;
}
if (opt.domain) {
if (!fieldContentRegExp.test(opt.domain)) {
if (!domainValueRegExp.test(opt.domain)) {
throw new TypeError('option domain is invalid');
}
@@ -147,7 +220,7 @@ function serialize(name, val, options) {
}
if (opt.path) {
if (!fieldContentRegExp.test(opt.path)) {
if (!pathValueRegExp.test(opt.path)) {
throw new TypeError('option path is invalid');
}
@@ -178,8 +251,7 @@ function serialize(name, val, options) {
if (opt.priority) {
var priority = typeof opt.priority === 'string'
? opt.priority.toLowerCase()
: opt.priority
? opt.priority.toLowerCase() : opt.priority;
switch (priority) {
case 'low':
@@ -234,17 +306,6 @@ function decode (str) {
: str
}
/**
* URL-encode value.
*
* @param {string} val
* @returns {string}
*/
function encode (val) {
return encodeURIComponent(val)
}
/**
* Determine if value is a Date.
*
@@ -253,8 +314,7 @@ function encode (val) {
*/
function isDate (val) {
return __toString.call(val) === '[object Date]' ||
val instanceof Date
return __toString.call(val) === '[object Date]';
}
/**

View File

@@ -1,7 +1,7 @@
{
"name": "cookie",
"description": "HTTP server cookie parsing and serialization",
"version": "0.6.0",
"version": "0.7.1",
"author": "Roman Shtylman <shtylman@gmail.com>",
"contributors": [
"Douglas Christopher Wilson <doug@somethingdoug.com>"
@@ -29,6 +29,7 @@
"SECURITY.md",
"index.js"
],
"main": "index.js",
"engines": {
"node": ">= 0.6"
},
@@ -38,7 +39,6 @@
"test": "mocha --reporter spec --bail --check-leaks test/",
"test-ci": "nyc --reporter=lcov --reporter=text npm test",
"test-cov": "nyc --reporter=html --reporter=text npm test",
"update-bench": "node scripts/update-benchmark.js",
"version": "node scripts/version-history.js && git add HISTORY.md"
"update-bench": "node scripts/update-benchmark.js"
}
}

View File

@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.0.1](https://github.com/ljharb/es-define-property/compare/v1.0.0...v1.0.1) - 2024-12-06
### Commits
- [types] use shared tsconfig [`954a663`](https://github.com/ljharb/es-define-property/commit/954a66360326e508a0e5daa4b07493d58f5e110e)
- [actions] split out node 10-20, and 20+ [`3a8e84b`](https://github.com/ljharb/es-define-property/commit/3a8e84b23883f26ff37b3e82ff283834228e18c6)
- [Dev Deps] update `@ljharb/eslint-config`, `@ljharb/tsconfig`, `@types/get-intrinsic`, `@types/tape`, `auto-changelog`, `gopd`, `tape` [`86ae27b`](https://github.com/ljharb/es-define-property/commit/86ae27bb8cc857b23885136fad9cbe965ae36612)
- [Refactor] avoid using `get-intrinsic` [`02480c0`](https://github.com/ljharb/es-define-property/commit/02480c0353ef6118965282977c3864aff53d98b1)
- [Tests] replace `aud` with `npm audit` [`f6093ff`](https://github.com/ljharb/es-define-property/commit/f6093ff74ab51c98015c2592cd393bd42478e773)
- [Tests] configure testling [`7139e66`](https://github.com/ljharb/es-define-property/commit/7139e66959247a56086d9977359caef27c6849e7)
- [Dev Deps] update `tape` [`b901b51`](https://github.com/ljharb/es-define-property/commit/b901b511a75e001a40ce1a59fef7d9ffcfc87482)
- [Tests] fix types in tests [`469d269`](https://github.com/ljharb/es-define-property/commit/469d269fd141b1e773ec053a9fa35843493583e0)
- [Dev Deps] add missing peer dep [`733acfb`](https://github.com/ljharb/es-define-property/commit/733acfb0c4c96edf337e470b89a25a5b3724c352)
## v1.0.0 - 2024-02-12
### Commits

View File

@@ -1,9 +1,7 @@
'use strict';
var GetIntrinsic = require('get-intrinsic');
/** @type {import('.')} */
var $defineProperty = GetIntrinsic('%Object.defineProperty%', true) || false;
var $defineProperty = Object.defineProperty || false;
if ($defineProperty) {
try {
$defineProperty({}, 'a', { value: 1 });

View File

@@ -1,6 +1,6 @@
{
"name": "es-define-property",
"version": "1.0.0",
"version": "1.0.1",
"description": "`Object.defineProperty`, but not IE 8's broken one.",
"main": "index.js",
"types": "./index.d.ts",
@@ -19,7 +19,7 @@
"pretest": "npm run lint",
"tests-only": "nyc tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>= 10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -42,29 +42,29 @@
"url": "https://github.com/ljharb/es-define-property/issues"
},
"homepage": "https://github.com/ljharb/es-define-property#readme",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"@types/get-intrinsic": "^1.2.2",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.2",
"@types/gopd": "^1.0.3",
"@types/tape": "^5.6.4",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"encoding": "^0.1.13",
"eslint": "^8.8.0",
"evalmd": "^0.0.19",
"gopd": "^1.0.1",
"gopd": "^1.2.0",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.4",
"tape": "^5.9.0",
"typescript": "next"
},
"engines": {
"node": ">= 0.4"
},
"testling": {
"files": "test/index.js"
},
"auto-changelog": {
"output": "CHANGELOG.md",
"template": "keepachangelog",

View File

@@ -10,6 +10,7 @@ test('defineProperty: supported', { skip: !$defineProperty }, function (t) {
t.equal(typeof $defineProperty, 'function', 'defineProperty is supported');
if ($defineProperty && gOPD) { // this `if` check is just to shut TS up
/** @type {{ a: number, b?: number, c?: number }} */
var o = { a: 1 };
$defineProperty(o, 'b', { enumerable: true, value: 2 });

View File

@@ -1,47 +1,7 @@
{
"extends": "@ljharb/tsconfig",
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
/* Language and Environment */
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
"useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": ["types"], /* Specify multiple folders that act like `./node_modules/@types`. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
"maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"noEmit": true, /* Disable emitting files from a compilation. */
/* Interop Constraints */
"allowSyntheticDefaultImports": true, /* Allow `import x from y` when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
// "skipLibCheck": true /* Skip type checking all .d.ts files. */
"target": "es2022",
},
"exclude": [
"coverage",

View File

@@ -1,3 +1,17 @@
4.21.2 / 2024-11-06
==========
* deps: path-to-regexp@0.1.12
- Fix backtracking protection
* deps: path-to-regexp@0.1.11
- Throws an error on invalid path values
4.21.1 / 2024-10-08
==========
* Backported a fix for [CVE-2024-47764](https://nvd.nist.gov/vuln/detail/CVE-2024-47764)
4.21.0 / 2024-09-11
==========

View File

@@ -1,7 +1,7 @@
{
"name": "express",
"description": "Fast, unopinionated, minimalist web framework",
"version": "4.21.0",
"version": "4.21.2",
"author": "TJ Holowaychuk <tj@vision-media.ca>",
"contributors": [
"Aaron Heckmann <aaron.heckmann+github@gmail.com>",
@@ -15,6 +15,10 @@
"license": "MIT",
"repository": "expressjs/express",
"homepage": "http://expressjs.com/",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
},
"keywords": [
"express",
"framework",
@@ -33,7 +37,7 @@
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -47,7 +51,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",

View File

@@ -11,6 +11,10 @@
"es2022": true,
},
"globals": {
"Float16Array": false,
},
"rules": {
"array-bracket-newline": 0,
"complexity": 0,

View File

@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.3.0](https://github.com/ljharb/get-intrinsic/compare/v1.2.7...v1.3.0) - 2025-02-22
### Commits
- [Dev Deps] update `es-abstract`, `es-value-fixtures`, `for-each`, `object-inspect` [`9b61553`](https://github.com/ljharb/get-intrinsic/commit/9b61553c587f1c1edbd435597e88c7d387da97dd)
- [Deps] update `call-bind-apply-helpers`, `es-object-atoms`, `get-proto` [`a341fee`](https://github.com/ljharb/get-intrinsic/commit/a341fee0f39a403b0f0069e82c97642d5eb11043)
- [New] add `Float16Array` [`de22116`](https://github.com/ljharb/get-intrinsic/commit/de22116b492fb989a0341bceb6e573abfaed73dc)
## [v1.2.7](https://github.com/ljharb/get-intrinsic/compare/v1.2.6...v1.2.7) - 2025-01-02
### Commits
- [Refactor] use `get-proto` directly [`00ab955`](https://github.com/ljharb/get-intrinsic/commit/00ab95546a0980c8ad42a84253daaa8d2adcedf9)
- [Deps] update `math-intrinsics` [`c716cdd`](https://github.com/ljharb/get-intrinsic/commit/c716cdd6bbe36b438057025561b8bb5a879ac8a0)
- [Dev Deps] update `call-bound`, `es-abstract` [`dc648a6`](https://github.com/ljharb/get-intrinsic/commit/dc648a67eb359037dff8d8619bfa71d86debccb1)
## [v1.2.6](https://github.com/ljharb/get-intrinsic/compare/v1.2.5...v1.2.6) - 2024-12-11
### Commits
- [Refactor] use `math-intrinsics` [`841be86`](https://github.com/ljharb/get-intrinsic/commit/841be8641a9254c4c75483b30c8871b5d5065926)
- [Refactor] use `es-object-atoms` [`42057df`](https://github.com/ljharb/get-intrinsic/commit/42057dfa16f66f64787e66482af381cc6f31d2c1)
- [Deps] update `call-bind-apply-helpers` [`45afa24`](https://github.com/ljharb/get-intrinsic/commit/45afa24a9ee4d6d3c172db1f555b16cb27843ef4)
- [Dev Deps] update `call-bound` [`9cba9c6`](https://github.com/ljharb/get-intrinsic/commit/9cba9c6e70212bc163b7a5529cb25df46071646f)
## [v1.2.5](https://github.com/ljharb/get-intrinsic/compare/v1.2.4...v1.2.5) - 2024-12-06
### Commits
- [actions] split out node 10-20, and 20+ [`6e2b9dd`](https://github.com/ljharb/get-intrinsic/commit/6e2b9dd23902665681ebe453256ccfe21d7966f0)
- [Refactor] use `dunder-proto` and `call-bind-apply-helpers` instead of `has-proto` [`c095d17`](https://github.com/ljharb/get-intrinsic/commit/c095d179ad0f4fbfff20c8a3e0cb4fe668018998)
- [Refactor] use `gopd` [`9841d5b`](https://github.com/ljharb/get-intrinsic/commit/9841d5b35f7ab4fd2d193f0c741a50a077920e90)
- [Dev Deps] update `@ljharb/eslint-config`, `auto-changelog`, `es-abstract`, `es-value-fixtures`, `gopd`, `mock-property`, `object-inspect`, `tape` [`2d07e01`](https://github.com/ljharb/get-intrinsic/commit/2d07e01310cee2cbaedfead6903df128b1f5d425)
- [Deps] update `gopd`, `has-proto`, `has-symbols`, `hasown` [`974d8bf`](https://github.com/ljharb/get-intrinsic/commit/974d8bf5baad7939eef35c25cc1dd88c10a30fa6)
- [Dev Deps] update `call-bind`, `es-abstract`, `tape` [`df9dde1`](https://github.com/ljharb/get-intrinsic/commit/df9dde178186631ab8a3165ede056549918ce4bc)
- [Refactor] cache `es-define-property` as well [`43ef543`](https://github.com/ljharb/get-intrinsic/commit/43ef543cb02194401420e3a914a4ca9168691926)
- [Deps] update `has-proto`, `has-symbols`, `hasown` [`ad4949d`](https://github.com/ljharb/get-intrinsic/commit/ad4949d5467316505aad89bf75f9417ed782f7af)
- [Tests] use `call-bound` directly [`ad5c406`](https://github.com/ljharb/get-intrinsic/commit/ad5c4069774bfe90e520a35eead5fe5ca9d69e80)
- [Deps] update `has-proto`, `hasown` [`45414ca`](https://github.com/ljharb/get-intrinsic/commit/45414caa312333a2798953682c68f85c550627dd)
- [Tests] replace `aud` with `npm audit` [`18d3509`](https://github.com/ljharb/get-intrinsic/commit/18d3509f79460e7924da70409ee81e5053087523)
- [Deps] update `es-define-property` [`aadaa3b`](https://github.com/ljharb/get-intrinsic/commit/aadaa3b2188d77ad9bff394ce5d4249c49eb21f5)
- [Dev Deps] add missing peer dep [`c296a16`](https://github.com/ljharb/get-intrinsic/commit/c296a16246d0c9a5981944f4cc5cf61fbda0cf6a)
## [v1.2.4](https://github.com/ljharb/get-intrinsic/compare/v1.2.3...v1.2.4) - 2024-02-05
### Commits

View File

@@ -2,6 +2,8 @@
var undefined;
var $Object = require('es-object-atoms');
var $Error = require('es-errors');
var $EvalError = require('es-errors/eval');
var $RangeError = require('es-errors/range');
@@ -10,6 +12,14 @@ var $SyntaxError = require('es-errors/syntax');
var $TypeError = require('es-errors/type');
var $URIError = require('es-errors/uri');
var abs = require('math-intrinsics/abs');
var floor = require('math-intrinsics/floor');
var max = require('math-intrinsics/max');
var min = require('math-intrinsics/min');
var pow = require('math-intrinsics/pow');
var round = require('math-intrinsics/round');
var sign = require('math-intrinsics/sign');
var $Function = Function;
// eslint-disable-next-line consistent-return
@@ -19,14 +29,8 @@ var getEvalledConstructor = function (expressionSyntax) {
} catch (e) {}
};
var $gOPD = Object.getOwnPropertyDescriptor;
if ($gOPD) {
try {
$gOPD({}, '');
} catch (e) {
$gOPD = null; // this is IE 8, which has a broken gOPD
}
}
var $gOPD = require('gopd');
var $defineProperty = require('es-define-property');
var throwTypeError = function () {
throw new $TypeError();
@@ -49,13 +53,13 @@ var ThrowTypeError = $gOPD
: throwTypeError;
var hasSymbols = require('has-symbols')();
var hasProto = require('has-proto')();
var getProto = Object.getPrototypeOf || (
hasProto
? function (x) { return x.__proto__; } // eslint-disable-line no-proto
: null
);
var getProto = require('get-proto');
var $ObjectGPO = require('get-proto/Object.getPrototypeOf');
var $ReflectGPO = require('get-proto/Reflect.getPrototypeOf');
var $apply = require('call-bind-apply-helpers/functionApply');
var $call = require('call-bind-apply-helpers/functionCall');
var needsEval = {};
@@ -86,6 +90,7 @@ var INTRINSICS = {
'%Error%': $Error,
'%eval%': eval, // eslint-disable-line no-eval
'%EvalError%': $EvalError,
'%Float16Array%': typeof Float16Array === 'undefined' ? undefined : Float16Array,
'%Float32Array%': typeof Float32Array === 'undefined' ? undefined : Float32Array,
'%Float64Array%': typeof Float64Array === 'undefined' ? undefined : Float64Array,
'%FinalizationRegistry%': typeof FinalizationRegistry === 'undefined' ? undefined : FinalizationRegistry,
@@ -102,7 +107,8 @@ var INTRINSICS = {
'%MapIteratorPrototype%': typeof Map === 'undefined' || !hasSymbols || !getProto ? undefined : getProto(new Map()[Symbol.iterator]()),
'%Math%': Math,
'%Number%': Number,
'%Object%': Object,
'%Object%': $Object,
'%Object.getOwnPropertyDescriptor%': $gOPD,
'%parseFloat%': parseFloat,
'%parseInt%': parseInt,
'%Promise%': typeof Promise === 'undefined' ? undefined : Promise,
@@ -128,7 +134,20 @@ var INTRINSICS = {
'%URIError%': $URIError,
'%WeakMap%': typeof WeakMap === 'undefined' ? undefined : WeakMap,
'%WeakRef%': typeof WeakRef === 'undefined' ? undefined : WeakRef,
'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet
'%WeakSet%': typeof WeakSet === 'undefined' ? undefined : WeakSet,
'%Function.prototype.call%': $call,
'%Function.prototype.apply%': $apply,
'%Object.defineProperty%': $defineProperty,
'%Object.getPrototypeOf%': $ObjectGPO,
'%Math.abs%': abs,
'%Math.floor%': floor,
'%Math.max%': max,
'%Math.min%': min,
'%Math.pow%': pow,
'%Math.round%': round,
'%Math.sign%': sign,
'%Reflect.getPrototypeOf%': $ReflectGPO
};
if (getProto) {
@@ -223,11 +242,11 @@ var LEGACY_ALIASES = {
var bind = require('function-bind');
var hasOwn = require('hasown');
var $concat = bind.call(Function.call, Array.prototype.concat);
var $spliceApply = bind.call(Function.apply, Array.prototype.splice);
var $replace = bind.call(Function.call, String.prototype.replace);
var $strSlice = bind.call(Function.call, String.prototype.slice);
var $exec = bind.call(Function.call, RegExp.prototype.exec);
var $concat = bind.call($call, Array.prototype.concat);
var $spliceApply = bind.call($apply, Array.prototype.splice);
var $replace = bind.call($call, String.prototype.replace);
var $strSlice = bind.call($call, String.prototype.slice);
var $exec = bind.call($call, RegExp.prototype.exec);
/* adapted from https://github.com/lodash/lodash/blob/4.17.15/dist/lodash.js#L6735-L6744 */
var rePropName = /[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g;

View File

@@ -1,6 +1,6 @@
{
"name": "get-intrinsic",
"version": "1.2.4",
"version": "1.3.0",
"description": "Get and robustly cache all JS language-level intrinsics at first require time",
"main": "index.js",
"exports": {
@@ -17,7 +17,7 @@
"pretest": "npm run lint",
"tests-only": "nyc tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>= 10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -43,26 +43,37 @@
"url": "https://github.com/ljharb/get-intrinsic/issues"
},
"homepage": "https://github.com/ljharb/get-intrinsic#readme",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"call-bind": "^1.0.5",
"es-abstract": "^1.22.3",
"es-value-fixtures": "^1.4.2",
"@ljharb/eslint-config": "^21.1.1",
"auto-changelog": "^2.5.0",
"call-bound": "^1.0.3",
"encoding": "^0.1.13",
"es-abstract": "^1.23.9",
"es-value-fixtures": "^1.7.1",
"eslint": "=8.8.0",
"evalmd": "^0.0.19",
"for-each": "^0.3.3",
"gopd": "^1.0.1",
"for-each": "^0.3.5",
"make-async-function": "^1.0.0",
"make-async-generator-function": "^1.0.0",
"make-generator-function": "^2.0.0",
"mock-property": "^1.0.3",
"mock-property": "^1.1.0",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"object-inspect": "^1.13.1",
"object-inspect": "^1.13.4",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.4"
"tape": "^5.9.0"
},
"auto-changelog": {
"output": "CHANGELOG.md",
@@ -72,13 +83,6 @@
"backfillLimit": false,
"hideCredit": true
},
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
},
"testling": {
"files": "test/GetIntrinsic.js"
},

View File

@@ -10,10 +10,10 @@ var asyncFns = require('make-async-function').list();
var asyncGenFns = require('make-async-generator-function')();
var mockProperty = require('mock-property');
var callBound = require('call-bind/callBound');
var callBound = require('call-bound');
var v = require('es-value-fixtures');
var $gOPD = require('gopd');
var DefinePropertyOrThrow = require('es-abstract/2021/DefinePropertyOrThrow');
var DefinePropertyOrThrow = require('es-abstract/2023/DefinePropertyOrThrow');
var $isProto = callBound('%Object.prototype.isPrototypeOf%');

View File

@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.2.0](https://github.com/ljharb/gopd/compare/v1.1.0...v1.2.0) - 2024-12-03
### Commits
- [New] add `gOPD` entry point; remove `get-intrinsic` [`5b61232`](https://github.com/ljharb/gopd/commit/5b61232dedea4591a314bcf16101b1961cee024e)
## [v1.1.0](https://github.com/ljharb/gopd/compare/v1.0.1...v1.1.0) - 2024-11-29
### Commits
- [New] add types [`f585e39`](https://github.com/ljharb/gopd/commit/f585e397886d270e4ba84e53d226e4f9ca2eb0e6)
- [Dev Deps] update `@ljharb/eslint-config`, `auto-changelog`, `tape` [`0b8e4fd`](https://github.com/ljharb/gopd/commit/0b8e4fded64397a7726a9daa144a6cc9a5e2edfa)
- [Dev Deps] update `aud`, `npmignore`, `tape` [`48378b2`](https://github.com/ljharb/gopd/commit/48378b2443f09a4f7efbd0fb6c3ee845a6cabcf3)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `tape` [`78099ee`](https://github.com/ljharb/gopd/commit/78099eeed41bfdc134c912280483689cc8861c31)
- [Tests] replace `aud` with `npm audit` [`4e0d0ac`](https://github.com/ljharb/gopd/commit/4e0d0ac47619d24a75318a8e1f543ee04b2a2632)
- [meta] add missing `engines.node` [`1443316`](https://github.com/ljharb/gopd/commit/14433165d07835c680155b3dfd62d9217d735eca)
- [Deps] update `get-intrinsic` [`eee5f51`](https://github.com/ljharb/gopd/commit/eee5f51769f3dbaf578b70e2a3199116b01aa670)
- [Deps] update `get-intrinsic` [`550c378`](https://github.com/ljharb/gopd/commit/550c3780e3a9c77b62565712a001b4ed64ea61f5)
- [Dev Deps] add missing peer dep [`8c2ecf8`](https://github.com/ljharb/gopd/commit/8c2ecf848122e4e30abfc5b5086fb48b390dce75)
## [v1.0.1](https://github.com/ljharb/gopd/compare/v1.0.0...v1.0.1) - 2022-11-01
### Commits

5
backend/node_modules/gopd/index.js generated vendored
View File

@@ -1,8 +1,7 @@
'use strict';
var GetIntrinsic = require('get-intrinsic');
var $gOPD = GetIntrinsic('%Object.getOwnPropertyDescriptor%', true);
/** @type {import('.')} */
var $gOPD = require('./gOPD');
if ($gOPD) {
try {

View File

@@ -1,10 +1,11 @@
{
"name": "gopd",
"version": "1.0.1",
"version": "1.2.0",
"description": "`Object.getOwnPropertyDescriptor`, but accounts for IE's broken implementation.",
"main": "index.js",
"exports": {
".": "./index.js",
"./gOPD": "./gOPD.js",
"./package.json": "./package.json"
},
"sideEffects": false,
@@ -12,12 +13,13 @@
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"prelint": "tsc -p . && attw -P",
"lint": "eslint --ext=js,mjs .",
"postlint": "evalmd README.md",
"pretest": "npm run lint",
"tests-only": "tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"posttest": "npx npm@'>=10.2' audit --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -41,19 +43,20 @@
"url": "https://github.com/ljharb/gopd/issues"
},
"homepage": "https://github.com/ljharb/gopd#readme",
"dependencies": {
"get-intrinsic": "^1.1.3"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.0.0",
"aud": "^2.0.1",
"auto-changelog": "^2.4.0",
"@arethetypeswrong/cli": "^0.17.0",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.0",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"encoding": "^0.1.13",
"eslint": "=8.8.0",
"evalmd": "^0.0.19",
"in-publish": "^2.0.1",
"npmignore": "^0.3.0",
"npmignore": "^0.3.1",
"safe-publish-latest": "^2.0.0",
"tape": "^5.6.1"
"tape": "^5.9.0",
"typescript": "next"
},
"auto-changelog": {
"output": "CHANGELOG.md",
@@ -67,5 +70,8 @@
"ignore": [
".github/workflows"
]
},
"engines": {
"node": ">= 0.4"
}
}

View File

@@ -10,6 +10,7 @@ test('gOPD', function (t) {
var obj = { x: 1 };
st.ok('x' in obj, 'property exists');
// @ts-expect-error TS can't figure out narrowing from `skip`
var desc = gOPD(obj, 'x');
st.deepEqual(
desc,
@@ -25,7 +26,7 @@ test('gOPD', function (t) {
st.end();
});
t.test('not supported', { skip: gOPD }, function (st) {
t.test('not supported', { skip: !!gOPD }, function (st) {
st.notOk(gOPD, 'is falsy');
st.end();

View File

@@ -1,5 +0,0 @@
{
"root": true,
"extends": "@ljharb",
}

View File

@@ -1,12 +0,0 @@
# These are supported funding model platforms
github: [ljharb]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: npm/has-proto
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,38 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.0.3](https://github.com/inspect-js/has-proto/compare/v1.0.2...v1.0.3) - 2024-02-19
### Commits
- [types] add missing declaration file [`26ecade`](https://github.com/inspect-js/has-proto/commit/26ecade05d253bb5dc376945ee3186d1fbe334f8)
## [v1.0.2](https://github.com/inspect-js/has-proto/compare/v1.0.1...v1.0.2) - 2024-02-19
### Commits
- add types [`6435262`](https://github.com/inspect-js/has-proto/commit/64352626cf511c0276d5f4bb6be770a0bf0f8524)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `npmignore`, `tape` [`f16a5e4`](https://github.com/inspect-js/has-proto/commit/f16a5e4121651e551271419f9d60fdd3561fd82c)
- [Refactor] tiny cleanup [`d1f1a4b`](https://github.com/inspect-js/has-proto/commit/d1f1a4bdc135f115a10f148ce302676224534702)
- [meta] add `sideEffects` flag [`e7ab1a6`](https://github.com/inspect-js/has-proto/commit/e7ab1a6f153b3e80dee68d1748b71e46767a0531)
## [v1.0.1](https://github.com/inspect-js/has-proto/compare/v1.0.0...v1.0.1) - 2022-12-21
### Commits
- [meta] correct URLs and description [`ef34483`](https://github.com/inspect-js/has-proto/commit/ef34483ca0d35680f271b6b96e35526151b25dfc)
- [patch] add an additional criteria [`e81959e`](https://github.com/inspect-js/has-proto/commit/e81959ed7c7a77fbf459f00cb4ef824f1099497f)
- [Dev Deps] update `aud` [`2bec2c4`](https://github.com/inspect-js/has-proto/commit/2bec2c47b072b122ff5443fba0263f6dc649531f)
## v1.0.0 - 2022-12-12
### Commits
- Initial implementation, tests, readme [`6886fea`](https://github.com/inspect-js/has-proto/commit/6886fea578f67daf69a7920b2eb7637ea6ebb0bc)
- Initial commit [`99129c8`](https://github.com/inspect-js/has-proto/commit/99129c8f42471ac89cb681ba9cb9d52a583eb94f)
- npm init [`2844ad8`](https://github.com/inspect-js/has-proto/commit/2844ad8e75b84d66a46765b3bab9d2e8ea692e10)
- Only apps should have lockfiles [`c65bc5e`](https://github.com/inspect-js/has-proto/commit/c65bc5e40b9004463f7336d47c67245fb139a36a)

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Inspect JS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,38 +0,0 @@
# has-proto <sup>[![Version Badge][npm-version-svg]][package-url]</sup>
[![github actions][actions-image]][actions-url]
[![coverage][codecov-image]][codecov-url]
[![License][license-image]][license-url]
[![Downloads][downloads-image]][downloads-url]
[![npm badge][npm-badge-png]][package-url]
Does this environment have the ability to set the [[Prototype]] of an object on creation with `__proto__`?
## Example
```js
var hasProto = require('has-proto');
var assert = require('assert');
assert.equal(typeof hasProto(), 'boolean');
```
## Tests
Simply clone the repo, `npm install`, and run `npm test`
[package-url]: https://npmjs.org/package/has-proto
[npm-version-svg]: https://versionbadg.es/inspect-js/has-proto.svg
[deps-svg]: https://david-dm.org/inspect-js/has-proto.svg
[deps-url]: https://david-dm.org/inspect-js/has-proto
[dev-deps-svg]: https://david-dm.org/inspect-js/has-proto/dev-status.svg
[dev-deps-url]: https://david-dm.org/inspect-js/has-proto#info=devDependencies
[npm-badge-png]: https://nodei.co/npm/has-proto.png?downloads=true&stars=true
[license-image]: https://img.shields.io/npm/l/has-proto.svg
[license-url]: LICENSE
[downloads-image]: https://img.shields.io/npm/dm/has-proto.svg
[downloads-url]: https://npm-stat.com/charts.html?package=has-proto
[codecov-image]: https://codecov.io/gh/inspect-js/has-proto/branch/main/graphs/badge.svg
[codecov-url]: https://app.codecov.io/gh/inspect-js/has-proto/
[actions-image]: https://img.shields.io/endpoint?url=https://github-actions-badge-u3jn4tfpocch.runkit.sh/inspect-js/has-proto
[actions-url]: https://github.com/inspect-js/has-proto/actions

View File

@@ -1,3 +0,0 @@
declare function hasProto(): boolean;
export = hasProto;

View File

@@ -1,15 +0,0 @@
'use strict';
var test = {
__proto__: null,
foo: {}
};
var $Object = Object;
/** @type {import('.')} */
module.exports = function hasProto() {
// @ts-expect-error: TS errors on an inherited property for some reason
return { __proto__: test }.foo === test.foo
&& !(test instanceof $Object);
};

View File

@@ -1,78 +0,0 @@
{
"name": "has-proto",
"version": "1.0.3",
"description": "Does this environment have the ability to get the [[Prototype]] of an object on creation with `__proto__`?",
"main": "index.js",
"exports": {
".": "./index.js",
"./package.json": "./package.json"
},
"sideEffects": false,
"scripts": {
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"lint": "eslint --ext=js,mjs .",
"postlint": "tsc -p .",
"pretest": "npm run lint",
"tests-only": "tape 'test/**/*.js'",
"test": "npm run tests-only",
"posttest": "aud --production",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/inspect-js/has-proto.git"
},
"keywords": [
"prototype",
"proto",
"set",
"get",
"__proto__",
"getPrototypeOf",
"setPrototypeOf",
"has"
],
"author": "Jordan Harband <ljharb@gmail.com>",
"funding": {
"url": "https://github.com/sponsors/ljharb"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/inspect-js/has-proto/issues"
},
"homepage": "https://github.com/inspect-js/has-proto#readme",
"testling": {
"files": "test/index.js"
},
"devDependencies": {
"@ljharb/eslint-config": "^21.1.0",
"@types/tape": "^5.6.4",
"aud": "^2.0.4",
"auto-changelog": "^2.4.0",
"eslint": "=8.8.0",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"safe-publish-latest": "^2.0.0",
"tape": "^5.7.5",
"typescript": "next"
},
"engines": {
"node": ">= 0.4"
},
"auto-changelog": {
"output": "CHANGELOG.md",
"template": "keepachangelog",
"unreleased": false,
"commitLimit": false,
"backfillLimit": false,
"hideCredit": true
},
"publishConfig": {
"ignore": [
".github/workflows"
]
}
}

View File

@@ -1,19 +0,0 @@
'use strict';
var test = require('tape');
var hasProto = require('../');
test('hasProto', function (t) {
var result = hasProto();
t.equal(typeof result, 'boolean', 'returns a boolean (' + result + ')');
var obj = { __proto__: null };
if (result) {
t.notOk('toString' in obj, 'null object lacks toString');
} else {
t.ok('toString' in obj, 'without proto, null object has toString');
t.equal(obj.__proto__, null); // eslint-disable-line no-proto
}
t.end();
});

View File

@@ -1,49 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
/* Language and Environment */
"target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
"useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
"typeRoots": ["types"], /* Specify multiple folders that act like './node_modules/@types'. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
/* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
"checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
"maxNodeModuleJsDepth": 0, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"declarationMap": true, /* Create sourcemaps for d.ts files. */
"noEmit": true, /* Disable emitting files from a compilation. */
/* Interop Constraints */
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
/* Completeness */
//"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"coverage"
]
}

View File

@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v1.1.0](https://github.com/inspect-js/has-symbols/compare/v1.0.3...v1.1.0) - 2024-12-02
### Commits
- [actions] update workflows [`548c0bf`](https://github.com/inspect-js/has-symbols/commit/548c0bf8c9b1235458df7a1c0490b0064647a282)
- [actions] further shard; update action deps [`bec56bb`](https://github.com/inspect-js/has-symbols/commit/bec56bb0fb44b43a786686b944875a3175cf3ff3)
- [meta] use `npmignore` to autogenerate an npmignore file [`ac81032`](https://github.com/inspect-js/has-symbols/commit/ac81032809157e0a079e5264e9ce9b6f1275777e)
- [New] add types [`6469cbf`](https://github.com/inspect-js/has-symbols/commit/6469cbff1866cfe367b2b3d181d9296ec14b2a3d)
- [actions] update rebase action to use reusable workflow [`9c9d4d0`](https://github.com/inspect-js/has-symbols/commit/9c9d4d0d8938e4b267acdf8e421f4e92d1716d72)
- [Dev Deps] update `eslint`, `@ljharb/eslint-config`, `aud`, `tape` [`adb5887`](https://github.com/inspect-js/has-symbols/commit/adb5887ca9444849b08beb5caaa9e1d42320cdfb)
- [Dev Deps] update `@ljharb/eslint-config`, `aud`, `tape` [`13ec198`](https://github.com/inspect-js/has-symbols/commit/13ec198ec80f1993a87710af1606a1970b22c7cb)
- [Dev Deps] update `auto-changelog`, `core-js`, `tape` [`941be52`](https://github.com/inspect-js/has-symbols/commit/941be5248387cab1da72509b22acf3fdb223f057)
- [Tests] replace `aud` with `npm audit` [`74f49e9`](https://github.com/inspect-js/has-symbols/commit/74f49e9a9d17a443020784234a1c53ce765b3559)
- [Dev Deps] update `npmignore` [`9c0ac04`](https://github.com/inspect-js/has-symbols/commit/9c0ac0452a834f4c2a4b54044f2d6a89f17e9a70)
- [Dev Deps] add missing peer dep [`52337a5`](https://github.com/inspect-js/has-symbols/commit/52337a5621cced61f846f2afdab7707a8132cc12)
## [v1.0.3](https://github.com/inspect-js/has-symbols/compare/v1.0.2...v1.0.3) - 2022-03-01
### Commits

View File

@@ -3,6 +3,7 @@
var origSymbol = typeof Symbol !== 'undefined' && Symbol;
var hasSymbolSham = require('./shams');
/** @type {import('.')} */
module.exports = function hasNativeSymbols() {
if (typeof origSymbol !== 'function') { return false; }
if (typeof Symbol !== 'function') { return false; }

View File

@@ -1,21 +1,23 @@
{
"name": "has-symbols",
"version": "1.0.3",
"version": "1.1.0",
"description": "Determine if the JS environment has Symbol support. Supports spec, or shams.",
"main": "index.js",
"scripts": {
"prepack": "npmignore --auto --commentLines=autogenerated",
"prepublishOnly": "safe-publish-latest",
"prepublish": "not-in-publish || npm run prepublishOnly",
"pretest": "npm run --silent lint",
"test": "npm run tests-only",
"posttest": "aud --production",
"tests-only": "npm run test:stock && npm run test:staging && npm run test:shams",
"posttest": "npx npm@'>=10.2' audit --production",
"tests-only": "npm run test:stock && npm run test:shams",
"test:stock": "nyc node test",
"test:staging": "nyc node --harmony --es-staging test",
"test:shams": "npm run --silent test:shams:getownpropertysymbols && npm run --silent test:shams:corejs",
"test:shams:corejs": "nyc node test/shams/core-js.js",
"test:shams:getownpropertysymbols": "nyc node test/shams/get-own-property-symbols.js",
"lint": "eslint --ext=js,mjs .",
"postlint": "tsc -p . && attw -P",
"version": "auto-changelog && git add CHANGELOG.md",
"postversion": "auto-changelog && git add CHANGELOG.md && git commit --no-edit --amend && git tag -f \"v$(node -e \"console.log(require('./package.json').version)\")\""
},
@@ -54,15 +56,22 @@
},
"homepage": "https://github.com/ljharb/has-symbols#readme",
"devDependencies": {
"@ljharb/eslint-config": "^20.2.3",
"aud": "^2.0.0",
"auto-changelog": "^2.4.0",
"@arethetypeswrong/cli": "^0.17.0",
"@ljharb/eslint-config": "^21.1.1",
"@ljharb/tsconfig": "^0.2.0",
"@types/core-js": "^2.5.8",
"@types/tape": "^5.6.5",
"auto-changelog": "^2.5.0",
"core-js": "^2.6.12",
"encoding": "^0.1.13",
"eslint": "=8.8.0",
"get-own-property-symbols": "^0.9.5",
"in-publish": "^2.0.1",
"npmignore": "^0.3.1",
"nyc": "^10.3.2",
"safe-publish-latest": "^2.0.0",
"tape": "^5.5.2"
"tape": "^5.9.0",
"typescript": "next"
},
"testling": {
"files": "test/index.js",
@@ -93,9 +102,10 @@
"backfillLimit": false,
"hideCredit": true
},
"greenkeeper": {
"publishConfig": {
"ignore": [
"core-js"
".github/workflows",
"types"
]
}
}

View File

@@ -1,10 +1,12 @@
'use strict';
/** @type {import('./shams')} */
/* eslint complexity: [2, 18], max-statements: [2, 33] */
module.exports = function hasSymbols() {
if (typeof Symbol !== 'function' || typeof Object.getOwnPropertySymbols !== 'function') { return false; }
if (typeof Symbol.iterator === 'symbol') { return true; }
/** @type {{ [k in symbol]?: unknown }} */
var obj = {};
var sym = Symbol('test');
var symObj = Object(sym);
@@ -23,7 +25,7 @@ module.exports = function hasSymbols() {
var symVal = 42;
obj[sym] = symVal;
for (sym in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop
for (var _ in obj) { return false; } // eslint-disable-line no-restricted-syntax, no-unreachable-loop
if (typeof Object.keys === 'function' && Object.keys(obj).length !== 0) { return false; }
if (typeof Object.getOwnPropertyNames === 'function' && Object.getOwnPropertyNames(obj).length !== 0) { return false; }
@@ -34,7 +36,8 @@ module.exports = function hasSymbols() {
if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { return false; }
if (typeof Object.getOwnPropertyDescriptor === 'function') {
var descriptor = Object.getOwnPropertyDescriptor(obj, sym);
// eslint-disable-next-line no-extra-parens
var descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(obj, sym));
if (descriptor.value !== symVal || descriptor.enumerable !== true) { return false; }
}

View File

@@ -8,6 +8,7 @@ if (typeof Symbol === 'function' && typeof Symbol() === 'symbol') {
t.equal(typeof Symbol(), 'symbol');
t.end();
});
// @ts-expect-error TS is stupid and doesn't know about top level return
return;
}

View File

@@ -8,6 +8,7 @@ if (typeof Symbol === 'function' && typeof Symbol() === 'symbol') {
t.equal(typeof Symbol(), 'symbol');
t.end();
});
// @ts-expect-error TS is stupid and doesn't know about top level return
return;
}

View File

@@ -1,5 +1,6 @@
'use strict';
/** @type {(t: import('tape').Test) => false | void} */
// eslint-disable-next-line consistent-return
module.exports = function runSymbolTests(t) {
t.equal(typeof Symbol, 'function', 'global Symbol is a function');
@@ -31,6 +32,7 @@ module.exports = function runSymbolTests(t) {
t.equal(typeof Object.getOwnPropertySymbols, 'function', 'Object.getOwnPropertySymbols is a function');
/** @type {{ [k in symbol]?: unknown }} */
var obj = {};
var sym = Symbol('test');
var symObj = Object(sym);
@@ -40,8 +42,8 @@ module.exports = function runSymbolTests(t) {
var symVal = 42;
obj[sym] = symVal;
// eslint-disable-next-line no-restricted-syntax
for (sym in obj) { t.fail('symbol property key was found in for..in of object'); }
// eslint-disable-next-line no-restricted-syntax, no-unused-vars
for (var _ in obj) { t.fail('symbol property key was found in for..in of object'); }
t.deepEqual(Object.keys(obj), [], 'no enumerable own keys on symbol-valued object');
t.deepEqual(Object.getOwnPropertyNames(obj), [], 'no own names on symbol-valued object');

View File

@@ -65,23 +65,33 @@ function pathToRegexp(path, keys, options) {
return new RegExp(path.join('|'), flags);
}
if (typeof path !== 'string') {
throw new TypeError('path must be a string, array of strings, or regular expression');
}
path = path.replace(
/\\.|(\/)?(\.)?:(\w+)(\(.*?\))?(\*)?(\?)?|[.*]|\/\(/g,
function (match, slash, format, key, capture, star, optional, offset) {
pos = offset + match.length;
if (match[0] === '\\') {
backtrack += match;
pos += 2;
return match;
}
if (match === '.') {
backtrack += '\\.';
extraOffset += 1;
pos += 1;
return '\\.';
}
backtrack = slash || format ? '' : path.slice(pos, offset);
if (slash || format) {
backtrack = '';
} else {
backtrack += path.slice(pos, offset);
}
pos = offset + match.length;
if (match === '*') {
extraOffset += 3;

View File

@@ -1,7 +1,7 @@
{
"name": "path-to-regexp",
"description": "Express style path to RegExp utility",
"version": "0.1.10",
"version": "0.1.12",
"files": [
"index.js",
"LICENSE"

View File

@@ -10,10 +10,12 @@
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
"axios": "^1.12.2",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"csv-parser": "^3.0.0",
"date-fns": "^2.30.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"iconv-lite": "^0.6.3",
@@ -21,6 +23,7 @@
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.3",
"nodemailer": "^6.9.14",
"pdf-parse": "^1.1.1",
"sequelize": "^6.37.3",
"sharp": "^0.33.5"
},
@@ -29,6 +32,15 @@
"vue-eslint-parser": "9.4.3"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz",
@@ -819,6 +831,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/aws-ssl-profiles": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.1.tgz",
@@ -827,6 +845,17 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.12.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -893,9 +922,10 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -963,6 +993,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1092,6 +1135,18 @@
"color-support": "bin.js"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1168,9 +1223,10 @@
}
},
"node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -1199,10 +1255,11 @@
}
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"path-key": "^3.1.0",
@@ -1233,6 +1290,22 @@
"node": ">= 10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1265,6 +1338,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -1321,6 +1403,20 @@
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
"integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA=="
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -1348,13 +1444,10 @@
}
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
@@ -1368,6 +1461,33 @@
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1585,16 +1705,17 @@
}
},
"node_modules/express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -1608,7 +1729,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -1623,6 +1744,10 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/encodeurl": {
@@ -1753,6 +1878,42 @@
"dev": true,
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1849,16 +2010,21 @@
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
"integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"has-proto": "^1.0.1",
"has-symbols": "^1.0.3",
"hasown": "^2.0.0"
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -1867,6 +2033,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1913,12 +2092,12 @@
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
"integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"dependencies": {
"get-intrinsic": "^1.1.3"
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1945,10 +2124,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-proto": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
"integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -1957,11 +2136,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
@@ -2405,6 +2587,15 @@
"semver": "bin/semver.js"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -2636,6 +2827,12 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",
"integrity": "sha512-DRI60hzo2oKN1ma0ckc6nQWlHU69RH6xN0sjQTjMpChPfTYvKZdcQFfdYK2RWbJcKyUizSIy/l8OTGxMAM1QDw==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
@@ -2888,9 +3085,37 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/pdf-parse": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-1.1.1.tgz",
"integrity": "sha512-v6ZJ/efsBpGrGGknjtq9J/oC8tZWq0KWL5vQrk2GlzLEQPUDB1ex+13Rmidl1neNN358Jn9EHZw5y07FFtaC7A==",
"license": "MIT",
"dependencies": {
"debug": "^3.1.0",
"node-ensure": "^0.0.0"
},
"engines": {
"node": ">=6.8.1"
}
},
"node_modules/pdf-parse/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/pdf-parse/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/pg-connection-string": {
@@ -2938,6 +3163,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@@ -5,17 +5,21 @@
"type": "module",
"scripts": {
"postinstall": "cd ../frontend && npm install && npm run build",
"dev": "nodemon server.js"
"dev": "nodemon server.js",
"cleanup:usertoken": "node ./scripts/cleanupUserTokenKeys.js",
"cleanup:indexes": "node ./scripts/cleanupAllIndexes.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.12.2",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"csv-parser": "^3.0.0",
"date-fns": "^2.30.0",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"iconv-lite": "^0.6.3",
@@ -23,6 +27,7 @@
"multer": "^1.4.5-lts.1",
"mysql2": "^3.10.3",
"nodemailer": "^6.9.14",
"pdf-parse": "^1.1.1",
"sequelize": "^6.37.3",
"sharp": "^0.33.5"
},

View File

@@ -0,0 +1,15 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { addMembersToActivity, removeMemberFromActivity, getMembersForActivity } from '../controllers/diaryMemberActivityController.js';
const router = express.Router();
router.use(authenticate);
router.get('/:clubId/:diaryDateActivityId', getMembersForActivity);
router.post('/:clubId/:diaryDateActivityId', addMembersToActivity);
router.delete('/:clubId/:diaryDateActivityId/:participantId', removeMemberFromActivity);
export default router;

View File

@@ -8,7 +8,8 @@ import {
deleteDiaryNote,
addDiaryTag,
addTagToDiaryDate,
deleteTagFromDiaryDate
deleteTagFromDiaryDate,
deleteDateForClub,
} from '../controllers/diaryController.js';
const router = express.Router();
@@ -21,5 +22,6 @@ router.delete('/:clubId/tag', authenticate, deleteTagFromDiaryDate);
router.get('/:clubId', authenticate, getDatesForClub);
router.post('/:clubId', authenticate, createDateForClub);
router.put('/:clubId', authenticate, updateTrainingTimes);
router.delete('/:clubId/:dateId', authenticate, deleteDateForClub);
export default router;

View File

@@ -0,0 +1,36 @@
import express from 'express';
import externalServiceController from '../controllers/externalServiceController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
// All routes require authentication
router.use(authenticate);
// GET /api/external-service/account?service=mytischtennis - Get account
router.get('/account', externalServiceController.getAccount);
// GET /api/external-service/status?service=mytischtennis - Check status
router.get('/status', externalServiceController.getStatus);
// POST /api/external-service/account - Create or update account
router.post('/account', externalServiceController.upsertAccount);
// DELETE /api/external-service/account?service=mytischtennis - Delete account
router.delete('/account', externalServiceController.deleteAccount);
// POST /api/external-service/verify - Verify login
router.post('/verify', externalServiceController.verifyLogin);
// GET /api/external-service/session?service=mytischtennis - Get stored session
router.get('/session', externalServiceController.getSession);
// HeTTV specific routes
// GET /api/external-service/hettv/main-page - Load HeTTV main page and find downloads
router.get('/hettv/main-page', externalServiceController.loadHettvMainPage);
// POST /api/external-service/hettv/download-page - Load specific HeTTV download page
router.post('/hettv/download-page', externalServiceController.loadHettvDownloadPage);
export default router;

View File

@@ -1,4 +1,4 @@
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage } from '../controllers/memberController.js';
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis } from '../controllers/memberController.js';
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import multer from 'multer';
@@ -13,5 +13,6 @@ router.get('/image/:clubId/:memberId', authenticate, getMemberImage);
router.get('/get/:id/:showAll', authenticate, getClubMembers);
router.post('/set/:id', authenticate, setClubMembers);
router.get('/notapproved/:id', authenticate, getWaitingApprovals);
router.post('/update-ratings/:id', authenticate, updateRatingsFromMyTischtennis);
export default router;

View File

@@ -0,0 +1,21 @@
import express from 'express';
import multer from 'multer';
import { authenticate } from '../middleware/authMiddleware.js';
import { uploadTournamentPdf, getParsedTournament, listOfficialTournaments, deleteOfficialTournament, upsertCompetitionMember, listClubParticipations, updateParticipantStatus } from '../controllers/officialTournamentController.js';
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
router.use(authenticate);
router.get('/:clubId', listOfficialTournaments);
router.get('/:clubId/participations/summary', listClubParticipations);
router.post('/:clubId/upload', upload.single('pdf'), uploadTournamentPdf);
router.get('/:clubId/:id', getParsedTournament);
router.delete('/:clubId/:id', deleteOfficialTournament);
router.post('/:clubId/:id/participation', upsertCompetitionMember);
router.post('/:clubId/:id/status', updateParticipantStatus);
export default router;

View File

@@ -4,13 +4,41 @@ import {
getAllPredefinedActivities,
getPredefinedActivityById,
updatePredefinedActivity,
searchPredefinedActivities,
mergePredefinedActivities,
deduplicatePredefinedActivities,
} from '../controllers/predefinedActivityController.js';
import multer from 'multer';
import { authenticate } from '../middleware/authMiddleware.js';
import { uploadPredefinedActivityImage, deletePredefinedActivityImage } from '../controllers/predefinedActivityImageController.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import path from 'path';
import fs from 'fs';
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
router.post('/', createPredefinedActivity);
router.get('/', getAllPredefinedActivities);
router.get('/:id', getPredefinedActivityById);
router.put('/:id', updatePredefinedActivity);
router.post('/', authenticate, createPredefinedActivity);
router.get('/', authenticate, getAllPredefinedActivities);
router.get('/:id', authenticate, getPredefinedActivityById);
router.put('/:id', authenticate, updatePredefinedActivity);
router.post('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.put('/:id/image', authenticate, upload.single('image'), uploadPredefinedActivityImage);
router.delete('/:id/image/:imageId', authenticate, deletePredefinedActivityImage);
router.get('/search/query', authenticate, searchPredefinedActivities);
router.post('/merge', authenticate, mergePredefinedActivities);
router.post('/deduplicate', authenticate, deduplicatePredefinedActivities);
router.get('/:id/image/:imageId', async (req, res) => {
try {
const { id, imageId } = req.params;
const image = await PredefinedActivityImage.findOne({ where: { id: imageId, predefinedActivityId: id } });
if (!image) return res.status(404).json({ error: 'Image not found' });
if (!fs.existsSync(image.imagePath)) return res.status(404).json({ error: 'Image file missing' });
res.sendFile(path.resolve(image.imagePath));
} catch (e) {
console.error('[getPredefinedActivityImage] - Error:', e);
res.status(500).json({ error: 'Failed to fetch image' });
}
});
export default router;

View File

@@ -0,0 +1,100 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
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',
};
async function getTables(connection) {
const [rows] = await connection.execute(
`SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE='BASE TABLE'`,
[dbConfig.database]
);
return rows.map(r => r.TABLE_NAME);
}
async function getIndexSummary(connection, table) {
const [rows] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
const byName = rows.reduce((acc, r) => {
const key = r.Key_name;
if (!acc[key]) acc[key] = { nonUnique: r.Non_unique === 1, seqMap: {}, columns: [] };
acc[key].seqMap[r.Seq_in_index] = r.Column_name;
return acc;
}, {});
// normalize columns order by seq
for (const name of Object.keys(byName)) {
const cols = Object.keys(byName[name].seqMap)
.sort((a, b) => Number(a) - Number(b))
.map(k => byName[name].seqMap[k]);
byName[name].columns = cols;
}
return byName;
}
async function cleanupDuplicates(connection, table) {
const before = await getIndexSummary(connection, table);
const keepSignatureToName = new Map();
const dropNames = [];
for (const [name, info] of Object.entries(before)) {
if (name === 'PRIMARY') continue; // niemals Primary droppen
const uniqueFlag = info.nonUnique ? 'N' : 'U';
const sig = `${uniqueFlag}|${info.columns.join(',')}`;
if (!keepSignatureToName.has(sig)) {
keepSignatureToName.set(sig, name);
} else {
// doppelter Index mit gleicher Spaltenliste und gleicher Einzigartigkeit
dropNames.push(name);
}
}
for (const idxName of dropNames) {
try {
await connection.execute(`DROP INDEX \`${idxName}\` ON \`${table}\``);
console.log(`[drop] ${table}: ${idxName}`);
} catch (e) {
console.warn(`[warn] ${table}: konnte Index ${idxName} nicht löschen: ${e.code || e.message}`);
}
}
const after = await getIndexSummary(connection, table);
return { beforeCount: Object.keys(before).length, afterCount: Object.keys(after).length, dropped: dropNames.length };
}
async function main() {
let connection;
try {
console.log('Connecting to DB:', dbConfig);
connection = await mysql.createConnection(dbConfig);
const tables = await getTables(connection);
console.log(`Found ${tables.length} tables`);
let totalBefore = 0;
let totalAfter = 0;
let totalDropped = 0;
for (const table of tables) {
const { beforeCount, afterCount, dropped } = await cleanupDuplicates(connection, table);
totalBefore += beforeCount;
totalAfter += afterCount;
totalDropped += dropped;
}
console.log('Summary:', { totalBefore, totalAfter, totalDropped });
} catch (e) {
console.error('Cleanup failed:', e);
process.exitCode = 1;
} finally {
if (connection) await connection.end();
}
}
main();

View File

@@ -0,0 +1,90 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
dotenv.config();
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',
};
async function getIndexSummary(connection, table) {
const [rows] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
const summary = rows.reduce((acc, r) => {
const key = r.Key_name;
acc[key] = acc[key] || { unique: r.Non_unique === 0, columns: [] };
acc[key].columns.push(r.Column_name);
return acc;
}, {});
return summary;
}
async function cleanupUserTokenKeys() {
let connection;
const table = 'UserToken';
try {
console.log('Connecting to DB:', dbConfig);
connection = await mysql.createConnection(dbConfig);
console.log(`\nBefore cleanup (indexes on ${table}):`);
let before = await getIndexSummary(connection, table);
Object.entries(before).forEach(([name, info]) => {
console.log(` - ${name} ${info.unique ? '(UNIQUE)' : ''} -> [${info.columns.join(', ')}]`);
});
// Drop all non-PRIMARY indexes on UserToken
const [indexes] = await connection.execute(`SHOW INDEX FROM \`${table}\``);
const keyNames = Array.from(new Set(indexes.map(i => i.Key_name))).filter(k => k !== 'PRIMARY');
for (const keyName of keyNames) {
try {
await connection.execute(`DROP INDEX \`${keyName}\` ON \`${table}\``);
console.log(`Dropped index: ${keyName}`);
} catch (err) {
console.warn(`Could not drop ${keyName}: ${err.code || err.message}`);
}
}
// Re-create minimal, deterministic indexes
// Unique on token (column is 'token')
try {
await connection.execute(`CREATE UNIQUE INDEX \`uniq_UserToken_token\` ON \`${table}\` (\`token\`)`);
console.log('Created UNIQUE index: uniq_UserToken_token (token)');
} catch (err) {
console.warn('Could not create uniq_UserToken_token:', err.code || err.message);
}
// Helpful index on user_id if column exists
try {
const [cols] = await connection.execute(`SHOW COLUMNS FROM \`${table}\` LIKE 'user_id'`);
if (cols && cols.length > 0) {
await connection.execute(`CREATE INDEX \`idx_UserToken_user_id\` ON \`${table}\` (\`user_id\`)`);
console.log('Created INDEX: idx_UserToken_user_id (user_id)');
} else {
console.log('Column user_id not found, skip creating idx_UserToken_user_id');
}
} catch (err) {
console.warn('Could not create idx_UserToken_user_id:', err.code || err.message);
}
console.log(`\nAfter cleanup (indexes on ${table}):`);
const after = await getIndexSummary(connection, table);
Object.entries(after).forEach(([name, info]) => {
console.log(` - ${name} ${info.unique ? '(UNIQUE)' : ''} -> [${info.columns.join(', ')}]`);
});
console.log('\nDone.');
} catch (err) {
console.error('Cleanup failed:', err);
process.exitCode = 1;
} finally {
if (connection) await connection.end();
}
}
cleanupUserTokenKeys();

View File

@@ -6,9 +6,9 @@ import cors from 'cors';
import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, DiaryDateActivity, Match, League, Team, Group,
PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, Group,
GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult,
TournamentMember, Accident, UserToken
TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis
} from './models/index.js';
import authRoutes from './routes/authRoutes.js';
import clubRoutes from './routes/clubRoutes.js';
@@ -22,6 +22,7 @@ import diaryNoteRoutes from './routes/diaryNoteRoutes.js';
import diaryMemberRoutes from './routes/diaryMemberRoutes.js';
import predefinedActivityRoutes from './routes/predefinedActivityRoutes.js';
import diaryDateActivityRoutes from './routes/diaryDateActivityRoutes.js';
import diaryMemberActivityRoutes from './routes/diaryMemberActivityRoutes.js';
import matchRoutes from './routes/matchRoutes.js';
import Season from './models/Season.js';
import Location from './models/Location.js';
@@ -31,6 +32,8 @@ import sessionRoutes from './routes/sessionRoutes.js';
import tournamentRoutes from './routes/tournamentRoutes.js';
import accidentRoutes from './routes/accidentRoutes.js';
import trainingStatsRoutes from './routes/trainingStatsRoutes.js';
import officialTournamentRoutes from './routes/officialTournamentRoutes.js';
import myTischtennisRoutes from './routes/myTischtennisRoutes.js';
const app = express();
const port = process.env.PORT || 3000;
@@ -38,9 +41,22 @@ const port = process.env.PORT || 3000;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.use(cors());
app.use(cors({
origin: true,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid']
}));
app.use(express.json());
// Globale Fehlerbehandlung, damit der Server bei unerwarteten Fehlern nicht hart abstürzt
process.on('uncaughtException', (err) => {
console.error('[uncaughtException]', err);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('[unhandledRejection]', reason);
});
app.use('/api/auth', authRoutes);
app.use('/api/clubs', clubRoutes);
app.use('/api/clubmembers', memberRoutes);
@@ -53,6 +69,7 @@ app.use('/api/tags', diaryTagRoutes);
app.use('/api/diarymember', diaryMemberRoutes);
app.use('/api/predefined-activities', predefinedActivityRoutes);
app.use('/api/diary-date-activities', diaryDateActivityRoutes);
app.use('/api/diary-member-activities', diaryMemberActivityRoutes);
app.use('/api/matches', matchRoutes);
app.use('/api/group', groupRoutes);
app.use('/api/diarydatetags', diaryDateTagRoutes);
@@ -60,6 +77,8 @@ app.use('/api/session', sessionRoutes);
app.use('/api/tournament', tournamentRoutes);
app.use('/api/accident', accidentRoutes);
app.use('/api/training-stats', trainingStatsRoutes);
app.use('/api/official-tournaments', officialTournamentRoutes);
app.use('/api/mytischtennis', myTischtennisRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));
@@ -72,37 +91,94 @@ app.get('*', (req, res) => {
try {
await sequelize.authenticate();
await User.sync({ alter: true });
await Club.sync({ alter: true });
await UserClub.sync({ alter: true });
await Log.sync({ alter: true });
await Member.sync({ alter: true });
await DiaryDate.sync({ alter: true });
await Participant.sync({ alter: true });
await Activity.sync({ alter: true });
await MemberNote.sync({ alter: true });
await DiaryNote.sync({ alter: true });
await DiaryTag.sync({ alter: true });
await MemberDiaryTag.sync({ alter: true });
await DiaryDateTag.sync({ alter: true });
await DiaryMemberTag.sync({ alter: true });
await DiaryMemberNote.sync({ alter: true });
await PredefinedActivity.sync({ alter: true });
await DiaryDateActivity.sync({ alter: true });
await Season.sync({ alter: true });
await League.sync({ alter: true });
await Team.sync({ alter: true });
await Location.sync({ alter: true });
await Match.sync({ alter: true });
await Group.sync({ alter: true });
await GroupActivity.sync({ alter: true });
await Tournament.sync({ alter: true });
await TournamentGroup.sync({ alter: true });
await TournamentMember.sync({ alter: true });
await TournamentMatch.sync({ alter: true });
await TournamentResult.sync({ alter: true });
await Accident.sync({ alter: true });
await UserToken.sync({ alter: true });
// Einmalige Migration: deutsche Spaltennamen -> englische
const renameColumnIfExists = async (table, from, to, typeSql) => {
try {
const [rows] = await sequelize.query(`SHOW COLUMNS FROM \`${table}\` LIKE :col`, { replacements: { col: from } });
if (Array.isArray(rows) && rows.length > 0) {
await sequelize.query(`ALTER TABLE \`${table}\` CHANGE \`${from}\` \`${to}\` ${typeSql}`);
}
} catch (e) {
console.error(`[migration] Failed to rename ${table}.${from} -> ${to}:`, e.message);
}
};
// official_competitions
await renameColumnIfExists('official_competitions', 'altersklasse_wettbewerb', 'age_class_competition', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'leistungsklasse', 'performance_class', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'startzeit', 'start_time', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'meldeschluss_datum', 'registration_deadline_date', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'meldeschluss_online', 'registration_deadline_online', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'stichtag', 'cutoff_date', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'offen_fuer', 'open_to', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'vorrunde', 'preliminary_round', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'endrunde', 'final_round', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'max_teilnehmer', 'max_participants', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_competitions', 'startgeld', 'entry_fee', 'VARCHAR(255) NULL');
// official_tournaments
await renameColumnIfExists('official_tournaments', 'termin', 'event_date', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_tournaments', 'veranstalter', 'organizer', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_tournaments', 'ausrichter', 'host', 'VARCHAR(255) NULL');
await renameColumnIfExists('official_tournaments', 'austragungsorte', 'venues', 'TEXT NULL');
await renameColumnIfExists('official_tournaments', 'konkurrenztypen', 'competition_types', 'TEXT NULL');
await renameColumnIfExists('official_tournaments', 'meldeschluesse', 'registration_deadlines', 'TEXT NULL');
const isDev = process.env.STAGE === 'dev';
const safeSync = async (model) => {
try {
if (isDev) {
await model.sync({ alter: true });
} else {
await model.sync();
}
} catch (e) {
try {
console.error(`[sync] ${model?.name || 'model'} alter failed:`, e?.message || e);
await model.sync();
} catch (e2) {
console.error(`[sync] fallback failed for ${model?.name || 'model'}:`, e2?.message || e2);
}
}
};
await safeSync(User);
await safeSync(Club);
await safeSync(UserClub);
await safeSync(Log);
await safeSync(Member);
await safeSync(DiaryDate);
await safeSync(Participant);
await safeSync(Activity);
await safeSync(MemberNote);
await safeSync(DiaryNote);
await safeSync(DiaryTag);
await safeSync(MemberDiaryTag);
await safeSync(DiaryDateTag);
await safeSync(DiaryMemberTag);
await safeSync(DiaryMemberNote);
await safeSync(PredefinedActivity);
await safeSync(PredefinedActivityImage);
await safeSync(DiaryDateActivity);
await safeSync(DiaryMemberActivity);
await safeSync(OfficialTournament);
await safeSync(OfficialCompetition);
await safeSync(OfficialCompetitionMember);
await safeSync(Season);
await safeSync(League);
await safeSync(Team);
await safeSync(Location);
await safeSync(Match);
await safeSync(Group);
await safeSync(GroupActivity);
await safeSync(Tournament);
await safeSync(TournamentGroup);
await safeSync(TournamentMember);
await safeSync(TournamentMatch);
await safeSync(TournamentResult);
await safeSync(Accident);
await safeSync(UserToken);
await safeSync(MyTischtennis);
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);

View File

@@ -3,6 +3,7 @@ import DiaryDate from '../models/DiaryDates.js';
import Member from '../models/Member.js';
import { checkAccess, getUserByToken} from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
class AccidentService {
async createAccident(userToken, clubId, memberId, diaryDateId, accident) {
await checkAccess(userToken, clubId);
@@ -14,7 +15,7 @@ class AccidentService {
if (!member || member.clubId != clubId) {
throw new Error('Member not found');
}
console.log(diaryDateId);
devLog(diaryDateId);
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (!diaryDate || diaryDate.clubId != clubId) {
throw new Error('Diary date not found');

View File

@@ -4,6 +4,7 @@ import User from '../models/User.js';
import UserToken from '../models/UserToken.js';
import { sendActivationEmail } from './emailService.js';
import { devLog } from '../utils/logger.js';
const register = async (email, password) => {
try {
const activationCode = Math.random().toString(36).substring(2, 15);
@@ -11,7 +12,7 @@ const register = async (email, password) => {
await sendActivationEmail(email, activationCode);
return user;
} catch (error) {
console.log(error);
devLog(error);
return null;
}
};
@@ -33,11 +34,11 @@ const login = async (email, password) => {
if (!user || !(await bcrypt.compare(password, user.password))) {
throw { status: 401, message: 'Ungültige Anmeldedaten' };
}
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '3h' });
await UserToken.create({
userId: user.id,
token,
expiresAt: new Date(Date.now() + 3600 * 1000),
expiresAt: new Date(Date.now() + 3 * 3600 * 1000),
});
return { token };
};

View File

@@ -2,44 +2,105 @@ import DiaryDateActivity from '../models/DiaryDateActivity.js';
import GroupActivity from '../models/GroupActivity.js';
import Group from '../models/Group.js';
import PredefinedActivity from '../models/PredefinedActivity.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import { checkAccess } from '../utils/userUtils.js';
import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class DiaryDateActivityService {
async createActivity(userToken, clubId, data) {
console.log('[DiaryDateActivityService::createActivity] - check user access');
devLog('[DiaryDateActivityService::createActivity] - check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::createActivity] - add: ', data);
devLog('[DiaryDateActivityService::createActivity] - add: ', data);
const { activity, ...restData } = data;
let predefinedActivity = await PredefinedActivity.findOne({ where: { name: data.activity } });
// Versuche, die PredefinedActivity robust zu finden:
// 1) per übergebener ID
// 2) per Name ODER Code (das Feld "activity" kann Kürzel oder Name sein)
// 3) erst dann neu anlegen
let predefinedActivity = null;
if (data.predefinedActivityId) {
predefinedActivity = await PredefinedActivity.findByPk(data.predefinedActivityId);
}
if (!predefinedActivity) {
const normalized = (data.activity || '').trim();
if (normalized) {
predefinedActivity = await PredefinedActivity.findOne({
where: {
[Op.or]: [
{ name: normalized },
{ code: normalized }
]
}
});
}
}
if (!predefinedActivity) {
predefinedActivity = await PredefinedActivity.create({
name: data.activity,
description: '',
duration: data.duration
name: data.name || data.activity || '',
code: data.code || (data.activity || ''),
description: data.description || '',
duration: data.duration && data.duration !== '' ? parseInt(data.duration) : null
});
}
restData.predefinedActivityId = predefinedActivity.id;
restData.predefinedActivityId = predefinedActivity.id;
// Bereinige duration-Feld für DiaryDateActivity
if (restData.duration === '' || restData.duration === undefined) {
restData.duration = null;
} else if (typeof restData.duration === 'string') {
restData.duration = parseInt(restData.duration);
}
const maxOrderId = await DiaryDateActivity.max('orderId', {
where: { diaryDateId: data.diaryDateId }
where: { diaryDateId: data.diaryDateId }
});
const newOrderId = maxOrderId !== null ? maxOrderId + 1 : 1;
restData.orderId = newOrderId;
console.log('[DiaryDateActivityService::createActivity] - create diary date activity');
devLog('[DiaryDateActivityService::createActivity] - create diary date activity');
return await DiaryDateActivity.create(restData);
}
async updateActivity(userToken, clubId, id, data) {
console.log('[DiaryDateActivityService::updateActivity] - check user access');
devLog('[DiaryDateActivityService::updateActivity] - check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::updateActivity] - load activity', id);
devLog('[DiaryDateActivityService::updateActivity] - load activity', id);
const activity = await DiaryDateActivity.findByPk(id);
if (!activity) {
console.log('[DiaryDateActivityService::updateActivity] - activity not found');
devLog('[DiaryDateActivityService::updateActivity] - activity not found');
throw new Error('Activity not found');
}
console.log('[DiaryDateActivityService::updateActivity] - update activity');
// Wenn customActivityName gesendet wird, müssen wir die PredefinedActivity behandeln
if (data.customActivityName) {
devLog('[DiaryDateActivityService::updateActivity] - handling customActivityName:', data.customActivityName);
// Suche nach einer existierenden PredefinedActivity mit diesem Namen
let predefinedActivity = await PredefinedActivity.findOne({
where: { name: data.customActivityName }
});
if (!predefinedActivity) {
// Erstelle eine neue PredefinedActivity
devLog('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity');
predefinedActivity = await PredefinedActivity.create({
name: data.customActivityName,
description: data.description || '',
duration: data.duration && data.duration !== '' ? parseInt(data.duration) : (activity.duration || null)
});
}
// Setze die predefinedActivityId
data.predefinedActivityId = predefinedActivity.id;
// Entferne customActivityName aus den zu aktualisierenden Daten
delete data.customActivityName;
}
devLog('[DiaryDateActivityService::updateActivity] - update activity', clubId, id, data, JSON.stringify(data));
return await activity.update(data);
}
@@ -53,22 +114,22 @@ class DiaryDateActivityService {
}
async updateActivityOrder(userToken, clubId, id, newOrderId) {
console.log(`[DiaryDateActivityService::updateActivityOrder] - Start update for activity id: ${id}`);
console.log(`[DiaryDateActivityService::updateActivityOrder] - User token: ${userToken}, Club id: ${clubId}, New order id: ${newOrderId}`);
console.log('[DiaryDateActivityService::updateActivityOrder] - Checking user access');
devLog(`[DiaryDateActivityService::updateActivityOrder] - Start update for activity id: ${id}`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - User token: ${userToken}, Club id: ${clubId}, New order id: ${newOrderId}`);
devLog('[DiaryDateActivityService::updateActivityOrder] - Checking user access');
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::updateActivityOrder] - User access confirmed');
console.log(`[DiaryDateActivityService::updateActivityOrder] - Finding activity with id: ${id}`);
devLog('[DiaryDateActivityService::updateActivityOrder] - User access confirmed');
devLog(`[DiaryDateActivityService::updateActivityOrder] - Finding activity with id: ${id}`);
const activity = await DiaryDateActivity.findByPk(id);
if (!activity) {
console.error('[DiaryDateActivityService::updateActivityOrder] - Activity not found, throwing error');
throw new Error('Activity not found');
}
console.log('[DiaryDateActivityService::updateActivityOrder] - Activity found:', activity);
devLog('[DiaryDateActivityService::updateActivityOrder] - Activity found:', activity);
const currentOrderId = activity.orderId;
console.log(`[DiaryDateActivityService::updateActivityOrder] - Current order id: ${currentOrderId}`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Current order id: ${currentOrderId}`);
if (newOrderId < currentOrderId) {
console.log(`[DiaryDateActivityService::updateActivityOrder] - Shifting items down. Moving activities with orderId between ${newOrderId} and ${currentOrderId - 1}`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Shifting items down. Moving activities with orderId between ${newOrderId} and ${currentOrderId - 1}`);
await DiaryDateActivity.increment(
{ orderId: 1 },
{
@@ -78,9 +139,9 @@ class DiaryDateActivityService {
},
}
);
console.log(`[DiaryDateActivityService::updateActivityOrder] - Items shifted down`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Items shifted down`);
} else if (newOrderId > currentOrderId) {
console.log(`[DiaryDateActivityService::updateActivityOrder] - Shifting items up. Moving activities with orderId between ${currentOrderId + 1} and ${newOrderId}`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Shifting items up. Moving activities with orderId between ${currentOrderId + 1} and ${newOrderId}`);
await DiaryDateActivity.decrement(
{ orderId: 1 },
{
@@ -90,23 +151,21 @@ class DiaryDateActivityService {
},
}
);
console.log(`[DiaryDateActivityService::updateActivityOrder] - Items shifted up`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Items shifted up`);
} else {
console.log('[DiaryDateActivityService::updateActivityOrder] - New order id is the same as the current order id. No shift required.');
devLog('[DiaryDateActivityService::updateActivityOrder] - New order id is the same as the current order id. No shift required.');
}
console.log(`[DiaryDateActivityService::updateActivityOrder] - Setting new order id for activity id: ${id}`);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Setting new order id for activity id: ${id}`);
activity.orderId = newOrderId;
console.log('[DiaryDateActivityService::updateActivityOrder] - Saving activity with new order id');
devLog('[DiaryDateActivityService::updateActivityOrder] - Saving activity with new order id');
const savedActivity = await activity.save();
console.log('[DiaryDateActivityService::updateActivityOrder] - Activity saved:', savedActivity);
console.log(`[DiaryDateActivityService::updateActivityOrder] - Finished update for activity id: ${id}`);
devLog('[DiaryDateActivityService::updateActivityOrder] - Activity saved:', savedActivity);
devLog(`[DiaryDateActivityService::updateActivityOrder] - Finished update for activity id: ${id}`);
return savedActivity;
}
async getActivities(userToken, clubId, diaryDateId) {
console.log('[DiaryDateActivityService::getActivities] - check user access');
await checkAccess(userToken, clubId);
console.log(`[DiaryDateActivityService::getActivities] - fetch activities for diaryDateId: ${diaryDateId}`);
const activities = await DiaryDateActivity.findAll({
where: { diaryDateId },
order: [['orderId', 'ASC']],
@@ -114,6 +173,12 @@ class DiaryDateActivityService {
{
model: PredefinedActivity,
as: 'predefinedActivity',
include: [
{
model: PredefinedActivityImage,
as: 'images'
}
]
},
{
model: GroupActivity,
@@ -131,14 +196,70 @@ class DiaryDateActivityService {
}
]
});
console.log(`[DiaryDateActivityService::getActivities] - found ${activities.length} activities`);
return activities;
// Füge imageUrl zu jeder PredefinedActivity hinzu
const activitiesWithImages = await Promise.all(activities.map(async activity => {
// Konvertiere zu JSON und zurück, um alle Eigenschaften zu serialisieren
const activityData = activity.toJSON();
if (activityData.predefinedActivity) {
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
const allImages = await PredefinedActivityImage.findAll({
where: { predefinedActivityId: activityData.predefinedActivity.id },
order: [['createdAt', 'ASC']]
});
const firstImage = allImages.length > 0 ? allImages[0] : null;
// Füge Zeichnungsdaten hinzu, falls vorhanden
if (firstImage && firstImage.drawingData) {
try {
activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
} catch (error) {
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
}
}
if (firstImage) {
// Füge sowohl imageUrl als auch imageLink mit Image-ID hinzu
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image/${firstImage.id}`;
} else {
// Fallback: Verwende den Basis-Pfad ohne Image-ID
activityData.predefinedActivity.imageUrl = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
activityData.predefinedActivity.imageLink = `/api/predefined-activities/${activityData.predefinedActivity.id}/image`;
}
}
// Auch für GroupActivities
if (activityData.groupActivities && activityData.groupActivities.length > 0) {
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({
where: { predefinedActivityId: groupActivity.groupPredefinedActivity.id },
order: [['createdAt', 'ASC']]
});
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}`;
} else {
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`;
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image`;
}
}
}
}
return activityData;
}));
return activitiesWithImages;
}
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity) {
console.log('[DiaryDateActivityService::addGroupActivity] Check user access');
devLog('[DiaryDateActivityService::addGroupActivity] Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryDateActivityService::addGroupActivity] Check diary date');
devLog('[DiaryDateActivityService::addGroupActivity] Check diary date');
const diaryDateActivity = await DiaryDateActivity.findOne({
where: {
diaryDateId,
@@ -151,26 +272,26 @@ class DiaryDateActivityService {
console.error('[DiaryDateActivityService::addGroupActivity] Activity not found');
throw new Error('Activity not found');
}
console.log('[DiaryDateActivityService::addGroupActivity] Check group');
devLog('[DiaryDateActivityService::addGroupActivity] Check group');
const group = await Group.findByPk(groupId);
if (!group || group.diaryDateId !== diaryDateActivity.diaryDateId) {
console.error('[DiaryDateActivityService::addGroupActivity] Group and date don\'t fit');
throw new Error('Group isn\'t related to date');
}
console.log('[DiaryDateActivityService::addGroupActivity] Get predefined activity');
devLog('[DiaryDateActivityService::addGroupActivity] Get predefined activity');
const [predefinedActivity, created] = await PredefinedActivity.findOrCreate({
where: {
name: activity
}
});
console.log('[DiaryDateActivityService::addGroupActivity] Add group activity');
console.log(predefinedActivity);
devLog('[DiaryDateActivityService::addGroupActivity] Add group activity');
devLog(predefinedActivity);
const activityData = {
diaryDateActivity: diaryDateActivity.id,
groupId: groupId,
customActivity: predefinedActivity.id
}
console.log(activityData);
devLog(activityData);
return await GroupActivity.create(activityData);
}
}

View File

@@ -6,6 +6,7 @@ import Member from "../models/Member.js";
import { checkAccess } from '../utils/userUtils.js';
import { Op, literal } from "sequelize";
import { devLog } from '../utils/logger.js';
class DiaryDateTagService {
async getDiaryDateMemberTags(userToken, clubId, memberId) {
await checkAccess(userToken, clubId);
@@ -35,7 +36,7 @@ class DiaryDateTagService {
}
async addDiaryDateTag(userToken, clubId, diaryDateId, memberId, tag) {
console.log(userToken, clubId, diaryDateId, memberId, tag);
devLog(userToken, clubId, diaryDateId, memberId, tag);
await checkAccess(userToken, clubId);
const tagObject = await DiaryTag.findOne({ where: { id: tag.id } });
if (!tagObject) {

View File

@@ -3,6 +3,7 @@ import DiaryMemberTag from '../models/DiaryMemberTag.js';
import { DiaryTag } from '../models/DiaryTag.js';
import { checkAccess } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
class DiaryMemberService {
async addNoteToMember(userToken, clubId, diaryDateId, memberId, content) {
await checkAccess(userToken, clubId);
@@ -14,7 +15,7 @@ class DiaryMemberService {
async getNotesForMember(userToken, clubId, diaryDateId, memberId) {
await checkAccess(userToken, clubId);
console.log(clubId, diaryDateId, memberId);
devLog(clubId, diaryDateId, memberId);
return await DiaryMemberNote.findAll({ where: { diaryDateId, memberId }, order: [['createdAt', 'DESC']] });
}
@@ -50,7 +51,7 @@ class DiaryMemberService {
if (tagLink) {
await tagLink.destroy();
} else {
console.log(diaryDateId, memberId, tagId);
devLog(diaryDateId, memberId, tagId);
throw new Error('Das Tag ist nicht verknüpft.');
}
}

View File

@@ -1,4 +1,5 @@
import DiaryDate from '../models/DiaryDates.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import Club from '../models/Club.js';
import DiaryNote from '../models/DiaryNote.js';
import { DiaryTag } from '../models/DiaryTag.js';
@@ -6,16 +7,17 @@ import DiaryDateTag from '../models/DiaryDateTag.js';
import { checkAccess } from '../utils/userUtils.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class DiaryService {
async getDatesForClub(userToken, clubId) {
console.log('[DiaryService::getDatesForClub] - Check user access');
devLog('[DiaryService::getDatesForClub] - Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryService::getDatesForClub] - Validate club existence');
devLog('[DiaryService::getDatesForClub] - Validate club existence');
const club = await Club.findByPk(clubId);
if (!club) {
throw new HttpError('Club not found', 404);
}
console.log('[DiaryService::getDatesForClub] - Load diary dates');
devLog('[DiaryService::getDatesForClub] - Load diary dates');
const dates = await DiaryDate.findAll({
where: { clubId },
include: [
@@ -28,14 +30,14 @@ class DiaryService {
}
async createDateForClub(userToken, clubId, date, trainingStart, trainingEnd) {
console.log('[DiaryService::createDateForClub] - Check user access');
devLog('[DiaryService::createDateForClub] - Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryService::createDateForClub] - Validate club existence');
devLog('[DiaryService::createDateForClub] - Validate club existence');
const club = await Club.findByPk(clubId);
if (!club) {
throw new HttpError('Club not found', 404);
}
console.log('[DiaryService::createDateForClub] - Validate date');
devLog('[DiaryService::createDateForClub] - Validate date');
const parsedDate = new Date(date);
if (isNaN(parsedDate.getTime())) {
throw new HttpError('Invalid date format', 400);
@@ -43,7 +45,7 @@ class DiaryService {
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
throw new HttpError('Training start time must be before training end time', 400);
}
console.log('[DiaryService::createDateForClub] - Create new diary date');
devLog('[DiaryService::createDateForClub] - Create new diary date');
const newDate = await DiaryDate.create({
date: parsedDate,
clubId,
@@ -55,9 +57,9 @@ class DiaryService {
}
async updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd) {
console.log('[DiaryService::updateTrainingTimes] - Check user access');
devLog('[DiaryService::updateTrainingTimes] - Check user access');
await checkAccess(userToken, clubId);
console.log('[DiaryService::updateTrainingTimes] - Validate date');
devLog('[DiaryService::updateTrainingTimes] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { clubId, id: dateId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
@@ -65,7 +67,7 @@ class DiaryService {
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
throw new HttpError('Training start time must be before training end time', 400);
}
console.log('[DiaryService::updateTrainingTimes] - Update training times');
devLog('[DiaryService::updateTrainingTimes] - Update training times');
diaryDate.trainingStart = trainingStart || null;
diaryDate.trainingEnd = trainingEnd || null;
await diaryDate.save();
@@ -73,14 +75,14 @@ class DiaryService {
}
async addNoteToDate(userToken, diaryDateId, content) {
console.log('[DiaryService::addNoteToDate] - Add note');
devLog('[DiaryService::addNoteToDate] - Add note');
await checkAccess(userToken, diaryDateId);
await DiaryNote.create({ diaryDateId, content });
return await DiaryNote.findAll({ where: { diaryDateId }, order: [['createdAt', 'DESC']] });
}
async deleteNoteFromDate(userToken, noteId) {
console.log('[DiaryService::deleteNoteFromDate] - Delete note');
devLog('[DiaryService::deleteNoteFromDate] - Delete note');
const note = await DiaryNote.findByPk(noteId);
if (!note) {
throw new HttpError('Note not found', 404);
@@ -91,7 +93,7 @@ class DiaryService {
}
async addTagToDate(userToken, diaryDateId, tagName) {
console.log('[DiaryService::addTagToDate] - Add tag');
devLog('[DiaryService::addTagToDate] - Add tag');
await checkAccess(userToken, diaryDateId);
let tag = await DiaryTag.findOne({ where: { name: tagName } });
if (!tag) {
@@ -104,29 +106,29 @@ class DiaryService {
async addTagToDiaryDate(userToken, clubId, diaryDateId, tagId) {
checkAccess(userToken, clubId);
console.log(`[DiaryService::addTagToDiaryDate] - diaryDateId: ${diaryDateId}, tagId: ${tagId}`);
devLog(`[DiaryService::addTagToDiaryDate] - diaryDateId: ${diaryDateId}, tagId: ${tagId}`);
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (!diaryDate) {
throw new HttpError('DiaryDate not found', 404);
}
console.log('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
devLog('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
const existingEntry = await DiaryDateTag.findOne({
where: { diaryDateId, tagId }
});
if (existingEntry) {
return;
}
console.log('[DiaryService::addTagToDiaryDate] - Tag not found, creating new entry');
devLog('[DiaryService::addTagToDiaryDate] - Tag not found, creating new entry');
const tag = await DiaryTag.findByPk(tagId);
if (!tag) {
throw new HttpError('Tag not found', 404);
}
console.log('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
devLog('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
await DiaryDateTag.create({
diaryDateId,
tagId
});
console.log('[DiaryService::addTagToDiaryDate] - Get tags');
devLog('[DiaryService::addTagToDiaryDate] - Get tags');
const tags = await DiaryDateTag.findAll({ where: {
diaryDateId: diaryDateId },
include: {
@@ -134,12 +136,12 @@ class DiaryService {
as: 'tag'
}
});
console.log(tags);
devLog(tags);
return tags.map(tag => tag.tag);
}
async getDiaryNotesForDateAndMember(diaryDateId, memberId) {
console.log('[DiaryService::getDiaryNotesForDateAndMember] - Fetching notes');
devLog('[DiaryService::getDiaryNotesForDateAndMember] - Fetching notes');
return await DiaryNote.findAll({
where: { diaryDateId, memberId },
order: [['createdAt', 'DESC']]
@@ -151,6 +153,23 @@ class DiaryService {
await DiaryDateTag.destroy({ where: { tagId } });
}
async removeDateForClub(userToken, clubId, dateId) {
devLog('[DiaryService::removeDateForClub] - Check user access');
await checkAccess(userToken, clubId);
devLog('[DiaryService::removeDateForClub] - Validate date');
const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } });
if (!diaryDate) {
throw new HttpError('Diary entry not found', 404);
}
devLog('[DiaryService::removeDateForClub] - Check for activities');
const activityCount = await DiaryDateActivity.count({ where: { diaryDateId: dateId } });
if (activityCount > 0) {
throw new HttpError('Cannot delete date with activities', 409);
}
devLog('[DiaryService::removeDateForClub] - Delete diary date');
await diaryDate.destroy();
return { ok: true };
}
}
export default new DiaryService();

View File

@@ -0,0 +1,347 @@
import ExternalServiceAccount from '../models/ExternalServiceAccount.js';
import User from '../models/User.js';
import myTischtennisClient from '../clients/myTischtennisClient.js';
import hettvClient from '../clients/hettvClient.js';
import HttpError from '../exceptions/HttpError.js';
class ExternalServiceService {
/**
* Get the appropriate client for a service
*/
getClientForService(service) {
switch (service) {
case 'mytischtennis':
return myTischtennisClient;
case 'hettv':
return hettvClient;
default:
throw new HttpError(400, `Unbekannter Service: ${service}`);
}
}
/**
* Get account for user and service
*/
async getAccount(userId, service = 'mytischtennis') {
const account = await ExternalServiceAccount.findOne({
where: { userId, service },
attributes: ['id', 'service', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
});
return account;
}
/**
* Create or update external service account
*/
async upsertAccount(userId, email, password, savePassword, userPassword, service = 'mytischtennis') {
// Verify user's app password
const user = await User.findByPk(userId);
if (!user) {
throw new HttpError(404, 'Benutzer nicht gefunden');
}
let loginResult = null;
// Wenn ein Passwort gesetzt/geändert wird, App-Passwort verifizieren
if (password) {
const isValidPassword = await user.validatePassword(userPassword);
if (!isValidPassword) {
throw new HttpError(401, 'Ungültiges Passwort');
}
// Login-Versuch beim entsprechenden Service
const client = this.getClientForService(service);
loginResult = await client.login(email, password);
if (!loginResult.success) {
throw new HttpError(401, loginResult.error || `${service}-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.`);
}
}
// Find or create account
let account = await ExternalServiceAccount.findOne({ where: { userId, service } });
const now = new Date();
if (account) {
// Update existing
account.email = email;
account.savePassword = savePassword;
if (password && savePassword) {
account.setPassword(password);
} else if (!savePassword) {
account.encryptedPassword = null;
}
if (loginResult && loginResult.success) {
account.lastLoginAttempt = now;
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-ID und Federation (nur für myTischtennis)
if (service === 'mytischtennis') {
console.log('[externalServiceService] - Getting user profile...');
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
console.log('[externalServiceService] - Profile result:', {
success: profileResult.success,
clubId: profileResult.clubId,
clubName: profileResult.clubName,
fedNickname: profileResult.fedNickname
});
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
console.log('[externalServiceService] - Updated account with club data');
} else {
console.error('[externalServiceService] - Failed to get profile:', profileResult.error);
}
}
} else if (password) {
account.lastLoginAttempt = now;
}
console.log('[externalServiceService] - Speichere Account (update).');
try {
await account.save();
} catch (e) {
console.error('[externalServiceService] - Fehler beim Speichern (update):', e.message, e.parent?.sqlMessage);
throw e;
}
} else {
// Create new
const accountData = {
userId,
service,
email,
savePassword,
lastLoginAttempt: password ? now : null,
lastLoginSuccess: loginResult?.success ? now : null
};
if (loginResult && loginResult.success) {
accountData.accessToken = loginResult.accessToken;
accountData.refreshToken = loginResult.refreshToken;
accountData.expiresAt = loginResult.expiresAt;
accountData.cookie = loginResult.cookie;
accountData.userData = loginResult.user;
// Hole Club-/Verbandsdaten nur für myTischtennis
if (service === 'mytischtennis') {
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
if (profileResult.success) {
accountData.clubId = profileResult.clubId;
accountData.clubName = profileResult.clubName;
accountData.fedNickname = profileResult.fedNickname;
}
}
}
console.log('[externalServiceService] - Erstelle Account (create).');
try {
account = await ExternalServiceAccount.create(accountData);
} catch (e) {
console.error('[externalServiceService] - Fehler beim Erstellen (create):', e.message, e.parent?.sqlMessage);
throw e;
}
if (password && savePassword) {
account.setPassword(password);
console.log('[externalServiceService] - Speichere Passwort (nach create).');
try {
await account.save();
} catch (e) {
console.error('[externalServiceService] - Fehler beim Speichern (nach create):', e.message, e.parent?.sqlMessage);
throw e;
}
}
}
return {
id: account.id,
email: account.email,
savePassword: account.savePassword,
lastLoginAttempt: account.lastLoginAttempt,
lastLoginSuccess: account.lastLoginSuccess,
expiresAt: account.expiresAt
};
}
/**
* Delete external service account
*/
async deleteAccount(userId, service = 'mytischtennis') {
const deleted = await ExternalServiceAccount.destroy({
where: { userId, service }
});
return deleted > 0;
}
/**
* Verify login with stored or provided credentials
*/
async verifyLogin(userId, providedPassword = null, service = 'mytischtennis') {
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
if (!account) {
throw new HttpError(404, `Kein ${service}-Account verknüpft`);
}
let password = providedPassword;
// Wenn kein Passwort übergeben wurde, versuche gespeichertes Passwort zu verwenden
if (!password) {
if (!account.savePassword || !account.encryptedPassword) {
throw new HttpError(400, 'Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.');
}
password = account.getPassword();
}
// Login-Versuch
const now = new Date();
account.lastLoginAttempt = now;
const client = this.getClientForService(service);
const loginResult = await client.login(account.email, password);
if (loginResult.success) {
account.lastLoginSuccess = now;
account.accessToken = loginResult.accessToken;
account.refreshToken = loginResult.refreshToken;
account.expiresAt = loginResult.expiresAt;
account.cookie = loginResult.cookie;
account.userData = loginResult.user;
// Hole Club-/Verbandsdaten nur für myTischtennis
if (service === 'mytischtennis') {
console.log('[externalServiceService] - Getting myTischtennis user profile...');
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
console.log('[externalServiceService] - Profile result:', {
success: profileResult.success,
clubId: profileResult.clubId,
clubName: profileResult.clubName,
fedNickname: profileResult.fedNickname
});
if (profileResult.success) {
account.clubId = profileResult.clubId;
account.clubName = profileResult.clubName;
account.fedNickname = profileResult.fedNickname;
console.log('[externalServiceService] - Updated account with club data');
} else {
console.error('[externalServiceService] - Failed to get profile:', profileResult.error);
}
}
await account.save();
return {
success: true,
accessToken: loginResult.accessToken,
refreshToken: loginResult.refreshToken,
expiresAt: loginResult.expiresAt,
user: loginResult.user,
clubId: account.clubId,
clubName: account.clubName
};
} else {
await account.save(); // Save lastLoginAttempt
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen');
}
}
/**
* Check if account is configured and ready
*/
async checkAccountStatus(userId, service = 'mytischtennis') {
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
return {
exists: !!account,
hasEmail: !!account?.email,
hasPassword: !!(account?.savePassword && account?.encryptedPassword),
hasValidSession: !!account?.accessToken && account?.expiresAt > Date.now() / 1000,
needsConfiguration: !account || !account.email,
needsPassword: !!account && (!account.savePassword || !account.encryptedPassword)
};
}
/**
* Get stored session for user (for authenticated API requests)
*/
async getSession(userId, service = 'mytischtennis') {
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
if (!account) {
throw new HttpError(404, `Kein ${service}-Account verknüpft`);
}
// Check if session is valid
if (service === 'hettv') {
// HeTTV nutzt Cookie-basierte Session, kein expiresAt verfügbar
if (!account.cookie) {
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
}
} else {
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
}
}
return {
accessToken: account.accessToken,
refreshToken: account.refreshToken,
cookie: account.cookie,
expiresAt: account.expiresAt,
userData: account.userData
};
}
/**
* Load HeTTV main page and find download links
*/
async loadHettvMainPage(userId) {
const client = this.getClientForService('hettv');
let session = await this.getSession(userId, 'hettv');
const finalUrl = session.userData?.finalUrl || null;
let result = await client.getMainPageWithDownloads(session.cookie, finalUrl);
// Wenn Session abgelaufen: versuche automatischen Re-Login mit gespeichertem Passwort
if (!result.success && result.status === 401) {
const account = await ExternalServiceAccount.findOne({ where: { userId, service: 'hettv' } });
if (account && account.savePassword && account.encryptedPassword) {
const password = account.getPassword();
const login = await client.login(account.email, password);
if (login.success) {
// Session aktualisieren
account.cookie = login.cookie;
account.userData = login.user;
await account.save();
// Erneut versuchen
result = await client.getMainPageWithDownloads(login.cookie, login.user?.finalUrl || null);
}
}
}
return result;
}
/**
* Load specific HeTTV download page
*/
async loadHettvDownloadPage(userId, downloadUrl) {
const session = await this.getSession(userId, 'hettv');
const client = this.getClientForService('hettv');
const finalUrl = session.userData?.finalUrl || null;
return await client.loadDownloadPage(downloadUrl, session.cookie, finalUrl);
}
}
export default new ExternalServiceService();

View File

@@ -3,6 +3,7 @@ import DiaryDate from '../models/DiaryDates.js';
import HttpError from '../exceptions/HttpError.js';
import Group from '../models/Group.js';
import { devLog } from '../utils/logger.js';
class GroupService {
async checkDiaryDateToClub(clubId, dateId) {
@@ -40,7 +41,7 @@ class GroupService {
}
async changeGroup(userToken, groupId, clubId, dateId, name, lead) {
console.log("changeGroup: ", groupId, clubId, dateId, name, lead);
devLog("changeGroup: ", groupId, clubId, dateId, name, lead);
await checkAccess(userToken, clubId);
await this.checkDiaryDateToClub(clubId, dateId);
const group = await Group.findOne({

View File

@@ -10,6 +10,7 @@ import Team from '../models/Team.js';
import { checkAccess } from '../utils/userUtils.js';
import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class MatchService {
generateSeasonString(date = new Date()) {
@@ -78,7 +79,7 @@ class MatchService {
return result;
} catch (error) {
console.log('Error during CSV import:', error);
devLog('Error during CSV import:', error);
throw error;
}
}

View File

@@ -1,6 +1,7 @@
import MemberNote from '../models/MemberNote.js';
import { checkAccess } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
class MemberNoteService {
async addNoteToMember(userToken, clubId, memberId, content) {
await checkAccess(userToken, clubId);
@@ -8,7 +9,7 @@ class MemberNoteService {
}
async getNotesForMember(userToken, clubId, memberId) {
console.log(userToken, clubId);
devLog(userToken, clubId);
await checkAccess(userToken, clubId);
return await MemberNote.findAll({
where: { memberId },

View File

@@ -6,13 +6,14 @@ import path from 'path';
import fs from 'fs';
import sharp from 'sharp';
import { devLog } from '../utils/logger.js';
class MemberService {
async getApprovalRequests(userToken, clubId) {
console.log('[MemberService::getApprovalRequest] - Check user access');
devLog('[MemberService::getApprovalRequest] - Check user access');
await checkAccess(userToken, clubId);
console.log('[MemberService::getApprovalRequest] - Load user');
devLog('[MemberService::getApprovalRequest] - Load user');
const user = await getUserByToken(userToken);
console.log('[MemberService::getApprovalRequest] - Load userclub');
devLog('[MemberService::getApprovalRequest] - Load userclub');
return await UserClub.findAll({
where: {
clubId: clubId,
@@ -23,9 +24,9 @@ class MemberService {
}
async getClubMembers(userToken, clubId, showAll) {
console.log('[getClubMembers] - Check access');
devLog('[getClubMembers] - Check access');
await checkAccess(userToken, clubId);
console.log('[getClubMembers] - Find members');
devLog('[getClubMembers] - Find members');
const where = {
clubId: clubId
};
@@ -44,7 +45,7 @@ class MemberService {
});
})
.then(membersWithImageStatus => {
console.log('[getClubMembers] - return members');
devLog('[getClubMembers] - return members');
return membersWithImageStatus;
})
.catch(error => {
@@ -54,17 +55,17 @@ class MemberService {
}
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate, phone, email, active = true, testMembership = false,
picsInInternetAllowed = false) {
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null) {
try {
console.log('[setClubMembers] - Check access');
devLog('[setClubMembers] - Check access');
await checkAccess(userToken, clubId);
console.log('[setClubMembers] - set default member');
devLog('[setClubMembers] - set default member');
let member = null;
console.log('[setClubMembers] - load member if possible');
devLog('[setClubMembers] - load member if possible');
if (memberId) {
member = await Member.findOne({ where: { id: memberId } });
}
console.log('[setClubMembers] - set member');
devLog('[setClubMembers] - set member');
if (member) {
member.firstName = firstName;
member.lastName = lastName;
@@ -76,6 +77,9 @@ class MemberService {
member.active = active;
member.testMembership = testMembership;
member.picsInInternetAllowed = picsInInternetAllowed;
if (gender) member.gender = gender;
if (ttr !== undefined) member.ttr = ttr;
if (qttr !== undefined) member.qttr = qttr;
await member.save();
} else {
await Member.create({
@@ -90,15 +94,18 @@ class MemberService {
active: active,
testMembership: testMembership,
picsInInternetAllowed: picsInInternetAllowed,
gender: gender || 'unknown',
ttr: ttr,
qttr: qttr,
});
}
console.log('[setClubMembers] - return response');
devLog('[setClubMembers] - return response');
return {
status: 200,
response: { result: "success" },
}
} catch (error) {
console.log(error);
devLog(error);
return {
status: error.statusCode || 500,
response: { error: "nocreation" }
@@ -108,7 +115,7 @@ class MemberService {
async uploadMemberImage(userToken, clubId, memberId, imageBuffer) {
try {
console.log('------>', userToken, clubId, memberId, imageBuffer);
devLog('------>', userToken, clubId, memberId, imageBuffer);
await checkAccess(userToken, clubId);
const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
if (!member) {
@@ -144,6 +151,189 @@ class MemberService {
return { status: 500, error: 'Failed to retrieve image' };
}
}
async updateRatingsFromMyTischtennis(userToken, clubId) {
devLog('[updateRatingsFromMyTischtennis] - Check access');
await checkAccess(userToken, clubId);
const user = await getUserByToken(userToken);
devLog('[updateRatingsFromMyTischtennis] - User:', user.id);
const externalServiceService = (await import('./externalServiceService.js')).default;
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
try {
// 1. myTischtennis-Session abrufen
devLog('[updateRatingsFromMyTischtennis] - Get session for user', user.id);
const session = await externalServiceService.getSession(user.id, 'mytischtennis');
devLog('[updateRatingsFromMyTischtennis] - Session retrieved:', {
hasAccessToken: !!session.accessToken,
hasCookie: !!session.cookie,
expiresAt: session.expiresAt
});
const account = await externalServiceService.getAccount(user.id, 'mytischtennis');
devLog('[updateRatingsFromMyTischtennis] - Account data:', {
id: account?.id,
email: account?.email,
clubId: account?.clubId,
clubName: account?.clubName,
fedNickname: account?.fedNickname,
hasSession: !!(account?.accessToken)
});
if (!account) {
console.error('[updateRatingsFromMyTischtennis] - No account found!');
return {
status: 400,
response: {
message: 'Kein myTischtennis-Account gefunden.',
updated: 0,
errors: [],
debug: { userId: user.id }
}
};
}
if (!account.clubId || !account.fedNickname) {
console.error('[updateRatingsFromMyTischtennis] - Missing clubId or fedNickname:', {
clubId: account.clubId,
fedNickname: account.fedNickname
});
return {
status: 400,
response: {
message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte einmal einloggen.',
updated: 0,
errors: [],
debug: {
hasClubId: !!account.clubId,
hasFedNickname: !!account.fedNickname,
clubId: account.clubId,
fedNickname: account.fedNickname
}
}
};
}
// 2. Rangliste vom Verein abrufen
devLog('[updateRatingsFromMyTischtennis] - Get club rankings', {
clubId: account.clubId,
fedNickname: account.fedNickname,
hasCookie: !!session.cookie
});
const rankings = await myTischtennisClient.getClubRankings(
session.cookie,
account.clubId,
account.fedNickname
);
devLog('[updateRatingsFromMyTischtennis] - Rankings result:', {
success: rankings.success,
entriesCount: rankings.entries?.length || 0,
error: rankings.error
});
if (!rankings.success) {
return {
status: 500,
response: {
message: rankings.error || 'Fehler beim Abrufen der Rangliste',
updated: 0,
errors: [],
debug: {
clubId: account.clubId,
fedNickname: account.fedNickname,
rankingsError: rankings.error
}
}
};
}
// 3. Alle Mitglieder des Clubs laden
devLog('[updateRatingsFromMyTischtennis] - Load club members for clubId:', clubId);
const members = await Member.findAll({ where: { clubId } });
devLog('[updateRatingsFromMyTischtennis] - Found members:', members.length);
let updated = 0;
const errors = [];
const notFound = [];
const matched = [];
// 4. Für jedes Mitglied TTR aktualisieren
for (const member of members) {
const firstName = member.firstName;
const lastName = member.lastName;
// Suche nach Match in rankings entries
const rankingEntry = rankings.entries.find(entry =>
entry.firstname.toLowerCase() === firstName.toLowerCase() &&
entry.lastname.toLowerCase() === lastName.toLowerCase()
);
if (rankingEntry) {
try {
// fedRank ist der TTR-Wert
const oldTtr = member.ttr;
member.ttr = rankingEntry.fedRank;
// TODO: QTTR muss von einem anderen Endpoint geholt werden
await member.save();
updated++;
matched.push({
name: `${firstName} ${lastName}`,
oldTtr: oldTtr,
newTtr: rankingEntry.fedRank
});
devLog(`[updateRatingsFromMyTischtennis] - Updated ${firstName} ${lastName}: TTR ${oldTtr}${rankingEntry.fedRank}`);
} catch (error) {
console.error(`[updateRatingsFromMyTischtennis] - Error updating ${firstName} ${lastName}:`, error);
errors.push({
member: `${firstName} ${lastName}`,
error: error.message
});
}
} else {
notFound.push(`${firstName} ${lastName}`);
devLog(`[updateRatingsFromMyTischtennis] - Not found in rankings: ${firstName} ${lastName}`);
}
}
devLog('[updateRatingsFromMyTischtennis] - Update complete');
devLog(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
let message = `${updated} Mitglied(er) aktualisiert.`;
if (notFound.length > 0) {
message += ` ${notFound.length} nicht in myTischtennis-Rangliste gefunden.`;
}
if (errors.length > 0) {
message += ` ${errors.length} Fehler beim Speichern.`;
}
return {
status: 200,
response: {
message: message,
updated: updated,
matched: matched,
notFound: notFound,
errors: errors,
totalEntries: rankings.entries.length,
totalMembers: members.length
}
};
} catch (error) {
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
return {
status: 500,
response: {
message: error.message || 'Fehler beim Aktualisieren',
updated: 0,
errors: [error.message]
}
};
}
}
}
export default new MemberService();

View File

@@ -1,45 +1,151 @@
import PredefinedActivity from '../models/PredefinedActivity.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import GroupActivity from '../models/GroupActivity.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import sequelize from '../database.js';
import { Op } from 'sequelize';
import { devLog } from '../utils/logger.js';
class PredefinedActivityService {
async createPredefinedActivity(data) {
console.log('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity');
devLog('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity');
return await PredefinedActivity.create({
name: data.name,
code: data.code,
description: data.description,
durationText: data.durationText,
duration: data.duration,
imageLink: data.imageLink,
drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null,
});
}
async updatePredefinedActivity(id, data) {
console.log(`[PredefinedActivityService::updatePredefinedActivity] - Updating predefined activity with id: ${id}`);
devLog(`[PredefinedActivityService::updatePredefinedActivity] - Updating predefined activity with id: ${id}`);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
console.log('[PredefinedActivityService::updatePredefinedActivity] - Activity not found');
devLog('[PredefinedActivityService::updatePredefinedActivity] - Activity not found');
throw new Error('Predefined activity not found');
}
return await activity.update({
name: data.name,
code: data.code,
description: data.description,
durationText: data.durationText,
duration: data.duration,
imageLink: data.imageLink,
drawingData: data.drawingData ? JSON.stringify(data.drawingData) : null,
});
}
async getAllPredefinedActivities() {
console.log('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities');
return await PredefinedActivity.findAll();
devLog('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities');
return await PredefinedActivity.findAll({
order: [
[sequelize.literal('code IS NULL'), 'ASC'], // Non-null codes first
['code', 'ASC'],
['name', 'ASC'],
],
});
}
async getPredefinedActivityById(id) {
console.log(`[PredefinedActivityService::getPredefinedActivityById] - Fetching predefined activity with id: ${id}`);
devLog(`[PredefinedActivityService::getPredefinedActivityById] - Fetching predefined activity with id: ${id}`);
const activity = await PredefinedActivity.findByPk(id);
if (!activity) {
console.log('[PredefinedActivityService::getPredefinedActivityById] - Activity not found');
devLog('[PredefinedActivityService::getPredefinedActivityById] - Activity not found');
throw new Error('Predefined activity not found');
}
return activity;
}
async searchPredefinedActivities(query, limit = 20) {
const q = (query || '').trim();
if (!q || q.length < 2) {
return [];
}
return await PredefinedActivity.findAll({
where: {
[Op.or]: [
{ name: { [Op.like]: `%${q}%` } },
{ code: { [Op.like]: `%${q}%` } },
],
},
order: [
[sequelize.literal('code IS NULL'), 'ASC'],
['code', 'ASC'],
['name', 'ASC'],
],
limit: Math.min(parseInt(limit || 20, 10), 50),
});
}
async mergeActivities(sourceId, targetId) {
devLog(`[PredefinedActivityService::mergeActivities] - Merge ${sourceId} -> ${targetId}`);
if (!sourceId || !targetId) throw new Error('sourceId and targetId are required');
if (Number(sourceId) === Number(targetId)) throw new Error('sourceId and targetId must differ');
const tx = await sequelize.transaction();
try {
const source = await PredefinedActivity.findByPk(sourceId, { transaction: tx });
const target = await PredefinedActivity.findByPk(targetId, { transaction: tx });
if (!source) throw new Error('Source activity not found');
if (!target) throw new Error('Target activity not found');
// Reassign references
await DiaryDateActivity.update(
{ predefinedActivityId: targetId },
{ where: { predefinedActivityId: sourceId }, transaction: tx }
);
await GroupActivity.update(
{ customActivity: targetId },
{ where: { customActivity: sourceId }, transaction: tx }
);
await PredefinedActivityImage.update(
{ predefinedActivityId: targetId },
{ where: { predefinedActivityId: sourceId }, transaction: tx }
);
// Finally delete source
await source.destroy({ transaction: tx });
await tx.commit();
return { ok: true };
} catch (err) {
await tx.rollback();
console.error('[PredefinedActivityService::mergeActivities] - Error:', err);
throw err;
}
}
async deduplicateActivities() {
devLog('[PredefinedActivityService::deduplicateActivities] - Start');
const all = await PredefinedActivity.findAll();
const nameToActivities = new Map();
for (const activity of all) {
const key = (activity.name || '').trim().toLowerCase();
if (!key) continue;
if (!nameToActivities.has(key)) nameToActivities.set(key, []);
nameToActivities.get(key).push(activity);
}
let mergedCount = 0;
let groupCount = 0;
for (const list of nameToActivities.values()) {
if (!list || list.length <= 1) continue;
groupCount++;
// Stable target: kleinste ID
list.sort((a, b) => a.id - b.id);
const target = list[0];
for (const src of list.slice(1)) {
await this.mergeActivities(src.id, target.id);
mergedCount++;
}
}
devLog('[PredefinedActivityService::deduplicateActivities] - Done', { mergedCount, groupCount });
return { mergedCount, groupCount };
}
}
export default new PredefinedActivityService();

View File

@@ -9,6 +9,7 @@ import { checkAccess } from '../utils/userUtils.js';
import { Op, literal } from 'sequelize';
import { devLog } from '../utils/logger.js';
function getRoundName(size) {
switch (size) {
case 2: return "Finale";
@@ -88,7 +89,7 @@ class TournamentService {
include: [{
model: Member,
as: 'member',
attributes: ['id', 'firstName', 'lastName'],
attributes: ['id', 'firstName', 'lastName', 'ttr', 'qttr'],
}],
order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']]
});
@@ -180,32 +181,61 @@ class TournamentService {
// 2) Alte Matches löschen
await TournamentMatch.destroy({ where: { tournamentId } });
// 3) Shuffle + verteilen
const shuffled = members.slice();
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
// 3) Prüfe, ob Spieler bereits manuell zugeordnet wurden
const alreadyAssigned = members.filter(m => m.groupId !== null);
const unassigned = members.filter(m => m.groupId === null);
if (alreadyAssigned.length > 0) {
// Spieler sind bereits manuell zugeordnet - nicht neu verteilen
devLog(`${alreadyAssigned.length} Spieler bereits zugeordnet, ${unassigned.length} noch nicht zugeordnet`);
} else {
// Keine manuellen Zuordnungen - zufällig verteilen
const shuffled = members.slice();
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
groups.forEach((g, idx) => {
shuffled
.filter((_, i) => i % groups.length === idx)
.forEach(m => m.update({ groupId: g.id }));
});
}
groups.forEach((g, idx) => {
shuffled
.filter((_, i) => i % groups.length === idx)
.forEach(m => m.update({ groupId: g.id }));
});
// 4) RoundRobin anlegen wie gehabt
// 4) RoundRobin anlegen wie gehabt - NUR innerhalb jeder Gruppe
devLog(`[fillGroups] Erstelle Matches für ${groups.length} Gruppen`);
for (const g of groups) {
devLog(`[fillGroups] Verarbeite Gruppe ${g.id}`);
const gm = await TournamentMember.findAll({ where: { groupId: g.id } });
devLog(`[fillGroups] Gruppe ${g.id} hat ${gm.length} Teilnehmer:`, gm.map(m => ({ id: m.id, name: m.member?.firstName + ' ' + m.member?.lastName })));
if (gm.length < 2) {
console.warn(`Gruppe ${g.id} hat nur ${gm.length} Teilnehmer - keine Matches erstellt`);
continue;
}
const rounds = this.generateRoundRobinSchedule(gm);
devLog(`[fillGroups] Gruppe ${g.id} hat ${rounds.length} Runden`);
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
devLog(`[fillGroups] Runde ${roundIndex + 1} für Gruppe ${g.id}:`, rounds[roundIndex]);
for (const [p1Id, p2Id] of rounds[roundIndex]) {
await TournamentMatch.create({
tournamentId,
groupId: g.id,
round: 'group',
player1Id: p1Id,
player2Id: p2Id,
groupRound: roundIndex + 1
});
// Prüfe, ob beide Spieler zur gleichen Gruppe gehören
const p1 = gm.find(p => p.id === p1Id);
const p2 = gm.find(p => p.id === p2Id);
if (p1 && p2 && p1.groupId === p2.groupId && p1.groupId === g.id) {
const match = await TournamentMatch.create({
tournamentId,
groupId: g.id,
round: 'group',
player1Id: p1Id,
player2Id: p2Id,
groupRound: roundIndex + 1
});
devLog(`[fillGroups] Match erstellt: ${match.id} - Spieler ${p1Id} vs ${p2Id} in Gruppe ${g.id}`);
} else {
console.warn(`Spieler gehören nicht zur gleichen Gruppe: ${p1Id} (${p1?.groupId}) vs ${p2Id} (${p2?.groupId}) in Gruppe ${g.id}`);
}
}
}
}
@@ -360,8 +390,13 @@ class TournamentService {
for (const m of groupMatches.filter(m => m.groupId === g.id)) {
if (!stats[m.player1Id] || !stats[m.player2Id]) continue;
const [p1, p2] = m.result.split(":").map(n => parseInt(n, 10));
if (p1 > p2) stats[m.player1Id].points += 2;
else stats[m.player2Id].points += 2;
if (p1 > p2) {
stats[m.player1Id].points += 1; // Sieger bekommt +1
stats[m.player2Id].points -= 1; // Verlierer bekommt -1
} else {
stats[m.player2Id].points += 1; // Sieger bekommt +1
stats[m.player1Id].points -= 1; // Verlierer bekommt -1
}
stats[m.player1Id].setsWon += p1;
stats[m.player1Id].setsLost += p2;
stats[m.player2Id].setsWon += p2;

19
backend/utils/logger.js Normal file
View File

@@ -0,0 +1,19 @@
// Zentrale Logger-Utility für dev/prod Umgebungen
const isDev = process.env.STAGE === 'dev';
// Debug-Logs nur im dev-Modus
export const devLog = (...args) => isDev && console.log(...args);
// Fehler-Logs immer ausgeben
export const errorLog = (...args) => console.error(...args);
// Info-Logs immer ausgeben (für wichtige Produktions-Events)
export const infoLog = (...args) => console.log(...args);
export default {
devLog,
errorLog,
infoLog,
isDev
};

View File

@@ -2,9 +2,10 @@ import jwt from 'jsonwebtoken';
import { Op } from 'sequelize';
import User from '../models/User.js';
import UserToken from '../models/UserToken.js';
import UserClub from '../models/UserClub.js'; // <-- hier hinzufügen
import UserClub from '../models/UserClub.js';
import HttpError from '../exceptions/HttpError.js';
import { config } from 'dotenv';
import { devLog } from './logger.js';
config(); // sorgt dafür, dass process.env.JWT_SECRET geladen wird
export const getUserByToken = async (token) => {
@@ -41,7 +42,7 @@ export const getUserByToken = async (token) => {
export const hasUserClubAccess = async (userId, clubId) => {
try {
console.log('[hasUserClubAccess]');
devLog('[hasUserClubAccess]');
const userClub = await UserClub.findOne({
where: {
user_id: userId,
@@ -51,10 +52,9 @@ export const hasUserClubAccess = async (userId, clubId) => {
});
return userClub !== null;
} catch (error) {
console.log(error);
console.error('[hasUserClubAccess] - error:', error);
throw new HttpError('notfound', 500);
}
console.log('---- no user found');
}
export const checkAccess = async (userToken, clubId) => {
@@ -62,11 +62,21 @@ export const checkAccess = async (userToken, clubId) => {
const user = await getUserByToken(userToken);
const hasAccess = await hasUserClubAccess(user.id, clubId);
if (!hasAccess) {
console.log('no club access');
devLog('[checkAccess] - no club access');
throw new HttpError('noaccess', 403);
}
} catch (error) {
console.log(error);
devLog('[checkAccess] - error:', error);
throw error;
}
};
export const checkGlobalAccess = async (userToken) => {
try {
const user = await getUserByToken(userToken);
return user; // Einfach den User zurückgeben, da globale Zugriffe nur Authentifizierung benötigen
} catch (error) {
devLog('[checkGlobalAccess] - error:', error);
throw error;
}
};

View File

@@ -1,10 +1,57 @@
<!doctype html>
<html lang="en">
<html lang="de">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Trainingstagebuch</title>
<title>Trainingstagebuch Vereinsverwaltung, Trainingsplanung & Turniere</title>
<meta name="description" content="Das TrainingsTagebuch hilft Vereinen und Trainer:innen, Mitglieder zu verwalten, Trainings zu dokumentieren, Spielpläne zu organisieren und Statistiken auszuwerten alles in einer modernen WebApp." />
<link rel="canonical" href="https://tt-tagebuch.de/" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Trainingstagebuch" />
<meta property="og:title" content="Trainingstagebuch Vereinsverwaltung, Trainingsplanung & Turniere" />
<meta property="og:description" content="Mitgliederverwaltung, Trainingstagebuch, Spiel und Turnierorganisation sowie Statistiken DSGVOfreundlich und einfach." />
<meta property="og:url" content="https://tt-tagebuch.de/" />
<meta property="og:image" content="https://tt-tagebuch.de/vite.svg" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Trainingstagebuch Vereinsverwaltung, Trainingsplanung & Turniere" />
<meta name="twitter:description" content="Mitgliederverwaltung, Trainingstagebuch, Spiel und Turnierorganisation sowie Statistiken DSGVOfreundlich und einfach." />
<meta name="twitter:image" content="https://tt-tagebuch.de/vite.svg" />
<!-- JSON-LD: Website + Organization -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "Trainingstagebuch",
"url": "https://tt-tagebuch.de/",
"potentialAction": {
"@type": "SearchAction",
"target": "https://tt-tagebuch.de/?q={search_term_string}",
"query-input": "required name=search_term_string"
}
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Trainingstagebuch",
"applicationCategory": "SportsApplication",
"operatingSystem": "Web",
"description": "Mitgliederverwaltung, Trainingstagebuch, Spiel- und Turnierorganisation sowie Statistiken DSGVO-freundlich und einfach.",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "EUR"
},
"url": "https://tt-tagebuch.de/"
}
</script>
</head>
<body>
<div id="app"></div>

View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://tt-tagebuch.de/sitemap.xml

Some files were not shown because too many files have changed in this diff Show More