103 Commits

Author SHA1 Message Date
Torsten Schulz (local)
36bf99c013 Add MyTischtennis fetch log functionality and new endpoints
Enhance MyTischtennis integration by introducing fetch log capabilities. Implement new controller methods to retrieve fetch logs and latest successful fetches for users. Update routes to include these new endpoints. Modify the MyTischtennis model to support fetch logs and ensure proper logging of fetch operations in various services. Update frontend components to display fetch statistics, improving user experience and data visibility.
2025-10-14 23:07:57 +02:00
Torsten Schulz (local)
7549fb5730 Implement league table functionality and MyTischtennis integration. Add new endpoints for retrieving and updating league tables in matchController and matchRoutes. Enhance Team model with additional fields for match statistics. Update frontend components to display league tables and allow fetching data from MyTischtennis, improving user experience and data accuracy. 2025-10-14 22:55:39 +02:00
Torsten Schulz (local)
1517d83f6c Refactor backend to enhance MyTischtennis integration. Update package.json to change main entry point to server.js. Modify server.js to improve scheduler service logging. Add new fields to ClubTeam, League, Match, and Member models for MyTischtennis data. Update routes to include new MyTischtennis URL parsing and configuration endpoints. Enhance services for fetching team data and scheduling match results. Improve frontend components for MyTischtennis URL configuration and display match results with scores. 2025-10-14 21:58:21 +02:00
Torsten Schulz (local)
993e12d4a5 Update MyTischtennis functionality to support automatic rating updates. Introduce new autoUpdateRatings field in MyTischtennis model and enhance MyTischtennisController to handle update history retrieval. Integrate node-cron for scheduling daily updates at 6:00 AM. Update frontend components to allow users to enable/disable automatic updates and display last update timestamps. 2025-10-09 00:18:41 +02:00
Torsten Schulz (local)
806cb527d4 Refactor group creation and assignment logic in DiaryView. Update group creation to allow specifying the number of groups, enhance participant assignment with group selection, and improve UI elements for better user experience. 2025-10-08 19:21:15 +02:00
Torsten Schulz (local)
7e9d2d2c4f Update member count calculations in MembersView to exclude inactive test memberships. Adjust activeMembersCount and testMembersCount computed properties for improved accuracy in member statistics. 2025-10-08 18:15:18 +02:00
Torsten Schulz (local)
ec9b92000e Update Member model to allow optional birthDate and enhance QuickAddMemberDialog for better input handling. Refactor DiaryView to remove default birthDate logic and improve member creation process. Adjust MembersView to handle empty birthDate gracefully in formatting. 2025-10-08 18:06:22 +02:00
Torsten Schulz (local)
d110900e85 Implement member statistics dropdown in MembersView. Add computed properties for active, test, and inactive member counts, and introduce a toggle for displaying member info. Enhance styling for the dropdown and member stats for improved user experience. 2025-10-08 17:47:43 +02:00
Torsten Schulz (local)
cd3c3502f6 Add tournament name column to OfficialTournaments view. Update data structure to include tournament names for better clarity in participation details. 2025-10-08 15:44:37 +02:00
Torsten Schulz (local)
ccce9bffac Enhance MembersView by adding a new row style for test memberships. Update the member row class to include 'row-test' for better visual distinction of test members, improving user experience in member management. 2025-10-08 15:00:38 +02:00
Torsten Schulz (local)
f1ba25f9f5 Implement logic to determine the quarter with the highest training data in TrainingStatsView. Update average participation calculation to reflect the best quarter's total participants, enhancing accuracy in participation metrics. 2025-10-08 14:54:57 +02:00
Torsten Schulz (local)
548f51ac54 Refactor participation metrics in TrainingStatsView to use total participant counts instead of member participation estimates. Update methods for calculating average participation across different time periods to enhance accuracy and maintainability. 2025-10-08 14:47:30 +02:00
Torsten Schulz (local)
946e4fce1e Enhance SeasonSelector and TeamManagementView with dialog components for improved user interaction. Introduce new dialog states and helper methods for consistent handling of information and confirmations. Update styles in TrainingStatsView to reflect new participation metrics and improve layout. Refactor document display in TeamManagementView to a table format for better readability. 2025-10-08 14:43:53 +02:00
Torsten Schulz (local)
40dcd0e54c Refactor modals in DiaryView, MembersView, OfficialTournaments, ScheduleView, and TrainingStatsView to use dedicated dialog components for improved maintainability and user experience. Update styles and structure for consistency across the application. 2025-10-08 12:49:42 +02:00
Torsten Schulz (local)
bd338b86df Implementiert InfoDialog und ConfirmDialog in mehreren Komponenten, um die Benutzerinteraktion zu verbessern. Fügt Dialogzustände und Hilfsmethoden hinzu, die eine konsistente Handhabung von Informationen und Bestätigungen ermöglichen. Diese Änderungen erhöhen die Benutzerfreundlichkeit und verbessern die visuelle Rückmeldung in der Anwendung. 2025-10-08 11:46:07 +02:00
Torsten Schulz (local)
1d4aa43b02 Aktualisiert das Styling in App.vue durch Hinzufügen von Padding am unteren Rand für die Statusleiste. Ändert den Hintergrund und die Polsterung in DialogManager.vue, um das visuelle Design zu verbessern und die Benutzeroberfläche zu optimieren. 2025-10-08 11:22:45 +02:00
Torsten Schulz (local)
cc08f4ba43 Verbessert die Lesbarkeit und Struktur des Codes in DiaryView.vue durch Anpassungen der Einrückungen und Formatierungen. Optimiert die Anzeige von Aktivitätsvisualisierungen und aktualisiert die Logik für die Eingabefelder, um die Benutzerfreundlichkeit zu erhöhen. Diese Änderungen tragen zur allgemeinen Verbesserung der Benutzeroberfläche und der Codequalität bei. 2025-10-08 11:14:20 +02:00
Torsten Schulz (local)
d0ccaa9e54 Fügt eine neue Methode hasActivityVisual in DiaryView.vue hinzu, um die Sichtbarkeit von Aktivitätsvisualisierungen zu überprüfen. Aktualisiert die Bedingungen für die Anzeige von Icons, die Bilder oder Zeichnungen darstellen, um die Benutzeroberfläche zu verbessern und die Logik zu optimieren. 2025-10-08 11:00:20 +02:00
Torsten Schulz (local)
dc0eff4e4c Entfernt die PDF-Datei 9_code_list_1759357969975.pdf und implementiert eine Sidebar-Toggle-Funktionalität in App.vue. Die Sidebar kann nun auf mobilen Geräten ein- und ausgeklappt werden, um die Benutzeroberfläche zu optimieren. Zudem wurden Titelattribute zu Navigationslinks hinzugefügt, um die Benutzerfreundlichkeit zu verbessern. Der Vuex-Store wurde aktualisiert, um den Zustand der Sidebar zu speichern und zu verwalten. 2025-10-08 10:52:07 +02:00
Torsten Schulz (local)
db9e404372 Aktualisiert die Logik zum Löschen eines Datums in DiaryView.vue. Die Schaltfläche zum Löschen wird nun nur angezeigt, wenn keine Inhalte (Trainingplan, Teilnehmer, Aktivitäten, Unfälle oder Notizen) vorhanden sind. Fügt eine neue Methode canDeleteCurrentDate hinzu, die diese Überprüfung durchführt, um die Benutzerfreundlichkeit zu verbessern und versehentliche Löschungen zu verhindern. 2025-10-04 02:30:23 +02:00
Torsten Schulz (local)
60ac89636e Ändert das Eingabefeld für den Nachnamen in der Mitgliederregistrierung auf optional und aktualisiert die Validierungslogik entsprechend. Setzt ein Standard-Geburtsdatum für neue Mitglieder auf den 01.01. des aktuellen Jahres minus 10 Jahre, wenn kein Geburtsdatum eingegeben wird. Diese Änderungen verbessern die Benutzerfreundlichkeit und Flexibilität bei der Registrierung neuer Mitglieder. 2025-10-04 02:27:49 +02:00
Torsten Schulz (local)
2b1365339e Fügt die Funktion zum Drehen von Mitgliedsbildern hinzu. Implementiert die Logik zur Bildrotation in MemberService und aktualisiert die entsprechenden Routen und Frontend-Komponenten, um die Benutzeroberfläche für die Bildbearbeitung zu verbessern. Ermöglicht das Drehen von Bildern über die Mitgliederansicht und aktualisiert die Anzeige nach der Bearbeitung. 2025-10-04 01:59:21 +02:00
Torsten Schulz (local)
0cf2351c79 Erweitert die Funktionalität von updateRatingsFromMyTischtennis, um eine automatische Anmeldung bei abgelaufener Session zu ermöglichen. Fügt Fehlerbehandlung für den Login-Prozess hinzu und entfernt die Bestätigungsabfrage vor der Aktualisierung der TTR/QTTR-Werte, um den Benutzerfluss zu verbessern. 2025-10-04 01:49:30 +02:00
Torsten Schulz (local)
5c32fad34e Fügt eine bedingte Navigation in App.vue hinzu, die eine leere Navigationsleiste anzeigt, wenn keine Authentifizierung vorliegt. Diese Änderung verbessert die Benutzeroberfläche und sorgt für eine klarere Struktur der Navigation. 2025-10-04 01:43:37 +02:00
Torsten Schulz (local)
7f0b681e88 Ermöglicht die Bearbeitung von Spielergebnissen in TournamentsView.vue durch klickbare Labels und editierbare Eingabefelder. Fügt Logik zum Speichern und Abbrechen von Änderungen hinzu. Aktualisiert das Styling für Eingabefelder und klickbare Texte, um die Benutzererfahrung zu verbessern. 2025-10-04 01:38:27 +02:00
Torsten Schulz (local)
cc964da9cf Fügt einen Dialog-Manager hinzu, um die Verwaltung von Dialogen zu ermöglichen. Aktualisiert den Vuex-Store mit neuen Mutationen und Aktionen zur Handhabung von Dialogen. Integriert den MatchReportDialog in ScheduleView.vue und ermöglicht das Öffnen von Spielberichten über die Benutzeroberfläche. Verbessert die Benutzererfahrung durch neue Schaltflächen und CSS-Stile für die Dialoge. 2025-10-02 11:44:27 +02:00
Torsten Schulz (local)
dbede48d4f Entfernt Konsolenausgaben aus der MyTischtennisClient-Klasse, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Methoden getUserProfile und getClubRankings und tragen zur Optimierung der Protokollierung und Performance bei. 2025-10-02 10:40:24 +02:00
Torsten Schulz (local)
6cd3c3a020 Entfernt Konsolenausgaben aus verschiedenen Controllern und Services, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Controller für Clubs, Club-Teams, Mitglieder, Tagebuch-Tags, Saisons und Teams sowie die zugehörigen Services. Ziel ist es, die Protokollierung zu optimieren und die Performance zu steigern. 2025-10-02 10:34:56 +02:00
Torsten Schulz (local)
7ecbef806d Entfernt Konsolenausgaben aus mehreren Komponenten, um den Code zu bereinigen und die Lesbarkeit zu verbessern. Betroffene Dateien sind CourtDrawingRender.vue, CourtDrawingTool.vue, SeasonSelector.vue, PredefinedActivities.vue, ScheduleView.vue, TeamManagementView.vue und TournamentsView.vue. Diese Änderungen tragen zur Optimierung der Performance und zur Reduzierung von unnötigen Protokollierungen bei. 2025-10-02 10:13:03 +02:00
Torsten Schulz (local)
1c70ca97bb Fügt Unterstützung für Team-Dokumente hinzu. Aktualisiert die Backend-Modelle und -Routen, um Team-Dokumente zu verwalten, einschließlich Upload- und Parsing-Funktionen für Code- und Pin-Listen. Ergänzt die Benutzeroberfläche in TeamManagementView.vue zur Anzeige und Verwaltung von Team-Dokumenten sowie zur Integration von PDF-Parsing. Aktualisiert die Match-Modelle, um zusätzliche Felder für Spiel-Codes und PINs zu berücksichtigen. 2025-10-02 09:04:19 +02:00
Torsten Schulz (local)
a6493990d3 Erweitert die Backend- und Frontend-Funktionalität zur Unterstützung von Teams und Saisons. Fügt neue Routen für Team- und Club-Team-Management hinzu, aktualisiert die Match- und Team-Modelle zur Berücksichtigung von Saisons, und implementiert die Saison-Auswahl in der Benutzeroberfläche. Optimiert die Logik zur Abfrage von Ligen und Spielen basierend auf der ausgewählten Saison. 2025-10-01 22:47:13 +02:00
Torsten Schulz (local)
f8f4d23c4e Aktualisiert die Schaltflächen im MyTischtennisAccount.vue, um die Benutzeroberfläche zu verbessern. Ändert den Text der Schaltfläche "Verbindung testen" in "Erneut einloggen" und entfernt die Testausgabe für Login-Tests. Optimiert die Erfolgsmeldung nach erfolgreichem Login und aktualisiert die Account-Daten. Entfernt die nicht mehr benötigte Funktionalität für den Login-Flow-Test. 2025-10-01 13:52:14 +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
231 changed files with 31284 additions and 2153 deletions

View File

View File

@@ -0,0 +1,212 @@
# MyTischtennis Automatischer Datenabruf
## Übersicht
Dieses System ermöglicht den automatischen Abruf von Spielergebnissen und Statistiken von myTischtennis.de.
## Scheduler
### 6:00 Uhr - Rating Updates
- **Service:** `autoUpdateRatingsService.js`
- **Funktion:** Aktualisiert TTR/QTTR-Werte für Spieler
- **TODO:** Implementierung der eigentlichen Rating-Update-Logik
### 6:30 Uhr - Spielergebnisse
- **Service:** `autoFetchMatchResultsService.js`
- **Funktion:** Ruft Spielerbilanzen für konfigurierte Teams ab
- **Status:** ✅ Grundlegende Implementierung fertig
## Benötigte Konfiguration
### 1. MyTischtennis-Account
- Account muss in den MyTischtennis-Settings verknüpft sein
- Checkbox "Automatische Updates" aktivieren
- Passwort speichern (erforderlich für automatische Re-Authentifizierung)
### 2. League-Konfiguration
Für jede Liga müssen folgende Felder ausgefüllt werden:
```sql
UPDATE league SET
my_tischtennis_group_id = '504417', -- Group ID von myTischtennis
association = 'HeTTV', -- Verband (z.B. HeTTV, DTTB)
groupname = '1.Kreisklasse' -- Gruppenname für URL
WHERE id = 1;
```
**Beispiel-URL:**
```
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/...
^^^^^ ^^^^^^^^^^^^^^ ^^^^^^
association groupname group_id
```
### 3. Team-Konfiguration
Für jedes Team muss die myTischtennis Team-ID gesetzt werden:
```sql
UPDATE club_team SET
my_tischtennis_team_id = '2995094' -- Team ID von myTischtennis
WHERE id = 1;
```
**Beispiel-URL:**
```
.../mannschaft/2995094/Harheimer_TC_(J11)/spielerbilanzen/gesamt
^^^^^^^
team_id
```
### 4. Spieler-Zuordnung (Optional)
Spieler werden automatisch anhand des Namens zugeordnet. Für genauere Zuordnung kann die myTischtennis Player-ID gesetzt werden:
```sql
UPDATE member SET
my_tischtennis_player_id = 'NU2705037' -- Player ID von myTischtennis
WHERE id = 1;
```
## Migrationen
Folgende Migrationen müssen ausgeführt werden:
```bash
# 1. MyTischtennis Auto-Update-Felder
mysql -u root -p trainingstagebuch < backend/migrations/add_auto_update_ratings_to_my_tischtennis.sql
# 2. MyTischtennis Update-History-Tabelle
mysql -u root -p trainingstagebuch < backend/migrations/create_my_tischtennis_update_history.sql
# 3. League MyTischtennis-Felder
mysql -u root -p trainingstagebuch < backend/migrations/add_mytischtennis_fields_to_league.sql
# 4. Team MyTischtennis-ID
mysql -u root -p trainingstagebuch < backend/migrations/add_mytischtennis_team_id_to_club_team.sql
# 5. Member MyTischtennis Player-ID
mysql -u root -p trainingstagebuch < backend/migrations/add_mytischtennis_player_id_to_member.sql
# 6. Match Result-Felder
mysql -u root -p trainingstagebuch < backend/migrations/add_match_result_fields.sql
```
## Abgerufene Daten
Von der myTischtennis API werden folgende Daten abgerufen:
### Einzelstatistiken
- Player ID, Vorname, Nachname
- Gewonnene/Verlorene Punkte
- Anzahl Spiele
- Detaillierte Statistiken nach Gegner-Position
### Doppelstatistiken
- Player IDs, Namen der beiden Spieler
- Gewonnene/Verlorene Punkte
- Anzahl Spiele
### Team-Informationen
- Teamname, Liga, Saison
- Gesamtpunkte (gewonnen/verloren)
- Doppel- und Einzelpunkte
## Implementierungsdetails
### Datenfluss
1. **Scheduler** (6:30 Uhr):
- `schedulerService.js` triggert `autoFetchMatchResultsService.executeAutomaticFetch()`
2. **Account-Verarbeitung**:
- Lädt alle MyTischtennis-Accounts mit `autoUpdateRatings = true`
- Prüft Session-Gültigkeit
- Re-Authentifizierung bei abgelaufener Session
3. **Team-Abfrage**:
- Lädt alle Teams mit konfigurierten myTischtennis-IDs
- Baut API-URL dynamisch zusammen
- Führt authentifizierten GET-Request durch
4. **Datenverarbeitung**:
- Parst JSON-Response
- Matched Spieler anhand von ID oder Name
- Speichert myTischtennis Player-ID bei Mitgliedern
- Loggt Statistiken
### Player-Matching-Algorithmus
```javascript
1. Suche nach myTischtennis Player-ID (exakte Übereinstimmung)
2. Falls nicht gefunden: Suche nach Name (case-insensitive)
3. Falls gefunden: Speichere myTischtennis Player-ID für zukünftige Abfragen
```
**Hinweis:** Da Namen verschlüsselt gespeichert werden, müssen für den Namens-Abgleich alle Members geladen und entschlüsselt werden. Dies ist bei großen Datenbanken ineffizient.
## TODO / Offene Punkte
### Noch zu implementieren:
1. **TTR/QTTR Updates** (6:00 Uhr Job):
- Endpoint für TTR/QTTR-Daten identifizieren
- Daten abrufen und in Member-Tabelle speichern
2. **Spielergebnis-Details**:
- Einzelne Matches mit Satzständen speichern
- Tabelle für Match-Historie erstellen
3. **History-Tabelle für Spielergebnis-Abrufe** (optional):
- Ähnlich zu `my_tischtennis_update_history`
- Speichert Erfolg/Fehler der Abrufe
4. **Benachrichtigungen** (optional):
- Email/Push bei neuen Ergebnissen
- Highlights für besondere Siege
5. **Performance-Optimierung**:
- Caching für Player-Matches
- Incremental Updates (nur neue Daten)
## Manueller Test
```javascript
// Im Node-Backend-Code oder über API-Endpoint:
import schedulerService from './services/schedulerService.js';
// Rating Updates manuell triggern
await schedulerService.triggerRatingUpdates();
// Spielergebnisse manuell abrufen
await schedulerService.triggerMatchResultsFetch();
```
## API-Dokumentation
### MyTischtennis Spielerbilanzen-Endpoint
**URL-Format:**
```
https://www.mytischtennis.de/click-tt/{association}/{season}/ligen/{groupname}/gruppe/{groupId}/mannschaft/{teamId}/{teamname}/spielerbilanzen/gesamt?_data=routes%2Fclick-tt%2B%2F%24association%2B%2F%24season%2B%2F%24type%2B%2F%28%24groupname%29.gruppe.%24urlid_.mannschaft.%24teamid.%24teamname%2B%2Fspielerbilanzen.%24filter
```
**Parameter:**
- `{association}`: Verband (z.B. "HeTTV")
- `{season}`: Saison im Format "25--26"
- `{groupname}`: Gruppenname URL-encoded (z.B. "1.Kreisklasse")
- `{groupId}`: Gruppen-ID (numerisch, z.B. "504417")
- `{teamId}`: Team-ID (numerisch, z.B. "2995094")
- `{teamname}`: Teamname URL-encoded mit Underscores (z.B. "Harheimer_TC_(J11)")
**Response:** JSON mit `data.balancesheet` Array
## Sicherheit
- ✅ Automatische Session-Verwaltung
- ✅ Re-Authentifizierung bei abgelaufenen Sessions
- ✅ Passwörter verschlüsselt gespeichert
- ✅ Fehlerbehandlung und Logging
- ✅ Graceful Degradation (einzelne Team-Fehler stoppen nicht den gesamten Prozess)

View File

@@ -0,0 +1,328 @@
# MyTischtennis URL Parser
## Übersicht
Der URL-Parser ermöglicht es, myTischtennis-Team-URLs automatisch zu parsen und die Konfiguration für automatische Datenabrufe vorzunehmen.
## Verwendung
### 1. URL Parsen
**Endpoint:** `POST /api/mytischtennis/parse-url`
**Request:**
```json
{
"url": "https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/spielerbilanzen/gesamt"
}
```
**Response:**
```json
{
"success": true,
"data": {
"association": "HeTTV",
"season": "25/26",
"type": "ligen",
"groupname": "1.Kreisklasse",
"groupId": "504417",
"teamId": "2995094",
"teamname": "Harheimer TC (J11)",
"originalUrl": "https://www.mytischtennis.de/click-tt/...",
"clubId": "43030",
"clubName": "Harheimer TC",
"teamName": "Jugend 11",
"leagueName": "Jugend 13 1. Kreisklasse",
"region": "Frankfurt",
"tableRank": 8,
"matchesWon": 0,
"matchesLost": 3
}
}
```
### 2. Team Automatisch Konfigurieren
**Endpoint:** `POST /api/mytischtennis/configure-team`
**Request:**
```json
{
"url": "https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/spielerbilanzen/gesamt",
"clubTeamId": 1,
"createLeague": false,
"createSeason": false
}
```
**Parameter:**
- `url` (required): Die myTischtennis-URL
- `clubTeamId` (required): Die ID des lokalen Club-Teams
- `createLeague` (optional): Wenn `true`, wird eine neue League erstellt
- `createSeason` (optional): Wenn `true`, wird eine neue Season erstellt
**Response:**
```json
{
"success": true,
"message": "Team configured successfully",
"data": {
"team": {
"id": 1,
"name": "Jugend 11",
"myTischtennisTeamId": "2995094"
},
"league": {
"id": 5,
"name": "Jugend 13 1. Kreisklasse",
"myTischtennisGroupId": "504417",
"association": "HeTTV",
"groupname": "1.Kreisklasse"
},
"season": {
"id": 2,
"name": "25/26"
},
"parsedData": { ... }
}
}
```
### 3. URL für Team Abrufen
**Endpoint:** `GET /api/mytischtennis/team-url/:teamId`
**Response:**
```json
{
"success": true,
"url": "https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer%20TC%20%28J11%29/spielerbilanzen/gesamt"
}
```
## URL-Format
### Unterstützte URL-Muster
```
https://www.mytischtennis.de/click-tt/{association}/{season}/{type}/{groupname}/gruppe/{groupId}/mannschaft/{teamId}/{teamname}/...
```
**Komponenten:**
- `{association}`: Verband (z.B. "HeTTV", "DTTB", "WestD")
- `{season}`: Saison im Format "YY--YY" (z.B. "25--26" für 2025/2026)
- `{type}`: Typ (meist "ligen")
- `{groupname}`: Gruppenname URL-encoded (z.B. "1.Kreisklasse", "Kreisliga")
- `{groupId}`: Numerische Gruppen-ID (z.B. "504417")
- `{teamId}`: Numerische Team-ID (z.B. "2995094")
- `{teamname}`: Teamname URL-encoded mit Underscores (z.B. "Harheimer_TC_(J11)")
### Beispiel-URLs
**Spielerbilanzen:**
```
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/spielerbilanzen/gesamt
```
**Spielplan:**
```
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/spielplan
```
**Tabelle:**
```
https://www.mytischtennis.de/click-tt/HeTTV/25--26/ligen/1.Kreisklasse/gruppe/504417/mannschaft/2995094/Harheimer_TC_(J11)/tabelle
```
## Datenfluss
### Ohne MyTischtennis-Login
1. URL wird geparst
2. Nur URL-Komponenten werden extrahiert
3. Zusätzliche Daten (clubName, leagueName, etc.) sind nicht verfügbar
### Mit MyTischtennis-Login
1. URL wird geparst
2. API-Request an myTischtennis mit Authentication
3. Vollständige Team-Daten werden abgerufen
4. Alle Felder sind verfügbar
## Frontend-Integration
### Vue.js Beispiel
```javascript
<template>
<div>
<input
v-model="myTischtennisUrl"
placeholder="MyTischtennis URL einfügen..."
@blur="parseUrl"
/>
<div v-if="parsedData">
<h3>{{ parsedData.teamname }}</h3>
<p>Liga: {{ parsedData.leagueName }}</p>
<p>Verband: {{ parsedData.association }}</p>
<p>Tabelle: Platz {{ parsedData.tableRank }}</p>
<button @click="configureTeam">Team konfigurieren</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
myTischtennisUrl: '',
parsedData: null
};
},
methods: {
async parseUrl() {
if (!this.myTischtennisUrl) return;
try {
const response = await fetch('/api/mytischtennis/parse-url', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'userid': this.userId,
'authcode': this.authCode
},
body: JSON.stringify({
url: this.myTischtennisUrl
})
});
const result = await response.json();
this.parsedData = result.data;
} catch (error) {
console.error('Fehler beim Parsen:', error);
alert('URL konnte nicht geparst werden');
}
},
async configureTeam() {
try {
const response = await fetch('/api/mytischtennis/configure-team', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'userid': this.userId,
'authcode': this.authCode
},
body: JSON.stringify({
url: this.myTischtennisUrl,
clubTeamId: this.selectedTeamId,
createLeague: false,
createSeason: true
})
});
const result = await response.json();
if (result.success) {
alert('Team erfolgreich konfiguriert!');
}
} catch (error) {
console.error('Fehler bei Konfiguration:', error);
alert('Team konnte nicht konfiguriert werden');
}
}
}
};
</script>
```
## Workflow
### Empfohlener Workflow für Benutzer
1. **MyTischtennis-URL kopieren:**
- Auf myTischtennis.de zum Team navigieren
- URL aus Adresszeile kopieren
2. **URL in Trainingstagebuch einfügen:**
- Zu Team-Verwaltung navigieren
- URL einfügen
- Automatisches Parsen
3. **Konfiguration überprüfen:**
- Geparste Daten werden angezeigt
- Benutzer kann Daten überprüfen und bei Bedarf anpassen
4. **Team konfigurieren:**
- Auf "Konfigurieren" klicken
- System speichert alle benötigten IDs
- Automatischer Datenabruf ist ab sofort aktiv
## Fehlerbehandlung
### Häufige Fehler
**"Invalid myTischtennis URL format"**
- URL entspricht nicht dem erwarteten Format
- Lösung: Vollständige URL von der Spielerbilanzen-Seite kopieren
**"Season not found"**
- Saison existiert noch nicht in der Datenbank
- Lösung: `createSeason: true` setzen
**"Team has no league assigned"**
- Team hat keine verknüpfte Liga
- Lösung: `createLeague: true` setzen oder Liga manuell zuweisen
**"HTTP 401: Unauthorized"**
- MyTischtennis-Login abgelaufen oder nicht vorhanden
- Lösung: In MyTischtennis-Settings erneut anmelden
## Sicherheit
- ✅ Alle Endpoints erfordern Authentifizierung
- ✅ UserID wird aus Header-Parameter gelesen
- ✅ MyTischtennis-Credentials werden sicher gespeichert
- ✅ Keine sensiblen Daten in URLs
## Technische Details
### Service: `myTischtennisUrlParserService`
**Methoden:**
- `parseUrl(url)` - Parst URL und extrahiert Komponenten
- `fetchTeamData(parsedUrl, cookie, accessToken)` - Ruft zusätzliche Daten ab
- `getCompleteConfig(url, cookie, accessToken)` - Kombination aus Parsen + Abrufen
- `isValidTeamUrl(url)` - Validiert URL-Format
- `buildUrl(config)` - Baut URL aus Komponenten
### Controller: `myTischtennisUrlController`
**Endpoints:**
- `POST /api/mytischtennis/parse-url` - URL parsen
- `POST /api/mytischtennis/configure-team` - Team konfigurieren
- `GET /api/mytischtennis/team-url/:teamId` - URL abrufen
## Zukünftige Erweiterungen
### Geplante Features
1. **Bulk-Import:**
- Mehrere URLs gleichzeitig importieren
- Alle Teams einer Liga auf einmal konfigurieren
2. **Auto-Discovery:**
- Automatisches Finden aller Teams eines Vereins
- Vorschläge für ähnliche Teams
3. **Validierung:**
- Prüfung, ob Team bereits konfiguriert ist
- Warnung bei Duplikaten
4. **History:**
- Speichern der URL-Konfigurationen
- Versionierung bei Änderungen

View File

@@ -0,0 +1,273 @@
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) {
const result = await this.authenticatedRequest('/?_data=root', cookie, {
method: 'GET'
});
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
});
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;
while (hasMorePages) {
const endpoint = `/rankings/andro-rangliste?all-players=on&clubnr=${clubId}&fednickname=${fedNickname}&results-per-page=100&page=${currentPage}&_data=routes%2F%24`;
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'
};
}
// 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;
} else if (rankingData.numberOfPages && currentPage >= rankingData.numberOfPages - 1) {
hasMorePages = false;
} else if (allEntries.length >= rankingData.resultLength) {
hasMorePages = false;
} else {
currentPage++;
}
}
return {
success: true,
entries: allEntries,
metadata: {
totalEntries: allEntries.length,
pagesFetched: currentPage + 1
}
};
}
}
export default new MyTischtennisClient();

View File

@@ -0,0 +1,128 @@
import ClubTeamService from '../services/clubTeamService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getClubTeams = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
// Check if user has access to this club
const clubTeams = await ClubTeamService.getAllClubTeamsByClub(clubId, seasonId);
res.status(200).json(clubTeams);
} catch (error) {
console.error('[getClubTeams] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const clubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
if (!clubTeam) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(clubTeam);
} catch (error) {
console.error('[getClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
if (!name) {
return res.status(400).json({ error: "missingname" });
}
const clubTeamData = {
name,
clubId: parseInt(clubId),
leagueId: leagueId ? parseInt(leagueId) : null,
seasonId: seasonId ? parseInt(seasonId) : null
};
const newClubTeam = await ClubTeamService.createClubTeam(clubTeamData);
res.status(201).json(newClubTeam);
} catch (error) {
console.error('[createClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const updateClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
const updateData = {};
if (name !== undefined) updateData.name = name;
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
const success = await ClubTeamService.updateClubTeam(clubTeamId, updateData);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
const updatedClubTeam = await ClubTeamService.getClubTeamById(clubTeamId);
res.status(200).json(updatedClubTeam);
} catch (error) {
console.error('[updateClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteClubTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const success = await ClubTeamService.deleteClubTeam(clubTeamId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Club team deleted successfully" });
} catch (error) {
console.error('[deleteClubTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getLeagues = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
const leagues = await ClubTeamService.getLeaguesByClub(clubId, seasonId);
res.status(200).json(leagues);
} catch (error) {
console.error('[getLeagues] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -1,77 +1,57 @@
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');
const clubs = await ClubService.getAllClubs();
console.log('[getClubs] - prepare response');
res.status(200).json(clubs);
console.log('[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');
const { authcode: token } = req.headers;
const { name: clubName } = req.body;
try {
console.log('[addClub] - find club by name');
const club = await ClubService.findClubByName(clubName);
console.log('[addClub] - get user');
const user = await getUserByToken(token);
console.log('[addClub] - check if club already exists');
if (club) {
res.status(409).json({ error: "alreadyexists" });
return;
}
console.log('[addClub] - create club');
const newClub = await ClubService.createClub(clubName);
console.log('[addClub] - add user to new club');
await ClubService.addUserToClub(user.id, newClub.id);
console.log('[addClub] - prepare response');
res.status(200).json(newClub);
console.log('[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');
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
console.log('[getClub] - get user');
const user = await getUserByToken(token);
console.log('[getClub] - get users club');
const access = await ClubService.getUserClubAccess(user.id, clubId);
console.log('[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');
const club = await ClubService.findClubById(clubId);
console.log('[getClub] - check club exists');
if (!club) {
return res.status(404).json({ message: 'Club not found' });
}
console.log('[getClub] - set response');
res.status(200).json(club);
console.log('[getClub] - done');
} catch (error) {
console.log(error);
console.error('[getClub] - error:', error);
res.status(500).json({ message: 'Server error' });
}
};
@@ -82,7 +62,6 @@ export const requestClubAccess = async (req, res) => {
try {
const user = await getUserByToken(token);
console.log(user);
await ClubService.requestAccessToClub(user.id, clubId);
res.status(200).json({});
@@ -92,6 +71,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,10 @@ 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);
res.status(500).json({ error: 'Error creating tag' });
}
};

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,10 +22,11 @@ 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);
const { seasonid: seasonId } = req.query;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId, seasonId);
return res.status(200).json(leagues);
} catch (error) {
console.error('Error retrieving leagues:', error);
@@ -36,7 +38,8 @@ export const getMatchesForLeagues = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId);
const { seasonid: seasonId } = req.query;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId, seasonId);
return res.status(200).json(matches);
} catch (error) {
console.error('Error retrieving matches:', error);
@@ -55,3 +58,47 @@ export const getMatchesForLeague = async (req, res) => {
return res.status(500).json({ error: 'Failed to retrieve matches' });
}
};
export const getLeagueTable = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
return res.status(200).json(table);
} catch (error) {
console.error('Error retrieving league table:', error);
return res.status(500).json({ error: 'Failed to retrieve league table' });
}
};
export const fetchLeagueTableFromMyTischtennis = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const autoFetchService = (await import('../services/autoFetchMatchResultsService.js')).default;
await autoFetchService.fetchAndUpdateLeagueTable(userId, leagueId);
// Return updated table data
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
return res.status(200).json({
message: 'League table updated from MyTischtennis',
data: table
});
} catch (error) {
console.error('Error fetching league table from MyTischtennis:', error);
return res.status(500).json({ error: 'Failed to fetch league table from MyTischtennis' });
}
};

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,17 @@ const getClubMembers = async(req, res) => {
}
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
} catch(error) {
console.log('[getClubMembers] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
}
const getWaitingApprovals = async(req, res) => {
try {
console.log('[getWaitingApprovals] - Start');
const { id: clubId } = req.params;
console.log('[getWaitingApprovals] - get token');
const { authcode: userToken } = req.headers;
console.log('[getWaitingApprovals] - load for waiting approvals');
const waitingApprovals = await MemberService.getApprovalRequests(userToken, clubId);
console.log('[getWaitingApprovals] - set response');
res.status(200).json(waitingApprovals);
console.log('[getWaitingApprovals] - done');
} catch(error) {
console.log('[getWaitingApprovals] - Error: ', error);
res.status(403).json({ error: error });
}
}
@@ -34,11 +28,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 +53,6 @@ const uploadMemberImage = async (req, res) => {
};
const getMemberImage = async (req, res) => {
console.log('[getMemberImage]');
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
@@ -75,4 +68,37 @@ const getMemberImage = async (req, res) => {
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage };
const updateRatingsFromMyTischtennis = async (req, res) => {
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' });
}
};
const rotateMemberImage = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { direction } = req.body;
const { authcode: userToken } = req.headers;
if (!direction || !['left', 'right'].includes(direction)) {
return res.status(400).json({
success: false,
error: 'Ungültige Drehrichtung. Verwenden Sie "left" oder "right".'
});
}
const result = await MemberService.rotateMemberImage(userToken, clubId, memberId, direction);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[rotateMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to rotate image' });
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage };

View File

@@ -1,15 +1,14 @@
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);
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
res.status(200).json(notes);
} catch (error) {
console.log('[getMemberNotes] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -18,12 +17,10 @@ const addMemberNote = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { memberId, content, clubId } = req.body;
console.log('[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);
res.status(500).json({ error: 'systemerror' });
}
};
@@ -33,13 +30,11 @@ const deleteMemberNote = async (req, res) => {
const { authcode: userToken } = req.headers;
const { noteId } = req.params;
const { clubId } = req.body;
console.log('[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);
res.status(500).json({ error: 'systemerror' });
}
};

View File

@@ -0,0 +1,205 @@
import myTischtennisService from '../services/myTischtennisService.js';
import HttpError from '../exceptions/HttpError.js';
class MyTischtennisController {
/**
* GET /api/mytischtennis/account
* Get current user's myTischtennis account
*/
async getAccount(req, res, next) {
try {
const userId = req.user.id;
const account = await myTischtennisService.getAccount(userId);
if (!account) {
return res.status(200).json({ account: null });
}
res.status(200).json({ account });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/status
* Check account configuration status
*/
async getStatus(req, res, next) {
try {
const userId = req.user.id;
const status = await myTischtennisService.checkAccountStatus(userId);
res.status(200).json(status);
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/account
* Create or update myTischtennis account
*/
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, autoUpdateRatings, userPassword } = 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 myTischtennisService.upsertAccount(
userId,
email,
password,
savePassword || false,
autoUpdateRatings || false,
userPassword
);
res.status(200).json({
message: 'myTischtennis-Account erfolgreich gespeichert',
account
});
} catch (error) {
next(error);
}
}
/**
* DELETE /api/mytischtennis/account
* Delete myTischtennis account
*/
async deleteAccount(req, res, next) {
try {
const userId = req.user.id;
const deleted = await myTischtennisService.deleteAccount(userId);
if (!deleted) {
throw new HttpError(404, 'Kein myTischtennis-Account gefunden');
}
res.status(200).json({ message: 'myTischtennis-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 } = req.body;
const result = await myTischtennisService.verifyLogin(userId, password);
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
* Get stored session data for authenticated requests
*/
async getSession(req, res, next) {
try {
const userId = req.user.id;
const session = await myTischtennisService.getSession(userId);
res.status(200).json({ session });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/update-history
* Get update ratings history
*/
async getUpdateHistory(req, res, next) {
try {
const userId = req.user.id;
const history = await myTischtennisService.getUpdateHistory(userId);
res.status(200).json({ history });
} catch (error) {
next(error);
}
}
/**
* Get fetch logs for current user
*/
async getFetchLogs(req, res, next) {
try {
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
const logs = await fetchLogService.getFetchLogs(userId, {
limit: req.query.limit ? parseInt(req.query.limit) : 50,
fetchType: req.query.type
});
res.status(200).json({ logs });
} catch (error) {
next(error);
}
}
/**
* Get latest successful fetches for each type
*/
async getLatestFetches(req, res, next) {
try {
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
const latestFetches = await fetchLogService.getLatestSuccessfulFetches(userId);
res.status(200).json({ latestFetches });
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisController();

View File

@@ -0,0 +1,477 @@
import myTischtennisUrlParserService from '../services/myTischtennisUrlParserService.js';
import myTischtennisService from '../services/myTischtennisService.js';
import autoFetchMatchResultsService from '../services/autoFetchMatchResultsService.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import User from '../models/User.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class MyTischtennisUrlController {
/**
* Parse myTischtennis URL and return configuration data
* POST /api/mytischtennis/parse-url
* Body: { url: string }
*/
async parseUrl(req, res, next) {
try {
const { url } = req.body;
if (!url) {
throw new HttpError(400, 'URL is required');
}
// Validate URL
if (!myTischtennisUrlParserService.isValidTeamUrl(url)) {
throw new HttpError(400, 'Invalid myTischtennis URL format');
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
// Try to fetch additional data if user is authenticated
const userIdOrEmail = req.headers.userid;
let completeData = parsedData;
if (userIdOrEmail) {
// Get actual user ID
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (user) userId = user.id;
}
try {
const account = await myTischtennisService.getAccount(userId);
if (account && account.accessToken) {
completeData = await myTischtennisUrlParserService.fetchTeamData(
parsedData,
account.cookie,
account.accessToken
);
}
} catch (error) {
console.error('Error fetching additional team data:', error);
// Continue with parsed data only
}
}
res.json({
success: true,
data: completeData
});
} catch (error) {
next(error);
}
}
/**
* Configure team from myTischtennis URL
* POST /api/mytischtennis/configure-team
* Body: { url: string, clubTeamId: number, createLeague?: boolean, createSeason?: boolean }
*/
async configureTeam(req, res, next) {
try {
const { url, clubTeamId, createLeague, createSeason } = req.body;
const userIdOrEmail = req.headers.userid;
if (!url || !clubTeamId) {
throw new HttpError(400, 'URL and clubTeamId are required');
}
// Get actual user ID
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError(404, 'User not found');
}
userId = user.id;
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
// Try to fetch additional data
let completeData = parsedData;
const account = await myTischtennisService.getAccount(userId);
if (account && account.accessToken) {
try {
completeData = await myTischtennisUrlParserService.fetchTeamData(
parsedData,
account.cookie,
account.accessToken
);
} catch (error) {
console.error('Error fetching team data:', error);
}
}
// Find or create season
let season = await Season.findOne({
where: { season: completeData.season }
});
if (!season && createSeason) {
season = await Season.create({
season: completeData.season
});
}
if (!season) {
throw new HttpError(404, `Season ${completeData.season} not found. Set createSeason=true to create it.`);
}
// Find or create league
const team = await ClubTeam.findByPk(clubTeamId);
if (!team) {
throw new HttpError(404, 'Club team not found');
}
let league;
// First, try to find existing league by name and season
const leagueName = completeData.leagueName || completeData.groupname;
league = await League.findOne({
where: {
name: leagueName,
seasonId: season.id,
clubId: team.clubId
}
});
if (league) {
devLog(`Found existing league: ${league.name} (ID: ${league.id})`);
// Update myTischtennis fields
await league.update({
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
} else if (team.leagueId) {
// Team has a league assigned, update it
league = await League.findByPk(team.leagueId);
if (league) {
devLog(`Updating team's existing league: ${league.name} (ID: ${league.id})`);
await league.update({
name: leagueName,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
}
} else if (createLeague) {
// Create new league
devLog(`Creating new league: ${leagueName}`);
league = await League.create({
name: leagueName,
seasonId: season.id,
clubId: team.clubId,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
} else {
throw new HttpError(400, 'League not found and team has no league assigned. Set createLeague=true to create one.');
}
// Update team
await team.update({
myTischtennisTeamId: completeData.teamId,
leagueId: league.id,
seasonId: season.id
});
res.json({
success: true,
message: 'Team configured successfully',
data: {
team: {
id: team.id,
name: team.name,
myTischtennisTeamId: completeData.teamId
},
league: {
id: league.id,
name: league.name,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
},
season: {
id: season.id,
name: season.season
},
parsedData: completeData
}
});
} catch (error) {
next(error);
}
}
/**
* Manually fetch team data from myTischtennis
* POST /api/mytischtennis/fetch-team-data
* Body: { clubTeamId: number }
*/
async fetchTeamData(req, res, next) {
try {
const { clubTeamId } = req.body;
const userIdOrEmail = req.headers.userid;
if (!clubTeamId) {
throw new HttpError(400, 'clubTeamId is required');
}
// Get actual user ID (userid header might be email address)
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
// It's an email, find the user
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError(404, 'User not found');
}
userId = user.id;
}
// Get myTischtennis session (similar to memberService.updateRatingsFromMyTischtennis)
console.log('Fetching session for userId:', userId, '(from header:', userIdOrEmail, ')');
let session;
try {
session = await myTischtennisService.getSession(userId);
console.log('Session found:', !!session);
} catch (sessionError) {
console.log('Session invalid, attempting login...', sessionError.message);
// Versuche automatischen Login mit gespeicherten Credentials
try {
await myTischtennisService.verifyLogin(userId);
session = await myTischtennisService.getSession(userId);
console.log('Automatic login successful');
} catch (loginError) {
console.error('Automatic login failed:', loginError.message);
throw new HttpError(401, 'MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.');
}
}
// Get account data (for clubId, etc.)
const account = await myTischtennisService.getAccount(userId);
if (!account) {
throw new HttpError(404, 'MyTischtennis-Account nicht verknüpft. Bitte verknüpfen Sie Ihren Account in den MyTischtennis-Einstellungen.');
}
console.log('Using session:', {
email: account.email,
hasCookie: !!session.cookie,
hasAccessToken: !!session.accessToken,
expiresAt: new Date(session.expiresAt * 1000)
});
// Get team with league and season
const team = await ClubTeam.findByPk(clubTeamId, {
include: [
{
model: League,
as: 'league',
include: [
{
model: Season,
as: 'season'
}
]
}
]
});
if (!team) {
throw new HttpError(404, 'Team not found');
}
console.log('Team data:', {
id: team.id,
name: team.name,
myTischtennisTeamId: team.myTischtennisTeamId,
hasLeague: !!team.league,
leagueData: team.league ? {
id: team.league.id,
name: team.league.name,
myTischtennisGroupId: team.league.myTischtennisGroupId,
association: team.league.association,
groupname: team.league.groupname,
hasSeason: !!team.league.season
} : null
});
if (!team.myTischtennisTeamId || !team.league || !team.league.myTischtennisGroupId) {
throw new HttpError(400, 'Team is not configured for myTischtennis');
}
// Fetch data for this specific team
const startTime = Date.now();
let matchResultsSuccess = false;
let tableUpdateSuccess = false;
let matchResultsCount = 0;
let tableUpdateCount = 0;
try {
const result = await autoFetchMatchResultsService.fetchTeamResults(
{
userId: account.userId,
email: account.email,
cookie: session.cookie,
accessToken: session.accessToken,
expiresAt: session.expiresAt,
getPassword: () => null // Not needed for manual fetch
},
team
);
matchResultsSuccess = true;
matchResultsCount = result.fetchedCount || 0;
// Log match results fetch
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
await fetchLogService.logFetch(
account.userId,
'match_results',
true,
`${matchResultsCount} Spielergebnisse erfolgreich abgerufen`,
{
recordsProcessed: matchResultsCount,
executionTime: Date.now() - startTime,
isAutomatic: false
}
);
} catch (error) {
console.error('Error fetching match results:', error);
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
await fetchLogService.logFetch(
account.userId,
'match_results',
false,
'Fehler beim Abrufen der Spielergebnisse',
{
errorDetails: error.message,
executionTime: Date.now() - startTime,
isAutomatic: false
}
);
}
// Also fetch and update league table data
let tableUpdateResult = null;
const tableStartTime = Date.now();
try {
await autoFetchMatchResultsService.fetchAndUpdateLeagueTable(account.userId, team.league.id);
tableUpdateResult = 'League table updated successfully';
tableUpdateSuccess = true;
tableUpdateCount = 1; // One table updated
console.log('✓ League table updated for league:', team.league.id);
// Log league table fetch
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
await fetchLogService.logFetch(
account.userId,
'league_table',
true,
'Ligatabelle erfolgreich aktualisiert',
{
recordsProcessed: tableUpdateCount,
executionTime: Date.now() - tableStartTime,
isAutomatic: false
}
);
} catch (error) {
console.error('Error fetching league table data:', error);
tableUpdateResult = 'League table update failed: ' + error.message;
// Log league table fetch failure
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
await fetchLogService.logFetch(
account.userId,
'league_table',
false,
'Fehler beim Aktualisieren der Ligatabelle',
{
errorDetails: error.message,
executionTime: Date.now() - tableStartTime,
isAutomatic: false
}
);
// Don't fail the entire request if table update fails
}
res.json({
success: true,
message: `${matchResultsCount} Datensätze abgerufen und verarbeitet`,
data: {
fetchedCount: matchResultsCount,
teamName: team.name,
tableUpdate: tableUpdateResult
}
});
} catch (error) {
console.error('Error in fetchTeamData:', error);
console.error('Error stack:', error.stack);
next(error);
}
}
/**
* Get myTischtennis URL for a team
* GET /api/mytischtennis/team-url/:teamId
*/
async getTeamUrl(req, res, next) {
try {
const { teamId } = req.params;
const team = await ClubTeam.findByPk(teamId, {
include: [
{
model: League,
as: 'league',
include: [
{
model: Season,
as: 'season'
}
]
}
]
});
if (!team) {
throw new HttpError(404, 'Team not found');
}
if (!team.myTischtennisTeamId || !team.league || !team.league.myTischtennisGroupId) {
throw new HttpError(400, 'Team is not configured for myTischtennis');
}
const url = myTischtennisUrlParserService.buildUrl({
association: team.league.association,
season: team.league.season.name,
groupname: team.league.groupname,
groupId: team.league.myTischtennisGroupId,
teamId: team.myTischtennisTeamId,
teamname: team.name
});
res.json({
success: true,
url
});
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisUrlController();

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,97 @@
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;
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

@@ -0,0 +1,103 @@
import SeasonService from '../services/seasonService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getSeasons = async (req, res) => {
try {
const { authcode: token } = req.headers;
const user = await getUserByToken(token);
const seasons = await SeasonService.getAllSeasons();
res.status(200).json(seasons);
} catch (error) {
console.error('[getSeasons] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getCurrentSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const user = await getUserByToken(token);
const season = await SeasonService.getOrCreateCurrentSeason();
res.status(200).json(season);
} catch (error) {
console.error('[getCurrentSeason] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { season } = req.body;
const user = await getUserByToken(token);
if (!season) {
return res.status(400).json({ error: "missingseason" });
}
// Validiere Saison-Format (z.B. "2023/2024")
const seasonRegex = /^\d{4}\/\d{4}$/;
if (!seasonRegex.test(season)) {
return res.status(400).json({ error: "invalidseasonformat" });
}
const newSeason = await SeasonService.createSeason(season);
res.status(201).json(newSeason);
} catch (error) {
console.error('[createSeason] - Error:', error);
if (error.message === 'Season already exists') {
res.status(409).json({ error: "alreadyexists" });
} else {
res.status(500).json({ error: "internalerror" });
}
}
};
export const getSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { seasonid: seasonId } = req.params;
const user = await getUserByToken(token);
const season = await SeasonService.getSeasonById(seasonId);
if (!season) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(season);
} catch (error) {
console.error('[getSeason] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteSeason = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { seasonid: seasonId } = req.params;
const user = await getUserByToken(token);
const success = await SeasonService.deleteSeason(seasonId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "deleted" });
} catch (error) {
console.error('[deleteSeason] - Error:', error);
if (error.message === 'Season is used by teams' || error.message === 'Season is used by leagues') {
res.status(409).json({ error: "seasoninuse" });
} else {
res.status(500).json({ error: "internalerror" });
}
}
};

View File

@@ -0,0 +1,130 @@
import TeamService from '../services/teamService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
export const getTeams = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
// Check if user has access to this club
const teams = await TeamService.getAllTeamsByClub(clubId, seasonId);
res.status(200).json(teams);
} catch (error) {
console.error('[getTeams] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const user = await getUserByToken(token);
const team = await TeamService.getTeamById(teamId);
if (!team) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(team);
} catch (error) {
console.error('[getTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const createTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
if (!name) {
return res.status(400).json({ error: "missingname" });
}
const teamData = {
name,
clubId: parseInt(clubId),
leagueId: leagueId ? parseInt(leagueId) : null,
seasonId: seasonId ? parseInt(seasonId) : null
};
const newTeam = await TeamService.createTeam(teamData);
res.status(201).json(newTeam);
} catch (error) {
console.error('[createTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const updateTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const { name, leagueId, seasonId } = req.body;
const user = await getUserByToken(token);
const updateData = {};
if (name !== undefined) updateData.name = name;
if (leagueId !== undefined) updateData.leagueId = leagueId ? parseInt(leagueId) : null;
if (seasonId !== undefined) updateData.seasonId = seasonId ? parseInt(seasonId) : null;
const success = await TeamService.updateTeam(teamId, updateData);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
const updatedTeam = await TeamService.getTeamById(teamId);
res.status(200).json(updatedTeam);
} catch (error) {
console.error('[updateTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteTeam = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { teamid: teamId } = req.params;
const user = await getUserByToken(token);
const success = await TeamService.deleteTeam(teamId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "deleted" });
} catch (error) {
console.error('[deleteTeam] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getLeagues = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubid: clubId } = req.params;
const { seasonid: seasonId } = req.query;
const user = await getUserByToken(token);
const leagues = await TeamService.getLeaguesByClub(clubId, seasonId);
res.status(200).json(leagues);
} catch (error) {
console.error('[getLeagues] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

View File

@@ -0,0 +1,215 @@
import multer from 'multer';
import path from 'path';
import TeamDocumentService from '../services/teamDocumentService.js';
import PDFParserService from '../services/pdfParserService.js';
import { getUserByToken } from '../utils/userUtils.js';
import { devLog } from '../utils/logger.js';
// Multer-Konfiguration für Datei-Uploads
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/temp/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
}
});
const upload = multer({
storage: storage,
limits: {
fileSize: 10 * 1024 * 1024 // 10MB Limit
},
fileFilter: (req, file, cb) => {
// Erlaube nur PDF, DOC, DOCX, TXT, CSV Dateien
const allowedTypes = /pdf|doc|docx|txt|csv/;
const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
const mimetype = allowedTypes.test(file.mimetype);
if (mimetype && extname) {
return cb(null, true);
} else {
cb(new Error('Nur PDF, DOC, DOCX, TXT und CSV Dateien sind erlaubt!'));
}
}
});
export const uploadMiddleware = upload.single('document');
export const uploadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const { documentType } = req.body;
const user = await getUserByToken(token);
if (!req.file) {
return res.status(400).json({ error: "nofile" });
}
if (!documentType || !['code_list', 'pin_list'].includes(documentType)) {
return res.status(400).json({ error: "invaliddocumenttype" });
}
const document = await TeamDocumentService.uploadDocument(req.file, clubTeamId, documentType);
res.status(201).json(document);
} catch (error) {
console.error('[uploadDocument] - Error:', error);
// Lösche temporäre Datei bei Fehler
if (req.file && req.file.path) {
try {
const fs = await import('fs');
fs.unlinkSync(req.file.path);
} catch (cleanupError) {
console.error('Fehler beim Löschen der temporären Datei:', cleanupError);
}
}
if (error.message === 'Club-Team nicht gefunden') {
return res.status(404).json({ error: "clubteamnotfound" });
}
res.status(500).json({ error: "internalerror" });
}
};
export const getDocuments = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { clubteamid: clubTeamId } = req.params;
const user = await getUserByToken(token);
const documents = await TeamDocumentService.getDocumentsByClubTeam(clubTeamId);
res.status(200).json(documents);
} catch (error) {
console.error('[getDocuments] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const getDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json(document);
} catch (error) {
console.error('[getDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const downloadDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "notfound" });
}
const filePath = await TeamDocumentService.getDocumentPath(documentId);
if (!filePath) {
return res.status(404).json({ error: "filenotfound" });
}
// Prüfe ob Datei existiert
const fs = await import('fs');
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: "filenotfound" });
}
// Setze Headers für Inline-Anzeige (PDF-Viewer)
res.setHeader('Content-Disposition', `inline; filename="${document.originalFileName}"`);
res.setHeader('Content-Type', document.mimeType);
// Sende die Datei
res.sendFile(filePath);
} catch (error) {
console.error('[downloadDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const deleteDocument = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const user = await getUserByToken(token);
const success = await TeamDocumentService.deleteDocument(documentId);
if (!success) {
return res.status(404).json({ error: "notfound" });
}
res.status(200).json({ message: "Document deleted successfully" });
} catch (error) {
console.error('[deleteDocument] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};
export const parsePDF = async (req, res) => {
try {
const { authcode: token } = req.headers;
const { documentid: documentId } = req.params;
const { leagueid: leagueId } = req.query;
const user = await getUserByToken(token);
if (!leagueId) {
return res.status(400).json({ error: "missingleagueid" });
}
// Hole Dokument-Informationen
const document = await TeamDocumentService.getDocumentById(documentId);
if (!document) {
return res.status(404).json({ error: "documentnotfound" });
}
// Prüfe ob es eine PDF- oder TXT-Datei ist
if (!document.mimeType.includes('pdf') && !document.mimeType.includes('text/plain')) {
return res.status(400).json({ error: "notapdfortxt" });
}
// Parse PDF
const parseResult = await PDFParserService.parsePDF(document.filePath, document.clubTeam.clubId);
// Speichere Matches in Datenbank
const saveResult = await PDFParserService.saveMatchesToDatabase(parseResult.matches, parseInt(leagueId));
res.status(200).json({
parseResult: {
matchesFound: parseResult.matches.length,
debugInfo: parseResult.debugInfo,
allLines: parseResult.allLines,
rawText: parseResult.rawText
},
saveResult: {
created: saveResult.created,
updated: saveResult.updated,
errors: saveResult.errors
}
});
} catch (error) {
console.error('[parsePDF] - Error:', error);
res.status(500).json({ error: "internalerror" });
}
};

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,13 @@
-- Migration: Add auto update ratings fields to my_tischtennis table
-- Date: 2025-01-27
-- Add auto_update_ratings column
ALTER TABLE my_tischtennis
ADD COLUMN auto_update_ratings BOOLEAN NOT NULL DEFAULT FALSE;
-- Add last_update_ratings column
ALTER TABLE my_tischtennis
ADD COLUMN last_update_ratings TIMESTAMP NULL;
-- Create index for auto_update_ratings for efficient querying
CREATE INDEX idx_my_tischtennis_auto_update_ratings ON my_tischtennis(auto_update_ratings);

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,28 @@
-- Migration: Add match result fields to match table
-- Date: 2025-01-27
-- For MariaDB
-- Add myTischtennis meeting ID
ALTER TABLE `match`
ADD COLUMN my_tischtennis_meeting_id VARCHAR(255) NULL UNIQUE COMMENT 'Meeting ID from myTischtennis (e.g. 15440488)';
-- Add home match points
ALTER TABLE `match`
ADD COLUMN home_match_points INT DEFAULT 0 NULL COMMENT 'Match points won by home team';
-- Add guest match points
ALTER TABLE `match`
ADD COLUMN guest_match_points INT DEFAULT 0 NULL COMMENT 'Match points won by guest team';
-- Add is_completed flag
ALTER TABLE `match`
ADD COLUMN is_completed BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Whether the match is completed';
-- Add PDF URL
ALTER TABLE `match`
ADD COLUMN pdf_url VARCHAR(512) NULL COMMENT 'PDF URL from myTischtennis';
-- Create indexes
CREATE INDEX idx_match_my_tischtennis_meeting_id ON `match`(my_tischtennis_meeting_id);
CREATE INDEX idx_match_is_completed ON `match`(is_completed);

View File

@@ -0,0 +1,4 @@
-- Add matches_tied column to team table
ALTER TABLE team
ADD COLUMN matches_tied INTEGER NOT NULL DEFAULT 0 AFTER matches_lost;

View File

@@ -0,0 +1,19 @@
-- Migration: Add myTischtennis fields to league table
-- Date: 2025-01-27
-- For MariaDB
-- Add my_tischtennis_group_id column
ALTER TABLE league
ADD COLUMN my_tischtennis_group_id VARCHAR(255) NULL COMMENT 'Group ID from myTischtennis (e.g. 504417)';
-- Add association column
ALTER TABLE league
ADD COLUMN association VARCHAR(255) NULL COMMENT 'Association/Verband (e.g. HeTTV)';
-- Add groupname column
ALTER TABLE league
ADD COLUMN groupname VARCHAR(255) NULL COMMENT 'Group name for URL (e.g. 1.Kreisklasse)';
-- Create index for efficient querying
CREATE INDEX idx_league_my_tischtennis_group_id ON league(my_tischtennis_group_id);

View File

@@ -0,0 +1,11 @@
-- Migration: Add myTischtennis player ID to member table
-- Date: 2025-01-27
-- For MariaDB
-- Add my_tischtennis_player_id column
ALTER TABLE member
ADD COLUMN my_tischtennis_player_id VARCHAR(255) NULL COMMENT 'Player ID from myTischtennis (e.g. NU2705037)';
-- Create index for efficient querying
CREATE INDEX idx_member_my_tischtennis_player_id ON member(my_tischtennis_player_id);

View File

@@ -0,0 +1,11 @@
-- Migration: Add myTischtennis team ID to club_team table
-- Date: 2025-01-27
-- For MariaDB
-- Add my_tischtennis_team_id column
ALTER TABLE club_team
ADD COLUMN my_tischtennis_team_id VARCHAR(255) NULL COMMENT 'Team ID from myTischtennis (e.g. 2995094)';
-- Create index for efficient querying
CREATE INDEX idx_club_team_my_tischtennis_team_id ON club_team(my_tischtennis_team_id);

View File

@@ -0,0 +1,44 @@
-- Migration: Add season_id to teams table
-- First, add the column as nullable
ALTER TABLE `team` ADD COLUMN `season_id` INT NULL;
-- Get or create current season
SET @current_season_id = (
SELECT id FROM `season`
WHERE season = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
);
-- If no season exists, create it
INSERT IGNORE INTO `season` (season) VALUES (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
);
-- Get the season ID again (in case we just created it)
SET @current_season_id = (
SELECT id FROM `season`
WHERE season = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
);
-- Update all existing teams to use the current season
UPDATE `team` SET `season_id` = @current_season_id WHERE `season_id` IS NULL;
-- Now make the column NOT NULL and add the foreign key constraint
ALTER TABLE `team` MODIFY COLUMN `season_id` INT NOT NULL;
ALTER TABLE `team` ADD CONSTRAINT `team_season_id_foreign_idx`
FOREIGN KEY (`season_id`) REFERENCES `season` (`id`)
ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,11 @@
-- Migration: Add table fields to team table
-- Add fields for league table calculations
ALTER TABLE team ADD COLUMN matches_played INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN matches_won INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN matches_lost INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN sets_won INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN sets_lost INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN points_won INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN points_lost INT NOT NULL DEFAULT 0;
ALTER TABLE team ADD COLUMN table_points INT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,5 @@
-- Add table_points_won and table_points_lost columns to team table
ALTER TABLE team
ADD COLUMN table_points_won INTEGER NOT NULL DEFAULT 0 AFTER table_points,
ADD COLUMN table_points_lost INTEGER NOT NULL DEFAULT 0 AFTER table_points_won;

View File

@@ -0,0 +1,20 @@
-- Create my_tischtennis_fetch_log table for tracking data fetches
CREATE TABLE IF NOT EXISTS my_tischtennis_fetch_log (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
fetch_type ENUM('ratings', 'match_results', 'league_table') NOT NULL COMMENT 'Type of data fetch',
success BOOLEAN NOT NULL DEFAULT FALSE,
message TEXT,
error_details TEXT,
records_processed INT NOT NULL DEFAULT 0 COMMENT 'Number of records processed',
execution_time INT COMMENT 'Execution time in milliseconds',
is_automatic BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Automatic or manual fetch',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE ON UPDATE CASCADE,
INDEX idx_user_fetch_type_created (user_id, fetch_type, created_at),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,23 @@
-- Migration: Create my_tischtennis_update_history table
-- Date: 2025-01-27
-- For MariaDB
CREATE TABLE IF NOT EXISTS my_tischtennis_update_history (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
success BOOLEAN NOT NULL DEFAULT FALSE,
message TEXT,
error_details TEXT,
updated_count INT DEFAULT 0,
execution_time INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_my_tischtennis_update_history_user_id
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create indexes for efficient querying
CREATE INDEX idx_my_tischtennis_update_history_user_id ON my_tischtennis_update_history(user_id);
CREATE INDEX idx_my_tischtennis_update_history_created_at ON my_tischtennis_update_history(created_at);
CREATE INDEX idx_my_tischtennis_update_history_success ON my_tischtennis_update_history(success);

View File

@@ -0,0 +1,8 @@
-- Migration: Make locationId optional in match table
-- Date: 2025-01-27
-- For MariaDB
-- Modify locationId to allow NULL
ALTER TABLE `match`
MODIFY COLUMN location_id INT NULL;

View File

@@ -0,0 +1,60 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Season from './Season.js';
const ClubTeam = sequelize.define('ClubTeam', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
leagueId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: League,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Season,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
myTischtennisTeamId: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Team ID from myTischtennis (e.g. 2995094)',
field: 'my_tischtennis_team_id'
},
}, {
underscored: true,
tableName: 'club_team',
timestamps: true,
});
export default ClubTeam;

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

@@ -34,6 +34,22 @@ const League = sequelize.define('League', {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
myTischtennisGroupId: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Group ID from myTischtennis (e.g. 504417)',
field: 'my_tischtennis_group_id'
},
association: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Association/Verband (e.g. HeTTV)',
},
groupname: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Group name for URL (e.g. 1.Kreisklasse)',
},
}, {
underscored: true,
tableName: 'league',

View File

@@ -3,7 +3,6 @@ import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Team from './Team.js';
import Season from './Season.js';
import Location from './Location.js';
const Match = sequelize.define('Match', {
@@ -21,21 +20,13 @@ const Match = sequelize.define('Match', {
type: DataTypes.TIME,
allowNull: true,
},
seasonId: {
type: DataTypes.INTEGER,
references: {
model: Season,
key: 'id',
},
allowNull: false,
},
locationId: {
type: DataTypes.INTEGER,
references: {
model: Location,
key: 'id',
},
allowNull: false,
allowNull: true,
},
homeTeamId: {
type: DataTypes.INTEGER,
@@ -69,6 +60,55 @@ const Match = sequelize.define('Match', {
},
allowNull: false,
},
code: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Spiel-Code aus PDF-Parsing'
},
homePin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Heimteam aus PDF-Parsing'
},
guestPin: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Pin-Code für Gastteam aus PDF-Parsing'
},
myTischtennisMeetingId: {
type: DataTypes.STRING,
allowNull: true,
unique: true,
comment: 'Meeting ID from myTischtennis (e.g. 15440488)',
field: 'my_tischtennis_meeting_id'
},
homeMatchPoints: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 0,
comment: 'Match points won by home team',
field: 'home_match_points'
},
guestMatchPoints: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 0,
comment: 'Match points won by guest team',
field: 'guest_match_points'
},
isCompleted: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Whether the match is completed',
field: 'is_completed'
},
pdfUrl: {
type: DataTypes.STRING,
allowNull: true,
comment: 'PDF URL from myTischtennis',
field: 'pdf_url'
},
}, {
underscored: true,
tableName: 'match',

View File

@@ -45,9 +45,9 @@ const Member = sequelize.define('Member', {
},
birthDate: {
type: DataTypes.STRING,
allowNull: false,
allowNull: true,
set(value) {
const encryptedValue = encryptData(value);
const encryptedValue = encryptData(value || '');
this.setDataValue('birthDate', encryptedValue);
},
get() {
@@ -122,6 +122,28 @@ 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
},
myTischtennisPlayerId: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Player ID from myTischtennis (e.g. NU2705037)',
field: 'my_tischtennis_player_id'
}
}, {
underscored: true,
sequelize,

View File

@@ -0,0 +1,133 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const MyTischtennis = sequelize.define('MyTischtennis', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
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'
},
autoUpdateRatings: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
field: 'auto_update_ratings'
},
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'
},
lastUpdateRatings: {
type: DataTypes.DATE,
allowNull: true,
field: 'last_update_ratings'
}
}, {
underscored: true,
tableName: 'my_tischtennis',
timestamps: true,
hooks: {
beforeSave: async (instance) => {
// Wenn savePassword false ist, password auf null setzen
if (!instance.savePassword) {
instance.encryptedPassword = null;
}
}
}
});
// Virtuelle Felder für password handling
MyTischtennis.prototype.setPassword = function(password) {
if (password && this.savePassword) {
this.encryptedPassword = encryptData(password);
} else {
this.encryptedPassword = null;
}
};
MyTischtennis.prototype.getPassword = function() {
if (this.encryptedPassword) {
try {
return decryptData(this.encryptedPassword);
} catch (error) {
console.error('Error decrypting myTischtennis password:', error);
return null;
}
}
return null;
};
export default MyTischtennis;

View File

@@ -0,0 +1,72 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import User from './User.js';
const MyTischtennisFetchLog = sequelize.define('MyTischtennisFetchLog', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: User,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
fetchType: {
type: DataTypes.ENUM('ratings', 'match_results', 'league_table'),
allowNull: false,
comment: 'Type of data fetch: ratings, match_results, or league_table'
},
success: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
message: {
type: DataTypes.TEXT,
allowNull: true,
},
errorDetails: {
type: DataTypes.TEXT,
allowNull: true,
},
recordsProcessed: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Number of records processed (e.g., players updated, matches fetched)'
},
executionTime: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Execution time in milliseconds'
},
isAutomatic: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Whether this was an automatic or manual fetch'
},
}, {
underscored: true,
tableName: 'my_tischtennis_fetch_log',
timestamps: true,
indexes: [
{
fields: ['user_id', 'fetch_type', 'created_at']
},
{
fields: ['created_at']
}
]
});
export default MyTischtennisFetchLog;

View File

@@ -0,0 +1,63 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const MyTischtennisUpdateHistory = sequelize.define('MyTischtennisUpdateHistory', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'user',
key: 'id'
},
onDelete: 'CASCADE'
},
success: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
message: {
type: DataTypes.TEXT,
allowNull: true
},
errorDetails: {
type: DataTypes.TEXT,
allowNull: true,
field: 'error_details'
},
updatedCount: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: 0,
field: 'updated_count'
},
executionTime: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Execution time in milliseconds',
field: 'execution_time'
}
}, {
underscored: true,
tableName: 'my_tischtennis_update_history',
timestamps: true,
indexes: [
{
fields: ['user_id']
},
{
fields: ['created_at']
},
{
fields: ['success']
}
]
});
export default MyTischtennisUpdateHistory;

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

@@ -1,6 +1,8 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import League from './League.js';
import Season from './Season.js';
const Team = sequelize.define('Team', {
id: {
@@ -23,6 +25,82 @@ const Team = sequelize.define('Team', {
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
leagueId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: League,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
seasonId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Season,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
// Tabellenfelder
matchesPlayed: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
matchesWon: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
matchesLost: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
matchesTied: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
setsWon: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
setsLost: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
pointsWon: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
pointsLost: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
tablePoints: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
tablePointsWon: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
tablePointsLost: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
}, {
underscored: true,
tableName: 'team',

View File

@@ -0,0 +1,52 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import ClubTeam from './ClubTeam.js';
const TeamDocument = sequelize.define('TeamDocument', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
fileName: {
type: DataTypes.STRING,
allowNull: false,
},
originalFileName: {
type: DataTypes.STRING,
allowNull: false,
},
filePath: {
type: DataTypes.STRING,
allowNull: false,
},
fileSize: {
type: DataTypes.INTEGER,
allowNull: false,
},
mimeType: {
type: DataTypes.STRING,
allowNull: false,
},
documentType: {
type: DataTypes.ENUM('code_list', 'pin_list'),
allowNull: false,
},
clubTeamId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: ClubTeam,
key: 'id',
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
},
}, {
underscored: true,
tableName: 'team_document',
timestamps: true,
});
export default TeamDocument;

View File

@@ -13,10 +13,14 @@ 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';
import Team from './Team.js';
import ClubTeam from './ClubTeam.js';
import TeamDocument from './TeamDocument.js';
import Season from './Season.js';
import Location from './Location.js';
import Group from './Group.js';
@@ -28,6 +32,22 @@ 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 MyTischtennis from './MyTischtennis.js';
import MyTischtennisUpdateHistory from './MyTischtennisUpdateHistory.js';
import MyTischtennisFetchLog from './MyTischtennisFetchLog.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 +96,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' });
@@ -94,8 +122,28 @@ Team.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Club.hasMany(League, { foreignKey: 'clubId', as: 'leagues' });
League.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Match.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
Season.hasMany(Match, { foreignKey: 'seasonId', as: 'matches' });
Season.hasMany(League, { foreignKey: 'seasonId', as: 'leagues' });
League.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
League.hasMany(Team, { foreignKey: 'leagueId', as: 'teams' });
Team.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(Team, { foreignKey: 'seasonId', as: 'teams' });
Team.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// ClubTeam relationships
Club.hasMany(ClubTeam, { foreignKey: 'clubId', as: 'clubTeams' });
ClubTeam.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
League.hasMany(ClubTeam, { foreignKey: 'leagueId', as: 'clubTeams' });
ClubTeam.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(ClubTeam, { foreignKey: 'seasonId', as: 'clubTeams' });
ClubTeam.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// TeamDocument relationships
ClubTeam.hasMany(TeamDocument, { foreignKey: 'clubTeamId', as: 'documents' });
TeamDocument.belongsTo(ClubTeam, { foreignKey: 'clubTeamId', as: 'clubTeam' });
Match.belongsTo(Location, { foreignKey: 'locationId', as: 'location' });
Location.hasMany(Match, { foreignKey: 'locationId', as: 'matches' });
@@ -181,6 +229,15 @@ Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
Accident.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' });
User.hasOne(MyTischtennis, { foreignKey: 'userId', as: 'myTischtennis' });
MyTischtennis.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(MyTischtennisUpdateHistory, { foreignKey: 'userId', as: 'updateHistory' });
MyTischtennisUpdateHistory.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(MyTischtennisFetchLog, { foreignKey: 'userId', as: 'fetchLogs' });
MyTischtennisFetchLog.belongsTo(User, { foreignKey: 'userId', as: 'user' });
export {
User,
Log,
@@ -198,10 +255,14 @@ export {
DiaryMemberNote,
DiaryMemberTag,
PredefinedActivity,
DiaryMemberActivity,
PredefinedActivityImage,
DiaryDateActivity,
Match,
League,
Team,
ClubTeam,
TeamDocument,
Group,
GroupActivity,
Tournament,
@@ -211,4 +272,10 @@ export {
TournamentResult,
Accident,
UserToken,
OfficialTournament,
OfficialCompetition,
OfficialCompetitionMember,
MyTischtennis,
MyTischtennisUpdateHistory,
MyTischtennisFetchLog,
};

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,21 @@
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="
},
"node_modules/node-cron": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz",
"integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==",
"license": "ISC",
"engines": {
"node": ">=6.0.0"
}
},
"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",
@@ -2322,9 +2851,10 @@
}
},
"node_modules/nodemailer": {
"version": "6.9.14",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz",
"integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==",
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz",
"integrity": "sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
@@ -2554,9 +3084,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 +3162,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 +3804,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

@@ -1,10 +1,9 @@
'use strict';
module.exports = {
upgrade: true,
reject: [
// API changes break existing tests
'proxy',
// API changes
'eslint'
'proxy'
]
};

View File

@@ -1,3 +1,5 @@
'use strict';
module.exports = {
printWidth: 160,
tabWidth: 4,

File diff suppressed because it is too large Load Diff

View File

@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities

View File

@@ -37,36 +37,36 @@ It's either a firewall issue, or your SMTP server blocks authentication attempts
#### I get TLS errors
- If you are running the code on your machine, check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using.
- Latest Node versions allow only TLS versions 1.2 and higher. Some servers might still use TLS 1.1 or lower. Check Node.js docs on how to get correct TLS support for your app. You can change this with [tls.minVersion](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tls_tls_createsecurecontext_options) option
- You might have the wrong value for the `secure` option. This should be set to `true` only for port 465. For every other port, it should be `false`. Setting it to `false` does not mean that Nodemailer would not use TLS. Nodemailer would still try to upgrade the connection to use TLS if the server supports it.
- Older Node versions do not fully support the certificate chain of the newest Let's Encrypt certificates. Either set [tls.rejectUnauthorized](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tlsconnectoptions-callback) to `false` to skip chain verification or upgrade your Node version
- If you are running the code on your machine, check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using.
- Latest Node versions allow only TLS versions 1.2 and higher. Some servers might still use TLS 1.1 or lower. Check Node.js docs on how to get correct TLS support for your app. You can change this with [tls.minVersion](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tls_tls_createsecurecontext_options) option
- You might have the wrong value for the `secure` option. This should be set to `true` only for port 465. For every other port, it should be `false`. Setting it to `false` does not mean that Nodemailer would not use TLS. Nodemailer would still try to upgrade the connection to use TLS if the server supports it.
- Older Node versions do not fully support the certificate chain of the newest Let's Encrypt certificates. Either set [tls.rejectUnauthorized](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tlsconnectoptions-callback) to `false` to skip chain verification or upgrade your Node version
```
```js
let configOptions = {
host: "smtp.example.com",
host: 'smtp.example.com',
port: 587,
tls: {
rejectUnauthorized: true,
minVersion: "TLSv1.2"
minVersion: 'TLSv1.2'
}
}
};
```
#### I have issues with DNS / hosts file
Node.js uses [c-ares](https://nodejs.org/en/docs/meta/topics/dependencies/#c-ares) to resolve domain names, not the DNS library provided by the system, so if you have some custom DNS routing set up, it might be ignored. Nodemailer runs [dns.resolve4()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnsresolve4hostname-options-callback) and [dns.resolve6()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnsresolve6hostname-options-callback) to resolve hostname into an IP address. If both calls fail, then Nodemailer will fall back to [dns.lookup()](https://nodejs.org/dist/latest-v16.x/docs/api/dns.html#dnslookuphostname-options-callback). If this does not work for you, you can hard code the IP address into the configuration like shown below. In that case, Nodemailer would not perform any DNS lookups.
```
```js
let configOptions = {
host: "1.2.3.4",
host: '1.2.3.4',
port: 465,
secure: true,
tls: {
// must provide server name, otherwise TLS certificate check will fail
servername: "example.com"
servername: 'example.com'
}
}
};
```
#### I have an issue with TypeScript types

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