123 Commits

Author SHA1 Message Date
438029a3a4 Merge pull request 'chore(deps): update dependency eslint to v9.39.2' (#3) from renovate/eslint-monorepo into main
Reviewed-on: #3
2025-12-19 16:15:43 +01:00
c58491c97a Merge pull request 'chore(deps): update dependency @vitejs/plugin-vue to v6.0.3' (#2) from renovate/vitejs-plugin-vue-6.x-lockfile into main
Reviewed-on: #2
2025-12-19 16:15:34 +01:00
1d9b9dbc45 chore(deps): update dependency eslint to v9.39.2 2025-12-19 16:13:52 +01:00
dc791dc33d chore(deps): update dependency @vitejs/plugin-vue to v6.0.3 2025-12-19 16:13:48 +01:00
57fbbff353 Merge pull request 'chore: Configure Renovate' (#1) from renovate/configure into main
Reviewed-on: #1
2025-12-19 16:08:23 +01:00
b00a35af30 Add renovate.json 2025-12-19 15:59:45 +01:00
Torsten Schulz (local)
dd0f29124c feat(tournament): add player details dialog and enhance player name interactions
- Implemented clickable player names in the TournamentPlacementsTab for improved user experience.
- Added a PlayerDetailsDialog component to display detailed player information when names are clicked.
- Updated localization files to include new strings for player details.
- Enhanced data handling for internal and external participants in player dialog logic.
2025-12-17 14:31:36 +01:00
Torsten Schulz (local)
dc084806ab feat(tournament): add group match creation and enhance match handling
- Implemented createGroupMatches function to generate matches for existing groups without altering group assignments.
- Updated resetMatches function to support optional class filtering when resetting group matches.
- Enhanced frontend components to filter and display group matches based on selected class, improving user experience.
- Adjusted tournament results display to reflect accurate match statistics, including wins and losses.
2025-12-17 13:38:40 +01:00
Torsten Schulz (local)
4b4c48a50f feat(tournament): refine external participant handling in match assignments
- Updated logic to replace null player1 and player2 with external participants, ensuring that the assigned objects do not include the member field.
- Created clean player objects for external participants, maintaining essential attributes while omitting sensitive information.
2025-12-17 08:52:38 +01:00
Torsten Schulz (local)
65acc9e0d5 feat(tournament): enhance validation of tournament members and external participants
- Implemented logic to load and validate TournamentMember and ExternalTournamentParticipant IDs for matches.
- Updated checks to set player1 and player2 to null if they belong to external participants or do not match valid tournament members.
2025-12-17 08:50:05 +01:00
Torsten Schulz (local)
13cd55c051 feat(tournament): validate tournament members and load external participants
- Added logic to check if loaded TournamentMembers belong to the current tournament, setting them to null if not.
- Updated filtering for external participant IDs to ensure only valid IDs are processed for matches with null players.
2025-12-17 08:45:52 +01:00
Torsten Schulz (local)
9bf37399d5 feat(tournament): improve result handling and display for matches and participants 2025-12-15 21:08:38 +01:00
Torsten Schulz (local)
047b1801b3 feat(tournament): enhance tournament configuration and results handling
- Updated TournamentConfigTab.vue to conditionally disable target type selection based on final stage type.
- Improved logic for determining target type and group count based on stage configuration.
- Refactored TournamentPlacementsTab.vue to streamline class and group placements display, including better handling of class visibility and player names.
- Enhanced TournamentResultsTab.vue to handle 'BYE' results and limit displayed entries to top three.
- Modified TournamentTab.vue to robustly determine match winners and losers, including handling 'BYE' scenarios and ensuring accurate knockout progression.
- Added logic to reset knockout matches with optional class filtering.
2025-12-15 15:36:18 +01:00
Torsten Schulz (local)
945ec0d48c feat(tournament): implement multi-stage tournament support with intermediate and final stages
- Added backend controller for tournament stages with endpoints to get, upsert, and advance stages.
- Created database migration for new tables: tournament_stage and tournament_stage_advancement.
- Updated models for TournamentStage and TournamentStageAdvancement.
- Enhanced frontend components to manage tournament stages, including configuration for intermediate and final rounds.
- Implemented logic for saving and advancing tournament stages, including handling of pool rules and third place matches.
- Added error handling and loading states in the frontend for better user experience.
2025-12-14 06:46:00 +01:00
Torsten Schulz (local)
e83bc250a8 Erlaube das Hinzufügen von Teilnehmern ohne Klasse und normalisiere die Anzahl der Gruppen auf mindestens 1 in der Turnierverwaltung 2025-12-13 12:25:17 +01:00
Torsten Schulz (local)
0c28b12978 Enhance HTTPS server setup and logging for Socket.IO
- Added detailed logging for SSL certificate loading and server status checks.
- Implemented error handling for port conflicts, providing guidance on resolving issues.
- Introduced a verification step to confirm server activation after startup.
- Improved fallback behavior for Socket.IO when SSL certificates are not found.
2025-12-01 08:43:31 +01:00
Torsten Schulz (local)
5aa11151cf Improve Socket.IO logging and connection handling
- Enhanced logging during WebSocket upgrade attempts by including specific headers for better debugging.
- Updated frontend socket service to allow forced upgrade attempts after successful polling connections, improving connection reliability.
2025-12-01 08:14:26 +01:00
Torsten Schulz (local)
a651113dee Enhance Socket.IO integration and improve error handling
- Updated CORS configuration for Socket.IO to allow all origins and added specific allowed headers.
- Improved error handling for Socket.IO connections, including detailed logging for connection errors and upgrade attempts.
- Implemented cleanup logic for socket connections during page reloads to prevent stale connections.
- Enhanced reconnection logic with unlimited attempts and improved logging for connection status.
- Updated frontend socket service to manage club room joining and leaving more effectively, with better state handling.
- Configured Vite for improved hot module replacement (HMR) settings to support local development.
2025-11-29 00:52:29 +01:00
Torsten Schulz (local)
bf0d5b0935 Refactor TournamentPlacementsTab to use groupedRankingList and improve final placements display
- Updated the final placements logic to utilize groupedRankingList for better performance and accuracy.
- Enhanced the display of class placements, including handling cases for entries without a class.
- Improved the no placements message condition to reflect the new data structure.
- Added a new computed method to retrieve player names from entry objects, enhancing clarity in the UI.
2025-11-29 00:28:47 +01:00
Torsten Schulz (local)
6acdcfa5c3 Add placements tab and localization support in TournamentTab
- Introduced a new tab for displaying tournament placements in the TournamentTab component.
- Added localization entries for placements in the German language JSON file, enhancing the user interface for German-speaking users.
- Updated the component structure to include the new TournamentPlacementsTab and ensure proper rendering based on the active tab.
2025-11-29 00:23:34 +01:00
Torsten Schulz (local)
dc2c60cefe Implement tournament pairing functionality and enhance participant management
- Introduced new endpoints for managing tournament pairings, including creating, updating, and deleting pairings.
- Updated the tournament service to handle pairing logic, ensuring validation for participants and preventing duplicate pairings.
- Enhanced participant management by adding class-based checks for gender and age restrictions when adding participants.
- Updated the tournament controller and routes to support the new pairing features and improved participant handling.
- Added localization support for new UI elements related to pairings in the frontend, enhancing user experience.
2025-11-29 00:15:01 +01:00
Torsten Schulz (local)
bdbbb88be9 Enhance MemberGalleryDialog to manage initial load state for gallery members. Introduce isInitialLoad flag to control gallery member loading behavior, ensuring size updates only occur on the first load. Reset isInitialLoad on dialog close for consistent state management. 2025-11-26 17:15:42 +01:00
Torsten Schulz (local)
e6146b8f5a Add participant assignment to groups functionality
Implement a new endpoint to assign participants to specific groups within tournaments. This includes the addition of the `assignParticipantToGroup` function in the tournament controller, which handles the assignment logic and emits relevant events. Update the tournament routes to include this new functionality. Enhance the tournament service to manage group assignments and ensure proper statistics are calculated for participants. Additionally, update the frontend to support adding participants, including external ones, and reflect changes in the UI for group assignments.
2025-11-23 17:09:41 +01:00
Torsten Schulz (local)
f7a799ea7f Implement login page proxy and CAPTCHA handling in MyTischtennisClient and Controller. Enhance login process with CAPTCHA token extraction and error handling. Update frontend to support iframe-based login and improve user experience with loading indicators. 2025-11-23 15:18:53 +01:00
Torsten Schulz (local)
b74cb30cf6 Enhance error handling in MyTischtennisClient and MyTischtennisService. Include status codes in error messages for better debugging and consistency across login failure responses. 2025-11-21 17:02:25 +01:00
Torsten Schulz (local)
0d2dfd9a07 Revert: Restore Vite 7.2.4 after Node.js upgrade to 20.19+ 2025-11-21 14:23:31 +01:00
Torsten Schulz (local)
61e5efadb8 Downgrade Vite to 6.0.0 for Node.js 20.17 compatibility on server 2025-11-21 14:20:12 +01:00
Torsten Schulz (local)
88d050392f Refactor localization handling in MemberTransferSettingsView to align with updated i18n methods. Update translation calls and placeholders for improved consistency and clarity in rendering across the component. 2025-11-21 14:17:20 +01:00
Torsten Schulz (local)
08b0be78ad Add server check script for i18n fixes 2025-11-21 14:15:21 +01:00
Torsten Schulz (local)
b0e610f3ab Fix: Replace all $t() calls with t() in PermissionsView and LogsView templates and add t to return statements 2025-11-21 14:12:42 +01:00
Torsten Schulz (local)
0285c05fa6 Fix: Replace all $t() calls with t() in TeamManagementView template to match Composition API setup 2025-11-21 14:10:01 +01:00
Torsten Schulz (local)
5d4f2ebd4b Update localization handling in TeamManagementView to use the new i18n method. Replace all instances of the translation function with the updated syntax for improved consistency and maintainability across the component. 2025-11-21 13:56:08 +01:00
Torsten Schulz (local)
bfa908ac9a Refactor localization handling in SeasonSelector and TeamManagementView to utilize the new i18n import method. Update German localization by removing escaped characters from placeholders for improved clarity and consistency in rendering. Ensure all translation calls are updated to use the new method for better maintainability. 2025-11-21 13:51:14 +01:00
Torsten Schulz (local)
9592459348 Enhance German localization for member transfer template by updating import template placeholder to include explicit quotes. Modify MemberTransferSettingsView to replace placeholders with the new format, ensuring accurate rendering in the UI. 2025-11-21 13:20:17 +01:00
Torsten Schulz (local)
47f53ee3fd Update German localization in member transfer templates to use explicit placeholders. Modify MemberTransferSettingsView to replace placeholder text with new format, ensuring accurate rendering in the UI. 2025-11-21 13:15:42 +01:00
Torsten Schulz (local)
c22f4016cc Refactor placeholders method in MemberTransferSettingsView to improve localization handling. Implement fallback German text when translation function is unavailable, ensuring consistent user experience across different scenarios. 2025-11-21 12:14:37 +01:00
Torsten Schulz (local)
2458ba2d37 Upgrade '@vitejs/plugin-vue' to version 6.0.2 and 'vite' to version 7.2.4 in package.json for improved compatibility. Add '@rolldown/pluginutils' as a new dependency in package-lock.json. 2025-11-21 12:02:07 +01:00
Torsten Schulz (local)
6eb42812fd Downgrade 'vite' package version to 6.0.0 in package.json to maintain compatibility with existing project dependencies. 2025-11-21 12:00:29 +01:00
Torsten Schulz (local)
938ce4d991 Improve error handling in MemberTransferSettingsView for translation availability, ensuring fallback text is provided when localization is unavailable. Update comments for clarity and maintain consistent localization handling across bulk wrapper, template, and import template placeholder texts. 2025-11-21 11:58:47 +01:00
Torsten Schulz (local)
cb6e84945b Enhance German localization for member transfer template placeholder by replacing escaped curly braces with placeholders. Update MemberTransferSettingsView to reflect these changes, ensuring proper rendering of the template in the UI. 2025-11-21 11:50:52 +01:00
Torsten Schulz (local)
8c6be234c6 Update package dependencies and enhance German localization for member transfer template placeholder
This commit updates the 'vite' package version to 7.2.4 and upgrades various esbuild dependencies to version 0.25.12. Additionally, it improves the German localization for the member transfer template placeholder in the MemberTransferSettingsView, ensuring proper rendering of escaped curly braces in the translation text.
2025-11-21 11:48:51 +01:00
Torsten Schulz (local)
fe160420c1 Update German localization for member transfer template placeholder and enhance MemberTransferSettingsView to improve error handling for translation availability 2025-11-21 11:47:02 +01:00
Torsten Schulz (local)
167e3ba3ec Refactor MemberTransferSettingsView to use a computed property for the import template placeholder text, improving localization handling and avoiding issues with vue-i18n placeholders. 2025-11-21 11:40:34 +01:00
Torsten Schulz (local)
9455b5d65a Update German localization for member transfer template to reflect new structure and adjust MemberTransferSettingsView for placeholder rendering 2025-11-21 11:38:13 +01:00
Torsten Schulz (local)
e6627a897e Refactor MemberTransferSettingsView to utilize computed properties for bulk wrapper and template descriptions, enhancing localization handling with placeholders. 2025-11-21 11:30:24 +01:00
Torsten Schulz (local)
71fc85427b Update German localization for bulk wrapper and template descriptions to use placeholders, and adjust MemberTransferSettingsView to reflect these changes. 2025-11-21 09:53:15 +01:00
Torsten Schulz (local)
76597a4360 Update German localization for bulk wrapper placeholder and refactor MemberTransferSettingsView to use a computed property for placeholder text 2025-11-21 09:35:56 +01:00
Torsten Schulz (local)
4f9761efb0 Update MyTischtennis model to use LONGTEXT for encrypted fields and enhance TeamManagementView with season change handling and async loading 2025-11-21 09:31:43 +01:00
Torsten Schulz (local)
51e47cf9f9 Refactor global error handling in server and improve logging in myTischtennisService
This commit moves the global error handling middleware in server.js to ensure it is applied after all routes. It also enhances the error handling in myTischtennisService by adding detailed logging for login attempts and failures, improving the visibility of issues during the login process. Additionally, the decryptData function in encrypt.js is updated to check if data is already decrypted, enhancing its robustness. Frontend changes include a minor adjustment to the button type in MyTischtennisAccount.vue for better accessibility.
2025-11-21 09:17:48 +01:00
Torsten Schulz (local)
0525f7908d Refactor localization handling in frontend components to use centralized i18n instance
This commit updates the localization implementation in App.vue, LogsView.vue, and PermissionsView.vue by replacing the use of `useI18n` with a direct import of the i18n instance. This change simplifies the translation function calls and ensures consistency across the application.
2025-11-21 09:03:53 +01:00
Torsten Schulz (local)
a4d89374b7 Remove unused localization keys from German translation files and update PermissionsView to include currentClub property 2025-11-21 09:00:46 +01:00
Torsten Schulz (local)
de907df092 Enhance API client configuration with timeout and error handling improvements
This commit updates the API client configuration to include a 60-second timeout for requests, allows for a maximum of 5 redirects, and modifies the status validation to handle all status codes. Additionally, it improves error handling by checking for network errors and preventing automatic logout on timeouts or network issues, enhancing the robustness of the API interactions.
2025-11-17 11:54:38 +01:00
Torsten Schulz (local)
b906ac64b3 Add updateGroupActivity method and corresponding route for editing group activities
This commit introduces the updateGroupActivity method in the diaryDateActivityController, allowing users to update existing group activities by linking them to predefined activities. The method includes error handling and emits a socket event upon successful updates. Additionally, the diaryDateActivityRoutes file is updated to include a new PUT route for updating group activities. Frontend changes in DiaryView enhance the user experience by enabling inline editing of group activities, including search functionality for predefined activities.
2025-11-17 10:12:21 +01:00
Torsten Schulz (local)
b7bbb92f86 Add initial fields to CourtDrawingDialog and enhance DiaryView for editing group activities
This commit introduces new props in CourtDrawingDialog for initialCode, initialName, and initialDescription, allowing for better handling of drawing data. Additionally, it updates the DiaryView to include an edit button for group activities, enabling users to modify existing activities and reset fields upon dialog closure. This enhances the user experience by providing more intuitive editing capabilities.
2025-11-16 22:25:34 +01:00
Torsten Schulz (local)
6896484e9e Enhance addGroupActivity method to support predefined activities in diary date activities
This commit updates the addGroupActivity method in the DiaryDateActivityService to accept a predefinedActivityId, allowing for the retrieval or creation of predefined activities based on the provided ID or activity name. Additionally, the frontend DiaryView is modified to include predefinedActivityId in the new plan item, improving the handling of group activities.
2025-11-16 22:12:11 +01:00
Torsten Schulz (local)
9cc9db3a5a Refine image validation logic in DiaryView to ensure accurate image handling
This commit enhances the image validation process in DiaryView by adding checks to confirm the presence of valid image IDs in the imageLink and verifying the existence of images in the associated array. This improvement ensures that only actual images are processed, leading to better data integrity and user experience.
2025-11-16 22:06:02 +01:00
Torsten Schulz (local)
1c99fb30a1 Enhance diary date activity service to include predefined activity images and improve image validation in DiaryView
This commit updates the DiaryDateActivityService to include associated images for predefined activities, enhancing the data structure. Additionally, it refines the image validation logic in DiaryView to check for both drawing data and standard images, ensuring a more robust handling of image data.
2025-11-16 21:57:27 +01:00
Torsten Schulz (local)
2782661206 Enhance member gallery generation to support JSON format without image creation
This commit updates the member gallery generation functionality to allow returning a list of members in JSON format without creating images. The `generateMemberGallery` method in the MemberService is modified to accept a new parameter, `createImage`, which determines whether images should be generated. This change improves the flexibility of the API for different use cases.
2025-11-16 21:26:15 +01:00
Torsten Schulz (local)
d10b663dc1 Refactor error handling and localization in frontend components
This commit enhances the error handling and user interface of various frontend components by integrating localization support. It updates error messages and titles across multiple views and dialogs to utilize the translation function, ensuring a consistent user experience in different languages. Additionally, it refines the handling of error messages in the MyTischtennis account and member transfer settings, improving clarity and user feedback during operations.
2025-11-16 20:48:31 +01:00
Torsten Schulz (local)
9baa6bae01 Update sitemap.xml to include lastmod dates and adjust priority values for SEO optimization 2025-11-16 12:18:05 +01:00
Torsten Schulz (local)
945fd85e39 Update Apache configuration example to clarify structure and enhance redirect handling
This commit revises the apache.conf.example file to provide clearer documentation on the configuration structure, emphasizing the separation of HTTP and HTTPS settings. It adds detailed comments regarding the use of separate configuration files for HTTP and HTTPS, and enhances the redirect rules for both www and non-www domains, ensuring proper traffic management and SEO practices.
2025-11-16 12:14:17 +01:00
Torsten Schulz (local)
5b04ed7904 Implement 301 redirects for www to non-www and enhance canonical tag handling
This commit adds 301 redirects in the Apache configuration to redirect traffic from www.tt-tagebuch.de to tt-tagebuch.de for both HTTP and HTTPS. Additionally, it introduces middleware in the backend to dynamically set canonical tags based on the request URL, ensuring proper SEO practices. The request logging middleware has been disabled, and sensitive data handling has been improved in the MyTischtennis model and API logging service, ensuring compliance with data protection regulations. Frontend updates include enhanced descriptions and features in the application, improving user experience and clarity.
2025-11-16 12:08:56 +01:00
Torsten Schulz (local)
de36a8ce2b Enhance fixCertPermissions.sh to set directory permissions for SSL certificate paths
This commit updates the fixCertPermissions.sh script to include specific permission settings for the archive and live directories of SSL certificates. It ensures that the group can navigate these directories by setting appropriate execute permissions, improving security and access management for SSL certificate handling.
2025-11-16 11:26:57 +01:00
Torsten Schulz (local)
903b036a63 Enhance fixCertPermissions.sh to improve SSL certificate permission handling
This commit updates the fixCertPermissions.sh script to include more specific permission settings for SSL certificate files by using the `-type f` option with the `find` command. It also adds a new feature to display the found private key files, improving visibility and debugging capabilities during the permission setting process.
2025-11-16 11:24:23 +01:00
Torsten Schulz (local)
5f3b6200ec Refactor fixCertPermissions.sh to improve permission handling for SSL certificates
This commit updates the fixCertPermissions.sh script to utilize the `find` command for setting permissions on SSL certificate files, ensuring that symlinks are properly handled. It also enhances the check for the archive directory's existence by using `sudo`, and reorganizes the output messages for clarity, emphasizing the need to restart the service after changes are made.
2025-11-16 11:19:15 +01:00
Torsten Schulz (local)
eff211856f Refactor fixCertPermissions.sh to improve SSL certificate handling and user configuration
This commit refines the fixCertPermissions.sh script to enhance its functionality for managing SSL certificate permissions. It introduces checks for the existence of the service user and defaults to `www-data` if not defined, ensuring proper access to SSL certificates. Additionally, the script is updated to handle scenarios where the service user is set to `nobody`, improving overall security and usability in the deployment process.
2025-11-16 11:17:30 +01:00
Torsten Schulz (local)
a81c3453b5 Update Socket.IO deployment documentation and fixCertPermissions.sh script for improved service user configuration
This commit enhances the Socket.IO deployment documentation by adding a new section on configuring the systemd service to run as `www-data`, ensuring proper permissions for SSL certificate access. It also updates the fixCertPermissions.sh script to handle cases where the service user is not defined or is set to `nobody`, defaulting to `www-data` and verifying its existence. These changes improve the overall security and functionality of the deployment process.
2025-11-16 09:50:26 +01:00
Torsten Schulz (local)
56c708d3a0 Update fixCertPermissions.sh to set executable permissions
This commit changes the permissions of the fixCertPermissions.sh script to make it executable. This adjustment ensures that the script can be run directly, facilitating its use in managing SSL certificate permissions.
2025-11-16 09:44:23 +01:00
Torsten Schulz (local)
062bddcf52 Update fixCertPermissions.sh to use sudo for certificate checks and listings
This commit modifies the fixCertPermissions.sh script to ensure that checks for the SSL certificate directory and the ssl-cert group are performed with sudo, allowing proper access for non-privileged users. Additionally, the script now lists the permissions of the private key and full chain certificate using sudo, enhancing its functionality for managing SSL certificate permissions.
2025-11-16 09:43:51 +01:00
Torsten Schulz (local)
4f98c782f3 Update Socket.IO deployment documentation to reflect new testing and SSL certificate error handling
This commit revises the deployment documentation for Socket.IO, updating the section numbers for clarity and adding detailed instructions for resolving SSL certificate permission errors. A new troubleshooting script is introduced to manage certificate access, ensuring the HTTPS server operates correctly on port 3051. These enhancements improve the overall guidance for deploying and testing the Socket.IO service.
2025-11-16 09:41:48 +01:00
Torsten Schulz (local)
3ea2907d08 Update Socket.IO deployment documentation to include SSL certificate permissions setup
This commit revises the deployment documentation for the Socket.IO backend, adding a new section on setting SSL certificate permissions. It introduces a script to manage certificate access for the Node.js process, ensuring proper functionality of the HTTPS server on port 3051. The order of sections has also been adjusted for clarity, enhancing the overall deployment guidance.
2025-11-16 09:41:33 +01:00
Torsten Schulz (local)
ba5d6b14a8 Enhance Socket.IO backend server configuration for improved HTTPS support
This commit updates the backend server configuration to ensure it properly handles HTTPS connections on port 3051. It includes adjustments to error handling for SSL certificate loading and improves the server's accessibility by listening on all interfaces (0.0.0.0). These changes aim to streamline the deployment process and enhance the overall reliability of the Socket.IO service over HTTPS.
2025-11-16 09:37:03 +01:00
Torsten Schulz (local)
004a94404a Update Socket.IO deployment documentation and backend server configuration
This commit revises the deployment documentation for Socket.IO, emphasizing the need to restart the backend server for HTTPS support on port 3051. It introduces a new diagnostic script to check SSL certificate existence, server accessibility, and port status. Additionally, the backend server configuration is updated to ensure it listens on all interfaces (0.0.0.0), enhancing accessibility. These changes improve clarity and troubleshooting guidance for deploying Socket.IO over HTTPS.
2025-11-16 09:35:57 +01:00
Torsten Schulz (local)
5ddf998672 Update Apache and backend configuration for direct Socket.IO HTTPS support
This commit modifies the Apache configuration to reflect that Socket.IO now runs directly on HTTPS port 3051, eliminating the need for Apache proxying. Additionally, the backend server setup is updated to create an HTTPS server for Socket.IO, including error handling for SSL certificate loading. The frontend service is also adjusted to connect to the new HTTPS endpoint, ensuring compatibility with the updated architecture.
2025-11-16 09:31:16 +01:00
Torsten Schulz (local)
baf5bda6f2 Update Apache configuration to improve WebSocket upgrade handling and enhance logging
This commit modifies the Apache configuration to refine the handling of WebSocket upgrades, ensuring better compatibility and reliability. Additionally, it enhances logging for the WebSocket upgrade process, providing more detailed insights into requests and responses, which aids in diagnostics and troubleshooting.
2025-11-16 00:13:30 +01:00
Torsten Schulz (local)
572de5f7d4 Update Apache configuration to include alternative WebSocket handling for Apache 2.4.47
This commit adds comments to the Apache configuration file, highlighting that mod_proxy_http can directly handle WebSockets starting from Apache 2.4.47. It provides an alternative ProxyPass configuration for WebSocket connections, ensuring better compatibility and guidance for users facing issues with the existing RewriteRule setup.
2025-11-16 00:12:44 +01:00
Torsten Schulz (local)
37893474b1 Refine Apache configuration for WebSocket upgrade handling
This commit updates the Apache configuration to improve the handling of WebSocket upgrades by clarifying the RewriteCond conditions for the Upgrade and Connection headers. The changes enhance the readability of the configuration and ensure more reliable WebSocket connections, aligning with previous enhancements for real-time communication.
2025-11-16 00:08:49 +01:00
Torsten Schulz (local)
f437747664 Enhance socketService configuration for WebSocket upgrades
This commit updates the `socketService.js` file to improve WebSocket upgrade handling by adding two new configuration options: `allowEIO3`, which permits WebSocket upgrades without an Origin header, and `serveClient`, which disables serving the client bundle. These changes enhance compatibility with reverse proxies and optimize the WebSocket connection process.
2025-11-16 00:05:45 +01:00
Torsten Schulz (local)
22e9750e5d Enhance WebSocket upgrade logging for improved diagnostics
This commit updates the `testWebSocket.js` script to include additional logging for the WebSocket upgrade process. It now logs the request path, Sec-WebSocket-Key, response status, and response headers, providing better visibility into the upgrade process. Additionally, error handling has been improved by logging the error code and adding a message for potential server connection issues during timeouts.
2025-11-16 00:04:43 +01:00
Torsten Schulz (local)
bd95f77131 Refactor WebSocket upgrade key generation for improved security
This commit updates the `testWebSocket.js` and `testWebSocketApache.js` scripts to enhance the generation of the Sec-WebSocket-Key. The key is now generated using a secure method that allocates 16 bytes of random data, ensuring compliance with WebSocket protocol requirements. This change improves the robustness of WebSocket upgrade requests in both scripts.
2025-11-16 00:02:55 +01:00
Torsten Schulz (local)
bbdc923950 Enhance WebSocket testing script to include session ID handling
This commit updates the `testWebSocket.js` script to extract and utilize the session ID from the HTTP polling response. The WebSocket upgrade request is modified to include the session ID when available, improving the accuracy of the upgrade tests. Additionally, detailed logging for response bodies is added to aid in diagnosing potential issues during the WebSocket upgrade process.
2025-11-16 00:00:42 +01:00
Torsten Schulz (local)
3e5ddd8a05 Refine Apache configuration for WebSocket upgrade handling and clarify conditions
This commit updates the Apache configuration to enhance the handling of WebSocket upgrades by specifying exact conditions for the Upgrade and Connection headers. It also clarifies the fallback mechanism for HTTP polling, ensuring better compatibility and reliability in real-time communication scenarios.
2025-11-15 23:59:00 +01:00
Torsten Schulz (local)
f4e5cf2edb Enhance Apache configuration for WebSocket handling by preserving query strings
This commit updates the Apache configuration to ensure that the query string is retained during WebSocket connections. The RewriteRule for the WebSocket upgrade has been modified to include the QSA flag, improving compatibility for applications relying on query parameters. These changes further optimize the server's handling of real-time communication.
2025-11-15 23:54:12 +01:00
Torsten Schulz (local)
44dba70aac Refactor Apache configuration for enhanced WebSocket support and security improvements
This commit further refines the Apache configuration to optimize WebSocket handling by adjusting the <LocationMatch> directive and ensuring proper upgrade handling. It also updates SSL settings and certificate paths to bolster security, while improving the organization of DocumentRoot and logging paths. These changes enhance the server's capability to manage real-time communication effectively and securely.
2025-11-15 23:45:56 +01:00
Torsten Schulz (local)
7698d87ba0 Update frontend dependencies to align with compatibility requirements
This commit downgrades the `@vitejs/plugin-vue` dependency from version 6.0.0 to 5.2.1 and the `vite` dependency from version 7.2.2 to 5.4.21 in the frontend package.json. These changes ensure compatibility with the current project setup and address potential issues arising from newer versions.
2025-11-15 23:42:50 +01:00
Torsten Schulz (local)
201d5e9214 Update eslint-plugin-vue dependency to version 9.0.0 for improved linting capabilities 2025-11-15 23:41:47 +01:00
Torsten Schulz (local)
c21544d9b6 Refine Apache configuration for WebSocket handling and improve request processing
This commit updates the Apache configuration to enhance WebSocket support by clarifying the <LocationMatch> directives. It ensures proper handling of WebSocket upgrade requests and introduces a fallback mechanism for HTTP polling. Additionally, it encapsulates header settings for better compatibility and emphasizes the order of LocationMatch blocks. These changes improve the server's capability to manage real-time communication effectively.
2025-11-15 23:40:43 +01:00
Torsten Schulz (local)
6167116630 Update frontend dependencies to improve compatibility and performance
This commit upgrades the `@vitejs/plugin-vue` dependency from version 5.2.1 to 6.0.0 in the frontend package.json. This change ensures better compatibility with the latest features and improvements in the Vue ecosystem, contributing to a more efficient development environment.
2025-11-15 23:36:26 +01:00
Torsten Schulz (local)
1bb5f61b57 Update Apache configuration and frontend dependencies for improved performance and compatibility
This commit adds a ProxyTimeout setting for all proxy connections in the Apache configuration, enhancing the server's ability to manage long-lived connections. Additionally, it updates the frontend dependencies, specifically upgrading `eslint` to version 9.39.1 and `vite` to version 7.2.2, ensuring better compatibility and performance in the development environment. These changes contribute to a more robust and efficient application.
2025-11-15 23:35:15 +01:00
Torsten Schulz (local)
1535c8795b Refactor WebSocket testing script to utilize HTTP polling and WebSocket upgrade checks
This commit updates the `testWebSocket.js` script to replace the Socket.IO client connection with direct HTTP polling for the initial handshake. It introduces structured tests for both HTTP polling and WebSocket upgrades, providing detailed logging for success and error scenarios. The changes enhance the script's ability to verify server connectivity and support for WebSocket protocols, improving the overall testing process for real-time communication.
2025-11-15 23:27:42 +01:00
Torsten Schulz (local)
cb2d7d3936 Update Apache configuration to optimize WebSocket handling and enhance security measures
This commit refines the Apache configuration for improved WebSocket support by adjusting the <LocationMatch> directive and ensuring proper upgrade handling. It also updates SSL settings and certificate paths for better security, while enhancing the organization of DocumentRoot and logging paths. These changes bolster the server's capability to manage real-time communication effectively and securely.
2025-11-15 23:24:55 +01:00
Torsten Schulz (local)
5b4a5ba501 Refine Apache configuration for WebSocket handling and improve fallback mechanisms
This commit updates the Apache configuration to enhance WebSocket support by clarifying the <LocationMatch> directive and ensuring proper handling of WebSocket upgrades. It introduces a fallback mechanism for HTTP polling and emphasizes the need for mod_rewrite and mod_proxy_wstunnel. These changes improve the server's capability to manage real-time communication effectively.
2025-11-15 23:21:59 +01:00
Torsten Schulz (local)
90b5f8d63d Refine Apache configuration for WebSocket handling and improve documentation
This commit updates the Apache configuration to enhance WebSocket support by refining the <LocationMatch> directive. It clarifies the need for multiple LocationMatch blocks and ensures proper handling of WebSocket upgrades and fallbacks to HTTP. Additionally, it encapsulates header settings within a conditional block for better compatibility. These changes improve the server's ability to manage real-time communication effectively.
2025-11-15 23:19:27 +01:00
Torsten Schulz (local)
1ff3d9d1a6 Update Apache configuration and socket service for WebSocket support
This commit adds a ProxyTimeout setting for WebSocket connections in the Apache configuration, enhancing the server's ability to manage long-lived connections. Additionally, the socket service documentation is updated to clarify compatibility with both Apache and Nginx as reverse proxies. These changes improve the handling of real-time communication in the application.
2025-11-15 23:10:32 +01:00
Torsten Schulz (local)
df6fb23132 Update Apache configuration for tt-tagebuch.de to support WebSocket and SSL
This commit modifies the Apache configuration for tt-tagebuch.de by adding WebSocket support and updating SSL settings. The configuration now includes a dedicated <LocationMatch> for WebSocket connections, ensuring proper handling of upgrade requests. Additionally, the SSL certificate paths have been updated to reflect the use of Let's Encrypt. The DocumentRoot and logging paths have also been adjusted for better organization and clarity. These changes enhance the server's capability to handle real-time communication and improve security.
2025-11-15 23:06:04 +01:00
Torsten Schulz (local)
1e86b821e8 Enhance socket service configuration for improved connection handling
This commit updates the socket service in both the backend and frontend to include explicit path and transport settings for Socket.IO. The backend configuration now allows for upgrades from polling to WebSocket, with defined timeouts for upgrades and pings. The frontend configuration adjusts the transport order and adds a timeout for reconnections, ensuring a more robust and efficient socket connection experience. These changes improve the reliability and performance of real-time communication in the application.
2025-11-15 23:05:27 +01:00
Torsten Schulz (local)
5923ef8bba Add training day PDF generation and summary functionality
This commit introduces a new method, `addTrainingDaySummary`, in the `PDFGenerator` class to create detailed summaries for training days, including member activities and their respective times. Additionally, the `DiaryView` component is updated with a new button to generate training day PDFs, enhancing the user experience by allowing easy access to training summaries. Debugging outputs are included for better tracking of data during PDF generation.
2025-11-15 22:58:47 +01:00
Torsten Schulz (local)
cd8f40aa9d Enhance member activity retrieval by tracking unique activity dates
This commit updates the `getMemberActivities` function to track unique date-activity combinations using a Set, ensuring accurate counting of distinct participation dates. It also refines the logic for handling dates, skipping entries without a date and sorting the dates in descending order. These changes improve the accuracy and clarity of member activity statistics presented to the user.
2025-11-15 22:41:45 +01:00
Torsten Schulz (local)
d392ccddd5 Update navigation titles and enhance tournament group validation logic
This commit modifies the navigation titles in `App.vue`, changing "Verwaltung" to "Tagesgeschäft" and "Organisation" to "Wettbewerbe", while also introducing a new section for "Einstellungen". Additionally, it refines the group count validation logic in `TournamentTab.vue` by correcting the method calls for `getTotalNumberOfGroups`, ensuring proper functionality for starting and resetting knockout rounds based on the number of groups present. These changes improve the user interface and enhance the tournament management experience.
2025-11-15 22:35:02 +01:00
Torsten Schulz (local)
4a83e5c159 Enhance knockout round functionality by adding group count validation
This commit updates the `TournamentTab.vue` component to ensure that knockout rounds can only be started or reset if there is more than one group present in the tournament. A new method, `getTotalNumberOfGroups`, is introduced to calculate the total number of groups, improving the logic for determining the availability of knockout rounds. These changes enhance the tournament management experience by preventing invalid operations in single-group scenarios.
2025-11-15 22:26:17 +01:00
Torsten Schulz (local)
911c07e522 Refactor tournament management by removing external and official tournament views
This commit simplifies the tournament management system by removing the `ExternalTournamentsView` and `OfficialTournaments` components, consolidating tournament functionality into the `TournamentsView`. The routing has been updated accordingly, and the UI has been adjusted to reflect these changes. Additionally, the `App.vue` navigation has been streamlined to enhance user experience by focusing on internal tournaments only.
2025-11-15 22:20:59 +01:00
Torsten Schulz (local)
cd89c68a69 Add participation overview to OfficialTournaments.vue 2025-11-15 21:31:09 +01:00
Torsten Schulz (local)
f1321b18bb Enhance official tournament listing and upload functionality
This commit updates the `listOfficialTournaments` function to ensure it returns an empty array if no tournaments are found, improving data handling. Additionally, the frontend `OfficialTournaments.vue` is enhanced with a file upload feature for PDF documents, along with improved error handling in the tournament list loading process. These changes enhance user experience by providing clearer feedback and functionality for managing official tournaments.
2025-11-15 21:25:03 +01:00
Torsten Schulz (local)
54ce09e9a9 Add training times management and enhance diary view with group selection dialog
This commit introduces the `TrainingTime` model and related functionality, allowing for the management of training times associated with training groups. The backend is updated to include new routes for training times, while the frontend is enhanced with a new dialog in the `DiaryView` for selecting training groups and suggesting available training times. This improves user experience by streamlining the process of scheduling training sessions and managing associated data.
2025-11-15 20:51:08 +01:00
Torsten Schulz (local)
7a9e856961 Update training group management and enhance UI components
This commit introduces the `TrainingGroup` model and related functionality, allowing for the management of training groups within the application. The `ClubService` is updated to automatically create preset groups upon club creation. The frontend is enhanced with new views and components, including `TrainingGroupsView` and `TrainingGroupsTab`, to facilitate the display and management of training groups. Additionally, the `MembersView` is updated to allow adding and removing members from training groups, improving the overall user experience and interactivity in managing club members and their associated training groups.
2025-11-15 20:38:53 +01:00
Torsten Schulz (local)
fd4b47327f Enhance diary date activity service and diary view with improved drawing data handling
This commit updates the `DiaryDateActivityService` to retrieve all images associated with predefined activities and parse their drawing data, ensuring robust error handling. The `DiaryView` component is also enhanced to support both string and object representations of drawing data, improving the logic for determining activity visuals. These changes streamline the handling of drawing data across the application, enhancing user experience and data integrity.
2025-11-14 23:48:30 +01:00
Torsten Schulz (local)
3a26f10110 Update member activity handling to support activity codes and improve display logic
This commit enhances the `getMemberActivities` and `getMemberLastParticipations` functions to utilize activity codes when available, improving the uniqueness of activity identification. The `MemberActivityStatsDialog` component is updated to handle both string and object representations of activities, ensuring a consistent display and tooltip functionality. These changes streamline the activity data structure and enhance the user experience when viewing participation statistics.
2025-11-14 23:38:36 +01:00
Torsten Schulz (local)
ce2bda37ac Refactor member activity display to group participations by date
This commit updates the `MemberActivityStatsDialog` component to group member participations by date, enhancing the presentation of activity data. The logic is introduced to aggregate activities under their respective dates, ensuring a clearer and more organized display. Additionally, CSS styles are added to improve the visual hierarchy and user experience when viewing recent participations.
2025-11-14 23:27:46 +01:00
Torsten Schulz (local)
5dda346fd7 Refactor member activity retrieval to support optional predefined activities for group activities
This commit updates the `getMemberActivities` and `getMemberLastParticipations` functions to allow for optional inclusion of predefined activities in group activities. The logic is enhanced to check for predefined activities associated with group activities, providing a fallback mechanism to ensure accurate activity representation. This change improves the clarity and efficiency of the activity retrieval process, ensuring that the correct data is returned to the frontend.
2025-11-14 23:24:51 +01:00
Torsten Schulz (local)
28c92b66af Refactor member activity retrieval to include group activities and eliminate duplicates
This commit enhances the `getMemberActivities` and `getMemberLastParticipations` functions by introducing logic to gather group activities for members based on their group associations. It ensures that both explicitly assigned member activities and group activities are combined while filtering out duplicates. The overall structure of the activity retrieval process is improved for better clarity and efficiency, enhancing the accuracy of the data returned to the frontend.
2025-11-14 23:07:02 +01:00
Torsten Schulz (local)
d08835e206 Implement external participant management and tournament class features
This commit enhances the tournament management system by introducing functionality for handling external participants and tournament classes. New methods are added to the `tournamentController` and `tournamentService` for adding, retrieving, updating, and removing external participants, as well as managing tournament classes. The backend models are updated to support these features, including new relationships and attributes. The frontend is also updated to allow users to manage external participants and classes, improving the overall user experience and interactivity in tournament management.
2025-11-14 22:36:51 +01:00
Torsten Schulz (local)
3334d76688 Enhance tournament management with new features and UI improvements
This commit introduces several enhancements to the tournament management system, including the addition of winning sets to tournament creation and updates. The `updateTournament` and `addTournament` methods in the backend now accept winning sets as a parameter, ensuring proper validation and handling. New functionality for updating participant seeded status and setting match activity is also implemented, along with corresponding routes and controller methods. The frontend is updated to reflect these changes, featuring new input fields for winning sets and improved participant management UI, enhancing overall user experience and interactivity.
2025-11-14 14:36:21 +01:00
Torsten Schulz (local)
d48cc4385f Add tournament update functionality and enhance UI for tournament management
This commit introduces the ability to update tournament details, including name and date, in the backend and frontend. The new `updateTournament` method is added to the `tournamentController` and `tournamentService`, allowing for validation and error handling. The frontend `TournamentsView` is updated to include input fields for editing tournament details, with real-time updates reflected in the UI. Additionally, new CSS styles are introduced for improved layout and user interaction, enhancing the overall experience in tournament management.
2025-11-14 10:44:18 +01:00
Torsten Schulz (local)
9b8dcd8561 Add group deletion functionality and socket event emissions for real-time updates
This commit introduces the ability to delete groups in the groupController, along with the necessary backend service updates. It also adds socket event emissions for group and activity changes, ensuring real-time updates are sent to clients when groups are deleted. The frontend is updated to include a delete button in the DiaryView, allowing users to remove groups easily. Additionally, the groupRoutes and socketService are modified to support these new features, enhancing the overall interactivity of the application.
2025-11-13 18:48:51 +01:00
Torsten Schulz (local)
2b06a8dd10 Enhance participant update handling and UI responsiveness in DiaryView
This commit improves the participant update process by ensuring the latest participant data is fetched from the database before emitting socket events. It also refines the DiaryView component's UI, adding better handling for dropdowns and member group selections, enhancing user experience. Additionally, new CSS styles are introduced for member group select elements to ensure consistent appearance across browsers.
2025-11-13 18:18:31 +01:00
Torsten Schulz (local)
58e773e51e Enhance DiaryView with mobile and desktop tab navigation improvements
This commit refines the DiaryView component by implementing a responsive tab navigation system for both mobile and desktop views. It introduces new CSS styles for better layout management and user interaction, ensuring a seamless experience when switching between 'Teilnehmer' and 'Aktivitäten' sections. The active tab state is now visually indicated, improving usability across devices.
2025-11-13 17:39:38 +01:00
Torsten Schulz (local)
8d17cad299 Add mobile tab navigation to DiaryView for improved user experience
This commit introduces a mobile-friendly tab navigation system in the DiaryView component, allowing users to switch between 'Trainingsplan', 'Teilnehmer', and 'Aktivitäten' seamlessly. The active tab state is managed with a new reactive property, enhancing the overall usability of the application on mobile devices.
2025-11-13 17:34:10 +01:00
Torsten Schulz (local)
156f4d6921 Add member change event handling for real-time updates
This commit introduces a new socket event for member changes, allowing real-time updates when members are created or updated. The backend now emits a 'member:changed' event upon successful member modifications, while the frontend listens for this event to refresh the member list in the DiaryView component. This enhances the interactivity and responsiveness of the application, ensuring users receive immediate feedback on member changes.
2025-11-13 17:32:29 +01:00
Torsten Schulz (local)
e27a4d960d Update dependencies in package.json and package-lock.json for improved functionality
This commit updates the versions of several dependencies in the frontend, including upgrading `jspdf` to version 3.0.3, `vite` to version 7.2.2, and `@vitejs/plugin-vue` to version 5.2.1. Additionally, it updates `axios` to version 1.13.2 and `dompurify` to version 3.3.0, among others. These changes enhance the overall performance and security of the application by incorporating the latest features and fixes from the respective libraries.
2025-11-13 17:00:28 +01:00
Torsten Schulz (local)
c589c11607 Add Socket.IO integration for real-time updates in diary features
This commit introduces Socket.IO to the backend and frontend, enabling real-time communication for diary-related events. Key updates include the addition of socket event emissions for diary date updates, tag additions/removals, and activity member changes in the backend controllers. The frontend DiaryView component has been enhanced to connect to the socket server and handle incoming events, ensuring that users receive immediate feedback on changes. Additionally, new dependencies for Socket.IO have been added to both the backend and frontend package files, improving the overall interactivity and responsiveness of the application.
2025-11-13 16:54:31 +01:00
Torsten Schulz (local)
0caa31e3eb Refactor file input sections in ImageViewerDialog and MembersView to enhance user experience. The updates include separate file input options for selecting files and capturing images via the device camera, improving accessibility and usability for image uploads. 2025-11-13 15:55:33 +01:00
Torsten Schulz (local)
fff5d404f5 Refactor file input fields in ImageViewerDialog and MembersView to remove camera capture attribute. This change simplifies the file selection process for users while maintaining functionality for image uploads. 2025-11-13 15:52:12 +01:00
Torsten Schulz (local)
7aff827711 Update file input fields to capture images using the device camera and set default match start date from scheduleDate if not already set. This enhances the user experience for image uploads and ensures match data is initialized correctly. 2025-11-13 15:47:58 +01:00
214 changed files with 31060 additions and 5081 deletions

View File

3
.gitignore vendored
View File

@@ -7,4 +7,5 @@ backend/.env
backend/images/*
backend/backend-debug.log
backend/*.log
backend/*.log
backend/.env.local

86
CHECK_SERVER.md Normal file
View File

@@ -0,0 +1,86 @@
# Server-Prüfung: i18n-Fixes
## Lokale Prüfung (bereits durchgeführt)
✅ Alle Dateien sind lokal korrekt:
- `TeamManagementView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
- `PermissionsView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
- `LogsView.vue` - Alle `$t()` durch `t()` ersetzt, `t` im return Statement
- `SeasonSelector.vue` - Bereits korrekt
## Server-Prüfung
### 1. Prüfskript auf den Server kopieren
```bash
# Vom lokalen Rechner aus:
scp check-i18n-fixes.sh rv2756:/var/www/tt-tagebuch.de/
```
### 2. Auf dem Server ausführen
```bash
# Auf dem Server:
cd /var/www/tt-tagebuch.de
chmod +x check-i18n-fixes.sh
./check-i18n-fixes.sh
```
### 3. Falls Dateien nicht aktualisiert sind
```bash
# Auf dem Server:
cd /var/www/tt-tagebuch.de
git pull origin main
cd backend
npm install # Erstellt automatisch den Frontend-Build (via postinstall script)
```
### 4. Backend neu starten (falls nötig)
```bash
# Falls als systemd-Service:
sudo systemctl restart tt-tagebuch
# Oder falls als PM2-Prozess:
pm2 restart tt-tagebuch-backend
```
## Erwartete Ergebnisse
Das Prüfskript sollte folgende Ausgabe zeigen:
```
1. TeamManagementView.vue:
✓ Enthält 'const t = (key, params) => i18n.global.t'
✓ Enthält keine $t() Aufrufe mehr
✓ 't' ist im return Statement enthalten
2. PermissionsView.vue:
✓ Enthält 'const t = (key, params) => i18n.global.t'
✓ Enthält keine $t() Aufrufe mehr
✓ 't' ist im return Statement enthalten
3. LogsView.vue:
✓ Enthält 'const t = (key, params) => i18n.global.t'
✓ Enthält keine $t() Aufrufe mehr
✓ 't' ist im return Statement enthalten
4. SeasonSelector.vue:
✓ Enthält 'const t = (key, params) => i18n.global.t'
✓ Enthält keine $t() Aufrufe mehr
```
## Commits, die auf den Server müssen
Die folgenden Commits müssen auf dem Server sein:
- `b0e610f` - Fix: Replace all $t() calls with t() in PermissionsView and LogsView templates
- `0285c05` - Fix: Replace all $t() calls with t() in TeamManagementView template
- `5d4f2eb` - Update localization handling in TeamManagementView
Prüfe mit:
```bash
git log --oneline -5
```

191
DEPLOYMENT_SOCKET_IO.md Normal file
View File

@@ -0,0 +1,191 @@
# Deployment-Anleitung: Socket.IO mit SSL
Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy).
## Schritte nach dem Deployment
### 1. Firewall-Port öffnen
```bash
# UFW (Ubuntu Firewall)
sudo ufw allow 3051/tcp
```
### 2. Apache-Konfiguration aktualisieren
```bash
sudo cp /var/www/tt-tagebuch.de/apache.conf.example /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
sudo systemctl restart apache2
```
### 3. systemd-Service konfigurieren (als www-data)
**WICHTIG:** Der Service sollte als `www-data` laufen, nicht als `nobody`!
```bash
# Service-Datei installieren
sudo cp /var/www/tt-tagebuch.de/tt-tagebuch.service /etc/systemd/system/
sudo systemctl daemon-reload
```
Die Service-Datei konfiguriert:
- User: `www-data` (Standard-Webserver-Benutzer)
- Group: `www-data`
- Port: 3050 (HTTP) und 3051 (HTTPS)
### 4. SSL-Zertifikat-Berechtigungen setzen
**WICHTIG:** Der Node.js-Prozess muss Zugriff auf die SSL-Zertifikate haben!
```bash
cd /var/www/tt-tagebuch.de/backend
chmod +x scripts/fixCertPermissions.sh
sudo ./scripts/fixCertPermissions.sh
```
Dieses Skript:
- Erstellt die Gruppe `ssl-cert` (falls nicht vorhanden)
- Fügt den Service-Benutzer (`www-data`) zur Gruppe hinzu
- Setzt die Berechtigungen für die Zertifikate
### 5. Backend neu starten
**WICHTIG:** Der Backend-Server muss neu gestartet werden, damit der HTTPS-Server auf Port 3051 läuft!
```bash
# Falls als systemd-Service:
sudo systemctl restart tt-tagebuch
# Oder falls als PM2-Prozess:
pm2 restart tt-tagebuch-backend
```
### 6. Prüfen, ob HTTPS-Server läuft
```bash
# Prüfe, ob Port 3051 geöffnet ist
sudo netstat -tlnp | grep 3051
# Oder:
sudo ss -tlnp | grep 3051
# Prüfe Backend-Logs
sudo journalctl -u tt-tagebuch -f
# Oder bei PM2:
pm2 logs tt-tagebuch-backend
```
Du solltest folgende Meldung sehen:
```
🚀 HTTPS-Server für Socket.IO läuft auf Port 3051
```
### 7. Diagnose-Skript ausführen
```bash
cd /var/www/tt-tagebuch.de/backend
node scripts/checkSocketIOServer.js
```
Dieses Skript prüft:
- Ob SSL-Zertifikate existieren
- Ob Port 3051 geöffnet ist
- Ob der Server erreichbar ist
### 8. Testen
Im Browser sollte Socket.IO jetzt direkt zu `wss://tt-tagebuch.de:3051` verbinden.
## Troubleshooting
### Port 3051 ist nicht erreichbar
1. **Prüfe Firewall:**
```bash
sudo ufw status
sudo ufw allow 3051/tcp
```
2. **Prüfe, ob der Server läuft:**
```bash
sudo netstat -tlnp | grep 3051
sudo ss -tlnp | grep 3051
```
3. **Prüfe Backend-Logs auf Fehler:**
```bash
sudo journalctl -u tt-tagebuch -n 50
# Oder:
pm2 logs tt-tagebuch-backend --lines 50
```
4. **Prüfe, ob HTTPS-Server gestartet wurde:**
- Suche in den Logs nach: `🚀 HTTPS-Server für Socket.IO läuft auf Port 3051`
- Falls nicht vorhanden, prüfe auf Fehler: `⚠️ HTTPS-Server konnte nicht gestartet werden`
### SSL-Zertifikat-Fehler / Berechtigungsfehler
**Fehler:** `EACCES: permission denied, open '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem'`
**Lösung:**
```bash
cd /var/www/tt-tagebuch.de/backend
chmod +x scripts/fixCertPermissions.sh
sudo ./scripts/fixCertPermissions.sh
sudo systemctl restart tt-tagebuch
```
Stelle sicher, dass die Zertifikate existieren:
```bash
ls -la /etc/letsencrypt/live/tt-tagebuch.de/
```
Falls die Zertifikate nicht existieren:
```bash
sudo certbot certonly --standalone -d tt-tagebuch.de
```
### Service läuft als "nobody"
**Problem:** Der Service läuft als `nobody`, was zu eingeschränkt ist.
**Lösung:**
1. Installiere die Service-Datei (siehe Schritt 3)
2. Führe das Berechtigungs-Skript aus (siehe Schritt 4)
3. Starte den Service neu
```bash
# Prüfe aktuellen Service-User
sudo systemctl show -p User tt-tagebuch.service
# Installiere Service-Datei
sudo cp /var/www/tt-tagebuch.de/tt-tagebuch.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl restart tt-tagebuch
# Prüfe, ob jetzt als www-data läuft
sudo systemctl show -p User tt-tagebuch.service
```
### Frontend verbindet nicht
1. **Prüfe Browser-Konsole auf Fehler**
2. **Prüfe, ob `import.meta.env.PROD` korrekt gesetzt ist:**
- In Produktion sollte die Socket.IO-URL `https://tt-tagebuch.de:3051` sein
- In Entwicklung sollte sie `http://localhost:3005` sein
3. **Prüfe, ob die Socket.IO-URL korrekt ist:**
- Öffne Browser-Entwicklertools → Network
- Suche nach WebSocket-Verbindungen
- Die URL sollte `wss://tt-tagebuch.de:3051/socket.io/...` sein
### Server lauscht nur auf localhost
Der Server sollte auf `0.0.0.0` lauschen (nicht nur auf `localhost`).
Dies ist bereits in der Konfiguration eingestellt:
```javascript
httpsServer.listen(httpsPort, '0.0.0.0', () => {
console.log(`🚀 HTTPS-Server für Socket.IO läuft auf Port ${httpsPort}`);
});
```
Falls der Server trotzdem nicht erreichbar ist, prüfe die Backend-Logs.

342
DSGVO_CHECKLIST.md Normal file
View File

@@ -0,0 +1,342 @@
# DSGVO-Konformitäts-Checkliste für Trainingstagebuch
## Status: ⚠️ PRÜFUNG ERFORDERLICH
Diese Checkliste dokumentiert den aktuellen Stand der DSGVO-Konformität der Anwendung.
---
## 1. Datenschutzerklärung ✅ / ⚠️
### Status: ⚠️ Teilweise vorhanden, muss aktualisiert werden
**Vorhanden:**
- ✅ Datenschutzerklärung vorhanden (`/datenschutz`)
- ✅ Impressum vorhanden (`/impressum`)
- ✅ Verlinkung im Footer
**Fehlend/Verbesserungsbedarf:**
- ⚠️ MyTischtennis-Integration nicht erwähnt (Drittlandübermittlung?)
- ⚠️ Logging von API-Requests nicht erwähnt
- ⚠️ Verschlüsselung von Mitgliederdaten nicht erwähnt
- ⚠️ Speicherdauer für Logs nicht konkretisiert
- ⚠️ Keine Informationen zu automatischer Löschung
---
## 2. Einwilligungen ⚠️
### Status: ⚠️ Teilweise vorhanden
**Vorhanden:**
-`picsInInternetAllowed` bei Mitgliedern (Einwilligung für Fotos im Internet)
- ✅ MyTischtennis: `savePassword` und `autoUpdateRatings` (Einwilligungen)
**Fehlend/Verbesserungsbedarf:**
- ⚠️ Keine explizite Einwilligung bei Registrierung zur Datenschutzerklärung
- ⚠️ Keine Einwilligung für Logging von API-Requests
- ⚠️ Keine Einwilligung für Datenübertragung an MyTischtennis.de
- ⚠️ Keine Möglichkeit, Einwilligungen zu widerrufen (außer manuell)
---
## 3. Löschrechte (Art. 17 DSGVO) ⚠️
### Status: ⚠️ Teilweise implementiert
**Vorhanden:**
- ✅ DELETE-Endpunkte für viele Ressourcen (Member, Tournament, etc.)
- ✅ MyTischtennis-Account kann gelöscht werden
**Fehlend/Verbesserungsbedarf:**
-**KRITISCH:** Kein Endpunkt zum vollständigen Löschen eines User-Accounts
-**KRITISCH:** Keine automatische Löschung aller zugehörigen Daten (Cascade-Delete)
- ❌ Keine Löschung von Logs nach Ablauf der Speicherdauer
- ⚠️ Keine Anonymisierung statt Löschung (falls gesetzliche Aufbewahrungspflichten bestehen)
- ⚠️ Keine Bestätigung vor Löschung kritischer Daten
**Empfehlung:**
- Implementiere `/api/user/delete` Endpunkt
- Implementiere automatische Löschung aller zugehörigen Daten:
- UserClub-Einträge
- MyTischtennis-Account
- Alle Logs (nach Anonymisierung)
- Alle Mitglieder, die nur diesem User zugeordnet sind
- Implementiere automatische Löschung von Logs nach 90 Tagen
---
## 4. Auskunftsrechte (Art. 15 DSGVO) ❌
### Status: ❌ Nicht implementiert
**Fehlend:**
-**KRITISCH:** Kein Endpunkt zur Auskunft über gespeicherte Daten
- ❌ Keine Übersicht über alle personenbezogenen Daten eines Users
- ❌ Keine Übersicht über alle Mitgliederdaten
- ❌ Keine Übersicht über Logs, die einen User betreffen
**Empfehlung:**
- Implementiere `/api/user/data-export` Endpunkt
- Exportiere alle Daten in strukturiertem Format (JSON)
- Inkludiere:
- User-Daten
- Vereinszugehörigkeiten
- Mitgliederdaten (falls User Zugriff hat)
- Logs
- MyTischtennis-Daten
---
## 5. Datenportabilität (Art. 20 DSGVO) ❌
### Status: ❌ Nicht implementiert
**Fehlend:**
-**KRITISCH:** Kein Export in maschinenlesbarem Format
- ❌ Keine JSON/XML-Export-Funktion
- ⚠️ PDF-Export für Trainingstage vorhanden, aber nicht für alle Daten
**Empfehlung:**
- Implementiere `/api/user/data-export` mit JSON-Format
- Implementiere Export für:
- Alle eigenen Daten
- Alle Mitgliederdaten (falls berechtigt)
- Alle Trainingsdaten
- Alle Turnierdaten
---
## 6. Verschlüsselung ✅ / ⚠️
### Status: ✅ Gut implementiert
**Vorhanden:**
- ✅ AES-256-CBC Verschlüsselung für Mitgliederdaten:
- firstName, lastName
- birthDate
- phone, street, city, postalCode
- email
- notes (Participant)
- ✅ Passwörter werden mit bcrypt gehasht
- ✅ HTTPS für alle Verbindungen
**Verbesserungsbedarf:**
- ⚠️ Verschlüsselungsschlüssel sollte in separater, sicherer Konfiguration sein
-**BEHOBEN:** MyTischtennis-Daten werden jetzt vollständig verschlüsselt (E-Mail, Zugriffstoken, Refresh-Token, Cookie, Benutzerdaten, Vereinsinformationen)
- ⚠️ Keine Verschlüsselung für Logs (können personenbezogene Daten enthalten)
---
## 7. Logging ⚠️
### Status: ⚠️ Verbesserungsbedarf
**Vorhanden:**
- ✅ Aktivitäts-Logging (`log` Tabelle) - protokolliert wichtige Aktionen
- ✅ Server-Logs - Standard-Server-Logs für Fehlerbehebung
-**ENTFERNT:** API-Logging für MyTischtennis-Requests wurde deaktiviert
**Probleme:**
-**BEHOBEN:** API-Logging für MyTischtennis-Requests wurde komplett entfernt (keine personenbezogenen Daten mehr in API-Logs)
- ⚠️ Keine automatische Löschung von Aktivitätslogs (noch zu implementieren)
-**BEHOBEN:** In Datenschutzerklärung dokumentiert, was geloggt wird
**Empfehlung:**
- ⚠️ Implementiere automatische Löschung von Aktivitätslogs nach angemessener Frist (noch ausstehend)
---
## 8. MyTischtennis-Integration ⚠️
### Status: ⚠️ Verbesserungsbedarf
**Vorhanden:**
- ✅ Verschlüsselung von Passwörtern
- ✅ Einwilligungen (`savePassword`, `autoUpdateRatings`)
- ✅ DELETE-Endpunkt für Account
**Probleme:**
-**BEHOBEN:** Drittlandübermittlung in Datenschutzerklärung erwähnt
- ⚠️ Keine explizite Einwilligung für Datenübertragung an MyTischtennis.de
-**BEHOBEN:** Informationen über Datenschutz bei MyTischtennis.de in Datenschutzerklärung
-**BEHOBEN:** Alle MyTischtennis-Daten werden jetzt verschlüsselt gespeichert
**Empfehlung:**
- Aktualisiere Datenschutzerklärung:
- Erwähne MyTischtennis-Integration
- Erkläre, welche Daten übertragen werden
- Verweise auf Datenschutzerklärung von MyTischtennis.de
- Erkläre Rechtsgrundlage (Einwilligung)
- Implementiere explizite Einwilligung bei Einrichtung der Integration
- Verschlüssele auch Zugriffstoken
---
## 9. Cookies & Local Storage ✅
### Status: ✅ Konform
**Vorhanden:**
- ✅ Nur technisch notwendige Cookies/Storage:
- Session-Token (Session Storage)
- Username, Clubs, Permissions (Local Storage)
- ✅ Keine Tracking-Cookies
- ✅ Keine Werbe-Cookies
- ✅ Dokumentiert in Datenschutzerklärung
**Hinweis:**
- Local Storage wird für persistente Daten verwendet (Clubs, Permissions)
- Dies ist technisch notwendig und DSGVO-konform
---
## 10. Berechtigungssystem ✅
### Status: ✅ Gut implementiert
**Vorhanden:**
- ✅ Rollenbasierte Zugriffe (Admin, Trainer, Mannschaftsführer, Mitglied)
- ✅ Individuelle Berechtigungen pro Ressource
- ✅ Transparente Zugriffskontrolle
- ✅ Logging von Aktivitäten
**Hinweis:**
- Berechtigungssystem ist DSGVO-konform
- Ermöglicht Datenminimierung (Zugriff nur auf notwendige Daten)
---
## 11. Datenminimierung ⚠️
### Status: ⚠️ Teilweise konform
**Vorhanden:**
- ✅ Nur notwendige Daten werden gespeichert
- ✅ Berechtigungssystem ermöglicht minimale Datenzugriffe
**Verbesserungsbedarf:**
- ⚠️ Logs enthalten möglicherweise zu viele Daten (Request/Response-Bodies)
- ⚠️ Keine automatische Löschung alter Daten
- ⚠️ Keine Option, Daten zu anonymisieren statt zu löschen
---
## 12. Technische und organisatorische Maßnahmen (TOM) ✅ / ⚠️
### Status: ✅ Gut, aber verbesserungsbedürftig
**Vorhanden:**
- ✅ Verschlüsselung sensibler Daten
- ✅ HTTPS für alle Verbindungen
- ✅ Passwort-Hashing (bcrypt)
- ✅ Authentifizierung und Autorisierung
- ✅ Berechtigungssystem
**Verbesserungsbedarf:**
- ⚠️ Keine Dokumentation der TOM
- ⚠️ Keine regelmäßigen Sicherheitsupdates dokumentiert
- ⚠️ Keine Backup-Strategie dokumentiert
- ⚠️ Keine Notfallpläne dokumentiert
---
## 13. Auftragsverarbeitung ⚠️
### Status: ⚠️ Nicht dokumentiert
**Fehlend:**
- ⚠️ Keine Informationen über Hosting-Provider
- ⚠️ Keine Informationen über Auftragsverarbeitungsverträge (AVV)
- ⚠️ Keine Informationen über Subunternehmer
**Empfehlung:**
- Dokumentiere alle Auftragsverarbeiter (Hosting, etc.)
- Erwähne in Datenschutzerklärung, dass AVV abgeschlossen wurden
---
## 14. Betroffenenrechte - Umsetzung ❌
### Status: ❌ Nicht vollständig implementiert
**Fehlend:**
-**KRITISCH:** Kein Endpunkt für Auskunft (Art. 15)
-**KRITISCH:** Kein Endpunkt für Löschung (Art. 17)
-**KRITISCH:** Kein Endpunkt für Datenexport (Art. 20)
- ❌ Kein Endpunkt für Berichtigung (Art. 16) - teilweise vorhanden über normale Edit-Endpunkte
- ❌ Kein Endpunkt für Einschränkung (Art. 18)
- ❌ Kein Endpunkt für Widerspruch (Art. 21)
**Empfehlung:**
- Implementiere zentrale Endpunkte für alle Betroffenenrechte:
- `GET /api/user/rights/information` - Auskunft
- `DELETE /api/user/rights/deletion` - Löschung
- `GET /api/user/rights/export` - Datenexport
- `PUT /api/user/rights/restriction` - Einschränkung
- `POST /api/user/rights/objection` - Widerspruch
---
## 15. Kontakt für Datenschutz ✅
### Status: ✅ Vorhanden
**Vorhanden:**
- ✅ E-Mail-Adresse in Datenschutzerklärung: tsschulz@tsschulz.de
- ✅ Vollständige Anschrift im Impressum
---
## Zusammenfassung
### ✅ Gut implementiert:
1. Verschlüsselung sensibler Daten
2. HTTPS
3. Berechtigungssystem
4. Cookies/Local Storage (nur technisch notwendig)
5. Datenschutzerklärung vorhanden
### ⚠️ Verbesserungsbedarf:
1. Datenschutzerklärung aktualisieren (MyTischtennis, Logging)
2. Logging von personenbezogenen Daten reduzieren/anonymisieren
3. Automatische Löschung von Logs implementieren
4. MyTischtennis-Integration in Datenschutzerklärung erwähnen
### ❌ Kritisch - Muss implementiert werden:
1. **Löschrechte-API** (Art. 17 DSGVO)
2. **Auskunftsrechte-API** (Art. 15 DSGVO)
3. **Datenexport-API** (Art. 20 DSGVO)
4. **Automatische Löschung von Logs** nach Retention-Periode
---
## Prioritäten
### Sofort (vor Live-Betrieb):
1. Datenschutzerklärung aktualisieren
2. Löschrechte-API implementieren
3. Auskunftsrechte-API implementieren
4. Datenexport-API implementieren
### Kurzfristig (innerhalb 1 Monat):
1. Automatische Löschung von Logs implementieren
2. Logging von personenbezogenen Daten reduzieren/anonymisieren
3. MyTischtennis-Integration in Datenschutzerklärung dokumentieren
### Mittelfristig (innerhalb 3 Monate):
1. Einwilligungsmanagement implementieren
2. TOM dokumentieren
3. Auftragsverarbeitung dokumentieren
---
## Nächste Schritte
1. ✅ Diese Checkliste erstellen
2. ⏳ Datenschutzerklärung aktualisieren
3. ⏳ Löschrechte-API implementieren
4. ⏳ Auskunftsrechte-API implementieren
5. ⏳ Datenexport-API implementieren
6. ⏳ Logging verbessern

69
SERVER_NODE_UPGRADE.md Normal file
View File

@@ -0,0 +1,69 @@
# Server Node.js Upgrade-Anleitung
## Problem
Der Server verwendet Node.js 20.17.0, aber Vite 7.2.4 benötigt Node.js 20.19+ oder 22.12+.
## Lösung 1: Node.js auf dem Server upgraden (Empfohlen)
### Option A: Node.js 20.19+ installieren
```bash
# Auf dem Server:
# Mit nvm (falls installiert):
nvm install 20.19.0
nvm use 20.19.0
nvm alias default 20.19.0
# Oder mit NodeSource Repository:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs=20.19.0-1nodesource1
# Prüfe Version:
node --version # Sollte 20.19.0 oder höher sein
```
### Option B: Node.js 22.12+ installieren (LTS)
```bash
# Auf dem Server:
# Mit nvm:
nvm install 22.12.0
nvm use 22.12.0
nvm alias default 22.12.0
# Oder mit NodeSource Repository:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# Prüfe Version:
node --version # Sollte 22.12.0 oder höher sein
```
### Nach dem Upgrade
```bash
cd /var/www/tt-tagebuch.de/backend
npm install # Erstellt automatisch den Frontend-Build
sudo systemctl restart tt-tagebuch
```
## Lösung 2: Vite auf Version 6 downgraden (Temporär)
Falls Node.js nicht upgradet werden kann, wurde Vite bereits auf Version 6.0.0 downgraded.
```bash
cd /var/www/tt-tagebuch.de/backend
npm install # Erstellt automatisch den Frontend-Build
sudo systemctl restart tt-tagebuch
```
**Hinweis:** Vite 6 funktioniert mit Node.js 20.17.0, aber Vite 7 bietet bessere Performance und Features.
## Empfehlung
**Node.js upgraden** ist die bessere Lösung, da:
- Vite 7 bessere Performance bietet
- Zukünftige Updates einfacher sind
- Node.js 20.19+ oder 22.12+ LTS-Versionen sind

109
SITEMAP_ANLEITUNG.md Normal file
View File

@@ -0,0 +1,109 @@
# Sitemap für Google Search Console einreichen
## Aktuelle Sitemap
Die Sitemap ist verfügbar unter: `https://tt-tagebuch.de/sitemap.xml`
Sie enthält folgende öffentliche Seiten:
- `/` (Home) - Priorität: 1.0
- `/register` (Registrierung) - Priorität: 0.8
- `/login` (Anmeldung) - Priorität: 0.7
- `/impressum` (Impressum) - Priorität: 0.3
- `/datenschutz` (Datenschutz) - Priorität: 0.3
## Sitemap aktualisieren
### Automatisch (empfohlen)
```bash
./update-sitemap.sh
```
Das Skript aktualisiert automatisch das `lastmod`-Datum auf das heutige Datum.
### Manuell
Die Sitemap-Datei befindet sich in: `frontend/public/sitemap.xml`
Nach Änderungen:
1. Frontend neu bauen: `cd frontend && npm run build`
2. Backend neu starten (falls nötig)
## Sitemap in Google Search Console einreichen
### Schritt 1: Google Search Console öffnen
1. Gehe zu: https://search.google.com/search-console
2. Wähle die Property für `tt-tagebuch.de` aus
### Schritt 2: Sitemap hinzufügen
1. Klicke im linken Menü auf **"Sitemaps"**
2. Im Feld **"Neue Sitemap hinzufügen"** eingeben:
```
sitemap.xml
```
Oder die vollständige URL:
```
https://tt-tagebuch.de/sitemap.xml
```
3. Klicke auf **"Senden"**
### Schritt 3: Status prüfen
- Google wird die Sitemap innerhalb weniger Minuten verarbeiten
- Der Status wird angezeigt:
- ✅ **Erfolgreich**: Sitemap wurde erfolgreich verarbeitet
- ⚠️ **Warnung**: Sitemap wurde verarbeitet, aber es gibt Warnungen
- ❌ **Fehler**: Sitemap konnte nicht verarbeitet werden
### Schritt 4: Indexierung anfordern
Nach dem Einreichen der Sitemap kannst du auch einzelne URLs zur Indexierung anfordern:
1. Gehe zu **"URL-Prüfung"**
2. Gib die URL ein: `https://tt-tagebuch.de/`
3. Klicke auf **"Indexierung anfordern"**
## Sitemap testen
### Online-Tools
- Google Sitemap Tester: https://www.xml-sitemaps.com/validate-xml-sitemap.html
- Sitemap Validator: https://validator.w3.org/
### Per Kommandozeile
```bash
# Sitemap abrufen
curl https://tt-tagebuch.de/sitemap.xml
# XML-Validierung (falls xmllint installiert ist)
curl -s https://tt-tagebuch.de/sitemap.xml | xmllint --noout -
```
## Wichtige Hinweise
1. **robots.txt**: Die Sitemap ist bereits in der `robots.txt` referenziert:
```
Sitemap: https://tt-tagebuch.de/sitemap.xml
```
2. **lastmod-Datum**: Wird automatisch beim Ausführen von `update-sitemap.sh` aktualisiert
3. **Nur öffentliche Seiten**: Die Sitemap enthält nur öffentlich zugängliche Seiten. Geschützte Seiten (die eine Anmeldung erfordern) sind nicht enthalten.
4. **Prioritäten**:
- Homepage: 1.0 (höchste Priorität)
- Registrierung/Login: 0.7-0.8 (wichtig für neue Nutzer)
- Rechtliche Seiten: 0.3 (niedrige Priorität, ändern sich selten)
## Troubleshooting
### Sitemap wird nicht gefunden
- Prüfe, ob die Sitemap unter `https://tt-tagebuch.de/sitemap.xml` erreichbar ist
- Stelle sicher, dass das Frontend gebaut wurde: `cd frontend && npm run build`
- Prüfe die Apache-Konfiguration (sollte statische Dateien aus `/var/www/tt-tagebuch.de` servieren)
### Sitemap wird nicht indexiert
- Warte einige Stunden/Tage - Google braucht Zeit zum Crawlen
- Prüfe in der Search Console, ob es Fehler gibt
- Stelle sicher, dass die URLs in der Sitemap erreichbar sind
- Prüfe, ob die `robots.txt` die Seiten nicht blockiert
### Sitemap enthält Fehler
- Validiere die XML-Struktur mit einem XML-Validator
- Prüfe, ob alle URLs korrekt sind (keine 404-Fehler)
- Stelle sicher, dass alle URLs HTTPS verwenden (nicht HTTP)

22
apache-http.conf.example Normal file
View File

@@ -0,0 +1,22 @@
# Apache-Konfiguration für tt-tagebuch.de - HTTP (Port 80)
#
# Diese Datei kopieren nach: /etc/apache2/sites-available/tt-tagebuch.de.conf
# Dann aktivieren mit: sudo a2ensite tt-tagebuch.de.conf
# Und neu starten: sudo systemctl restart apache2
#
# WICHTIG: Folgende Module müssen aktiviert sein:
# sudo a2enmod rewrite
# sudo systemctl restart apache2
# HTTP: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de
<VirtualHost *:80>
ServerName www.tt-tagebuch.de
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# HTTP: tt-tagebuch.de -> HTTPS: tt-tagebuch.de
<VirtualHost *:80>
ServerName tt-tagebuch.de
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>

60
apache-https.conf.example Normal file
View File

@@ -0,0 +1,60 @@
# Apache-Konfiguration für tt-tagebuch.de - HTTPS (Port 443)
#
# Diese Datei kopieren nach: /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
# Dann aktivieren mit: sudo a2ensite tt-tagebuch.de-le-ssl.conf
# Und neu starten: sudo systemctl restart apache2
#
# WICHTIG: Folgende Module müssen aktiviert sein:
# sudo a2enmod proxy
# sudo a2enmod proxy_http
# sudo a2enmod proxy_wstunnel
# sudo a2enmod rewrite
# sudo a2enmod headers
# sudo systemctl restart apache2
# HTTPS: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de (301-Weiterleitung)
<VirtualHost *:443>
ServerName www.tt-tagebuch.de
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# HTTPS: tt-tagebuch.de - Hauptkonfiguration (non-www)
<VirtualHost *:443>
ServerName tt-tagebuch.de
DocumentRoot /var/www/tt-tagebuch.de
<Directory /var/www/tt-tagebuch.de>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/tt-tagebuch.de_error.log
CustomLog ${APACHE_LOG_DIR}/tt-tagebuch.de_access.log combined
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
ProxyRequests Off
# HINWEIS: Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy)
# Siehe backend/SOCKET_IO_SSL_SETUP.md für Details
# API-Routen
ProxyPass /api http://localhost:3050/api
ProxyPassReverse /api http://localhost:3050/api
# Alle anderen Anfragen an den Backend-Server (für Frontend)
ProxyPass / http://localhost:3050/
ProxyPassReverse / http://localhost:3050/
</VirtualHost>

89
apache.conf.example Normal file
View File

@@ -0,0 +1,89 @@
# Apache-Konfiguration für tt-tagebuch.de
#
# HINWEIS: Diese Datei ist eine kombinierte Referenz.
# Für die tatsächliche Konfiguration werden zwei separate Dateien verwendet:
#
# 1. apache-http.conf.example -> /etc/apache2/sites-available/tt-tagebuch.de.conf
# (HTTP, Port 80 - Weiterleitung zu HTTPS)
#
# 2. apache-https.conf.example -> /etc/apache2/sites-available/tt-tagebuch.de-le-ssl.conf
# (HTTPS, Port 443 - Hauptkonfiguration)
#
# Oder verwende das Update-Skript: ./update-apache-config.sh
#
# WICHTIG: Folgende Module müssen aktiviert sein:
# sudo a2enmod proxy
# sudo a2enmod proxy_http
# sudo a2enmod proxy_wstunnel
# sudo a2enmod rewrite
# sudo a2enmod headers
# sudo systemctl restart apache2
# ============================================
# HTTP (Port 80) - Weiterleitung zu HTTPS
# ============================================
# HTTP: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de
<VirtualHost *:80>
ServerName www.tt-tagebuch.de
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# HTTP: tt-tagebuch.de -> HTTPS: tt-tagebuch.de
<VirtualHost *:80>
ServerName tt-tagebuch.de
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# ============================================
# HTTPS (Port 443) - Weiterleitung www -> non-www
# ============================================
# HTTPS: www.tt-tagebuch.de -> HTTPS: tt-tagebuch.de (301-Weiterleitung)
<VirtualHost *:443>
ServerName www.tt-tagebuch.de
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
Redirect permanent / https://tt-tagebuch.de/
</VirtualHost>
# ============================================
# HTTPS (Port 443) - Hauptkonfiguration (non-www)
# ============================================
<VirtualHost *:443>
ServerName tt-tagebuch.de
DocumentRoot /var/www/tt-tagebuch.de
<Directory /var/www/tt-tagebuch.de>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/tt-tagebuch.de_error.log
CustomLog ${APACHE_LOG_DIR}/tt-tagebuch.de_access.log combined
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
ProxyRequests Off
# HINWEIS: Socket.IO läuft jetzt direkt auf HTTPS-Port 3051 (nicht über Apache-Proxy)
# Siehe backend/SOCKET_IO_SSL_SETUP.md für Details
# API-Routen
ProxyPass /api http://localhost:3050/api
ProxyPassReverse /api http://localhost:3050/api
# Alle anderen Anfragen an den Backend-Server (für Frontend)
ProxyPass / http://localhost:3050/
ProxyPassReverse / http://localhost:3050/
</VirtualHost>

View File

@@ -0,0 +1,140 @@
# Socket.IO mit SSL direkt betreiben (Alternative zu Apache-Proxy)
Falls die Apache-WebSocket-Proxy-Konfiguration nicht funktioniert, kann Socket.IO direkt mit SSL betrieben werden.
## Voraussetzungen
1. SSL-Zertifikat (z.B. von Let's Encrypt)
2. Port in der Firewall öffnen (z.B. 3051)
3. Socket.IO-Server auf HTTPS konfigurieren
## Backend-Konfiguration
### 1. Socket.IO auf HTTPS umstellen
Ändere `backend/server.js`:
```javascript
import https from 'https';
import fs from 'fs';
// SSL-Zertifikat laden
const httpsOptions = {
key: fs.readFileSync('/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem')
};
// HTTPS-Server erstellen
const httpsServer = https.createServer(httpsOptions, app);
// Socket.IO initialisieren
initializeSocketIO(httpsServer);
// HTTPS-Server starten
const httpsPort = process.env.HTTPS_PORT || 3051;
httpsServer.listen(httpsPort, () => {
console.log(`🚀 HTTPS-Server läuft auf Port ${httpsPort}`);
});
// HTTP-Server für API (optional, falls API weiterhin über HTTP laufen soll)
const httpServer = createServer(app);
const httpPort = process.env.PORT || 3005;
httpServer.listen(httpPort, () => {
console.log(`🚀 HTTP-Server läuft auf Port ${httpPort}`);
});
```
### 2. Frontend-Konfiguration
Ändere `frontend/src/services/socketService.js`:
```javascript
import { io } from 'socket.io-client';
import { backendBaseUrl } from '../apiClient.js';
let socket = null;
export const connectSocket = (clubId) => {
// Verwende HTTPS-URL für Socket.IO
const socketUrl = backendBaseUrl.replace('http://', 'https://').replace(':3005', ':3051');
if (socket && socket.connected) {
// Wenn bereits verbunden, verlasse den alten Club-Raum und trete dem neuen bei
if (socket.currentClubId) {
socket.emit('leave-club', socket.currentClubId);
}
} else {
// Neue Verbindung erstellen
socket = io(socketUrl, {
path: '/socket.io/',
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 5,
timeout: 20000,
upgrade: true,
forceNew: false,
secure: true // Wichtig für HTTPS
});
socket.on('connect', () => {
console.log('Socket.IO verbunden');
if (socket.currentClubId) {
socket.emit('join-club', socket.currentClubId);
}
});
socket.on('disconnect', () => {
console.log('Socket.IO getrennt');
});
socket.on('connect_error', (error) => {
console.error('Socket.IO Verbindungsfehler:', error);
});
}
// Club-Raum beitreten
if (clubId) {
socket.emit('join-club', clubId);
socket.currentClubId = clubId;
}
return socket;
};
export const disconnectSocket = () => {
if (socket) {
socket.disconnect();
socket = null;
}
};
export const getSocket = () => socket;
```
### 3. Firewall-Port öffnen
```bash
# UFW (Ubuntu Firewall)
sudo ufw allow 3051/tcp
# Oder iptables
sudo iptables -A INPUT -p tcp --dport 3051 -j ACCEPT
```
### 4. Apache-Konfiguration anpassen
Entferne die Socket.IO-Proxy-Konfiguration aus Apache, da Socket.IO jetzt direkt erreichbar ist.
## Vorteile
- Einfacher zu konfigurieren
- Keine Apache-Proxy-Probleme
- Direkte WebSocket-Verbindung
## Nachteile
- Separater Port muss geöffnet sein
- Zwei Ports (HTTP für API, HTTPS für Socket.IO)
- CORS-Konfiguration muss angepasst werden

View File

@@ -17,19 +17,146 @@ class MyTischtennisClient {
});
}
/**
* Get login page to extract XSRF token and CAPTCHA token
* @returns {Promise<Object>} Object with xsrfToken, captchaToken, and captchaClicked flag
*/
async getLoginPage() {
try {
const response = await this.client.get('/login?next=%2F');
const html = response.data;
// Extract XSRF token from hidden input
const xsrfMatch = html.match(/<input[^>]*name="xsrf"[^>]*value="([^"]+)"/);
const xsrfToken = xsrfMatch ? xsrfMatch[1] : null;
// Extract CAPTCHA token from hidden input (if present)
const captchaMatch = html.match(/<input[^>]*name="captcha"[^>]*value="([^"]+)"/);
const captchaToken = captchaMatch ? captchaMatch[1] : null;
// Check if captcha_clicked is true or false
const captchaClickedMatch = html.match(/<input[^>]*name="captcha_clicked"[^>]*value="([^"]+)"/);
const captchaClicked = captchaClickedMatch ? captchaClickedMatch[1] === 'true' : false;
// Check if CAPTCHA is required (look for private-captcha element or captcha input)
const requiresCaptcha = html.includes('private-captcha') || html.includes('name="captcha"');
console.log('[myTischtennisClient.getLoginPage]', {
hasXsrfToken: !!xsrfToken,
hasCaptchaToken: !!captchaToken,
captchaClicked,
requiresCaptcha
});
return {
success: true,
xsrfToken,
captchaToken,
captchaClicked,
requiresCaptcha
};
} catch (error) {
console.error('Error fetching login page:', error.message);
return {
success: false,
error: error.message
};
}
}
/**
* Login to myTischtennis API
* @param {string} email - myTischtennis email (not username!)
* @param {string} password - myTischtennis password
* @param {string} captchaToken - Optional CAPTCHA token if required
* @param {string} xsrfToken - Optional XSRF token (will be fetched if not provided)
* @returns {Promise<Object>} Login response with token and session data
*/
async login(email, password) {
async login(email, password, captchaToken = null, xsrfToken = null) {
try {
let loginPage = null;
let captchaClicked = false;
// If XSRF token not provided, fetch login page to get it
if (!xsrfToken) {
loginPage = await this.getLoginPage();
if (!loginPage.success) {
return {
success: false,
error: 'Konnte Login-Seite nicht abrufen: ' + loginPage.error
};
}
xsrfToken = loginPage.xsrfToken;
// If CAPTCHA token not provided but found in HTML, use it
if (!captchaToken && loginPage.captchaToken) {
captchaToken = loginPage.captchaToken;
captchaClicked = loginPage.captchaClicked;
console.log('[myTischtennisClient.login] CAPTCHA-Token aus HTML extrahiert, captcha_clicked:', captchaClicked);
}
// If CAPTCHA is required but no token found yet, wait and try to get it again
// Das CAPTCHA-System löst das Puzzle im Hintergrund via JavaScript, daher kann es einen Moment dauern
// Wir müssen mehrmals versuchen, da das Token erst generiert wird, nachdem das JavaScript gelaufen ist
if (loginPage.requiresCaptcha && !captchaToken) {
console.log('[myTischtennisClient.login] CAPTCHA erforderlich, aber noch kein Token gefunden. Warte und versuche erneut...');
// Versuche bis zu 5 Mal, das CAPTCHA-Token zu erhalten
let maxRetries = 5;
let retryCount = 0;
let foundToken = false;
while (retryCount < maxRetries && !foundToken) {
// Warte 2-4 Sekunden zwischen den Versuchen
const waitMs = Math.floor(Math.random() * 2000) + 2000; // 2000-4000ms
console.log(`[myTischtennisClient.login] Versuch ${retryCount + 1}/${maxRetries}: Warte ${waitMs}ms...`);
await new Promise(resolve => setTimeout(resolve, waitMs));
// Versuche erneut, die Login-Seite abzurufen, um das gelöste CAPTCHA-Token zu erhalten
const retryLoginPage = await this.getLoginPage();
if (retryLoginPage.success && retryLoginPage.captchaToken) {
captchaToken = retryLoginPage.captchaToken;
captchaClicked = retryLoginPage.captchaClicked;
xsrfToken = retryLoginPage.xsrfToken || xsrfToken; // Aktualisiere XSRF-Token falls nötig
foundToken = true;
console.log(`[myTischtennisClient.login] CAPTCHA-Token nach ${retryCount + 1} Versuchen gefunden, captcha_clicked:`, captchaClicked);
} else {
retryCount++;
}
}
if (!foundToken) {
// Wenn nach allen Versuchen kein Token gefunden wurde, Fehler zurückgeben
console.log('[myTischtennisClient.login] CAPTCHA-Token konnte nach mehreren Versuchen nicht gefunden werden');
return {
success: false,
error: 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.',
requiresCaptcha: true
};
}
}
// Zufällige Verzögerung von 2-5 Sekunden zwischen Laden des Forms und Absenden
// Simuliert menschliches Verhalten und gibt dem CAPTCHA-System Zeit
const delayMs = Math.floor(Math.random() * 3000) + 2000; // 2000-5000ms
console.log(`[myTischtennisClient] Warte ${delayMs}ms vor Login-Request (simuliert menschliches Verhalten)`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
// Create form data
const formData = new URLSearchParams();
formData.append('email', email);
formData.append('password', password);
formData.append('intent', 'login');
if (xsrfToken) {
formData.append('xsrf', xsrfToken);
}
if (captchaToken) {
formData.append('captcha', captchaToken);
formData.append('captcha_clicked', captchaClicked ? 'true' : 'false');
}
const response = await this.client.post(
'/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
@@ -86,11 +213,36 @@ class MyTischtennisClient {
cookie: authCookie.split(';')[0] // Just the cookie value without attributes
};
} catch (error) {
console.error('MyTischtennis login error:', error.message);
const statusCode = error.response?.status || 500;
const responseData = error.response?.data;
// Check if response contains CAPTCHA error
let errorMessage = error.response?.data?.message || error.message || 'Login fehlgeschlagen';
let requiresCaptcha = false;
// Check for CAPTCHA-related errors in response
if (typeof responseData === 'string') {
if (responseData.includes('Captcha') || responseData.includes('CAPTCHA') ||
responseData.includes('captcha') || responseData.includes('Captcha-Bestätigung')) {
requiresCaptcha = true;
errorMessage = 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.';
}
} else if (responseData && typeof responseData === 'object') {
// Check for CAPTCHA errors in JSON response or HTML
const dataString = JSON.stringify(responseData);
if (dataString.includes('Captcha') || dataString.includes('CAPTCHA') ||
dataString.includes('captcha') || dataString.includes('Captcha-Bestätigung')) {
requiresCaptcha = true;
errorMessage = 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.';
}
}
console.error('MyTischtennis login error:', errorMessage, `(Status: ${statusCode})`, requiresCaptcha ? '(CAPTCHA erforderlich)' : '');
return {
success: false,
error: error.response?.data?.message || 'Login fehlgeschlagen',
status: error.response?.status || 500
error: errorMessage,
status: statusCode,
requiresCaptcha
};
}
}

View File

@@ -0,0 +1,125 @@
# Fehlercode-System - Verwendungsanleitung
## Übersicht
Das Fehlercode-System ersetzt hardcodierte deutsche Fehlermeldungen durch strukturierte Fehlercodes, die im Frontend übersetzt werden.
## Backend-Verwendung
### 1. Fehlercode verwenden
```javascript
import HttpError from '../exceptions/HttpError.js';
import { ERROR_CODES, createError } from '../constants/errorCodes.js';
// Einfacher Fehlercode ohne Parameter
throw new HttpError(createError(ERROR_CODES.USER_NOT_FOUND), 404);
// Fehlercode mit Parametern
throw new HttpError(
createError(ERROR_CODES.MEMBER_NOT_FOUND, { memberId: 123 }),
404
);
// Oder direkt:
throw new HttpError(
{ code: ERROR_CODES.MEMBER_NOT_FOUND, params: { memberId: 123 } },
404
);
```
### 2. Legacy-Format (wird weiterhin unterstützt)
```javascript
// Alte Variante funktioniert noch:
throw new HttpError('Benutzer nicht gefunden', 404);
```
## Frontend-Verwendung
### 1. Fehlermeldungen automatisch übersetzen
Die `getSafeErrorMessage`-Funktion erkennt automatisch Fehlercodes:
```javascript
import { getSafeErrorMessage } from '../utils/errorMessages.js';
// In einer Vue-Komponente (Options API)
try {
await apiClient.post('/api/endpoint', data);
} catch (error) {
const message = getSafeErrorMessage(error, this.$t('errors.ERROR_UNKNOWN_ERROR'), this.$t);
await this.showInfo(this.$t('messages.error'), message, '', 'error');
}
// In einer Vue-Komponente (Composition API)
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
try {
await apiClient.post('/api/endpoint', data);
} catch (error) {
const message = getSafeErrorMessage(error, t('errors.ERROR_UNKNOWN_ERROR'), t);
await showInfo(t('messages.error'), message, '', 'error');
}
```
### 2. Dialog-Utils mit Übersetzung
```javascript
import { buildInfoConfig, safeErrorMessage } from '../utils/dialogUtils.js';
// Mit Übersetzungsfunktion
this.infoDialog = buildInfoConfig({
title: this.$t('messages.error'),
message: safeErrorMessage(error, this.$t('errors.ERROR_UNKNOWN_ERROR'), this.$t),
type: 'error'
}, this.$t);
```
## API-Response-Format
### Neues Format (mit Fehlercode):
```json
{
"success": false,
"code": "ERROR_MEMBER_NOT_FOUND",
"params": {
"memberId": 123
},
"error": "ERROR_MEMBER_NOT_FOUND" // Für Rückwärtskompatibilität
}
```
### Legacy-Format (wird weiterhin unterstützt):
```json
{
"success": false,
"message": "Mitglied nicht gefunden",
"error": "Mitglied nicht gefunden"
}
```
## Übersetzungen hinzufügen
1. **Backend**: Fehlercode in `backend/constants/errorCodes.js` definieren
2. **Frontend**: Übersetzung in `frontend/src/i18n/locales/de.json` unter `errors` hinzufügen
Beispiel:
```json
{
"errors": {
"ERROR_MEMBER_NOT_FOUND": "Mitglied nicht gefunden.",
"ERROR_MEMBER_NOT_FOUND_WITH_ID": "Mitglied mit ID {memberId} nicht gefunden."
}
}
```
## Migration bestehender Fehler
1. Hardcodierte Fehlermeldung identifizieren
2. Passenden Fehlercode in `errorCodes.js` finden oder erstellen
3. Backend-Code anpassen: `throw new HttpError(createError(ERROR_CODES.XXX), status)`
4. Übersetzung in `de.json` hinzufügen
5. Frontend-Code muss nicht geändert werden (automatische Erkennung)

View File

@@ -0,0 +1,121 @@
/**
* Fehlercodes für die API
* Diese Codes werden an das Frontend gesendet und dort übersetzt
*
* Format: { code: string, params?: object }
*
* Beispiel:
* - { code: 'ERROR_USER_NOT_FOUND' }
* - { code: 'ERROR_MEMBER_NOT_FOUND', params: { memberId: 123 } }
* - { code: 'ERROR_VALIDATION_FAILED', params: { field: 'email', value: 'invalid' } }
*/
export const ERROR_CODES = {
// Allgemeine Fehler
INTERNAL_SERVER_ERROR: 'ERROR_INTERNAL_SERVER_ERROR',
UNKNOWN_ERROR: 'ERROR_UNKNOWN_ERROR',
VALIDATION_FAILED: 'ERROR_VALIDATION_FAILED',
NOT_FOUND: 'ERROR_NOT_FOUND',
UNAUTHORIZED: 'ERROR_UNAUTHORIZED',
FORBIDDEN: 'ERROR_FORBIDDEN',
BAD_REQUEST: 'ERROR_BAD_REQUEST',
// Authentifizierung
USER_NOT_FOUND: 'ERROR_USER_NOT_FOUND',
INVALID_PASSWORD: 'ERROR_INVALID_PASSWORD',
LOGIN_FAILED: 'ERROR_LOGIN_FAILED',
SESSION_EXPIRED: 'ERROR_SESSION_EXPIRED',
// MyTischtennis
MYTISCHTENNIS_USER_NOT_FOUND: 'ERROR_MYTISCHTENNIS_USER_NOT_FOUND',
MYTISCHTENNIS_INVALID_PASSWORD: 'ERROR_MYTISCHTENNIS_INVALID_PASSWORD',
MYTISCHTENNIS_LOGIN_FAILED: 'ERROR_MYTISCHTENNIS_LOGIN_FAILED',
MYTISCHTENNIS_ACCOUNT_NOT_LINKED: 'ERROR_MYTISCHTENNIS_ACCOUNT_NOT_LINKED',
MYTISCHTENNIS_PASSWORD_NOT_SAVED: 'ERROR_MYTISCHTENNIS_PASSWORD_NOT_SAVED',
MYTISCHTENNIS_SESSION_EXPIRED: 'ERROR_MYTISCHTENNIS_SESSION_EXPIRED',
MYTISCHTENNIS_NO_PASSWORD_SAVED: 'ERROR_MYTISCHTENNIS_NO_PASSWORD_SAVED',
MYTISCHTENNIS_CAPTCHA_REQUIRED: 'ERROR_MYTISCHTENNIS_CAPTCHA_REQUIRED',
// Mitglieder
MEMBER_NOT_FOUND: 'ERROR_MEMBER_NOT_FOUND',
MEMBER_ALREADY_EXISTS: 'ERROR_MEMBER_ALREADY_EXISTS',
MEMBER_FIRSTNAME_REQUIRED: 'ERROR_MEMBER_FIRSTNAME_REQUIRED',
MEMBER_LASTNAME_REQUIRED: 'ERROR_MEMBER_LASTNAME_REQUIRED',
// Gruppen
GROUP_NOT_FOUND: 'ERROR_GROUP_NOT_FOUND',
GROUP_NAME_REQUIRED: 'ERROR_GROUP_NAME_REQUIRED',
GROUP_ALREADY_EXISTS: 'ERROR_GROUP_ALREADY_EXISTS',
GROUP_INVALID_PRESET_TYPE: 'ERROR_GROUP_INVALID_PRESET_TYPE',
GROUP_CANNOT_RENAME_PRESET: 'ERROR_GROUP_CANNOT_RENAME_PRESET',
// Turniere
TOURNAMENT_NOT_FOUND: 'ERROR_TOURNAMENT_NOT_FOUND',
TOURNAMENT_NO_DATE: 'ERROR_TOURNAMENT_NO_DATE',
TOURNAMENT_CLASS_NAME_REQUIRED: 'ERROR_TOURNAMENT_CLASS_NAME_REQUIRED',
TOURNAMENT_NO_PARTICIPANTS: 'ERROR_TOURNAMENT_NO_PARTICIPANTS',
TOURNAMENT_NO_VALID_PARTICIPANTS: 'ERROR_TOURNAMENT_NO_VALID_PARTICIPANTS',
TOURNAMENT_NO_TRAINING_DAY: 'ERROR_TOURNAMENT_NO_TRAINING_DAY',
TOURNAMENT_PDF_GENERATION_FAILED: 'ERROR_TOURNAMENT_PDF_GENERATION_FAILED',
TOURNAMENT_SELECT_FIRST: 'ERROR_TOURNAMENT_SELECT_FIRST',
// Trainingstagebuch
DIARY_DATE_NOT_FOUND: 'ERROR_DIARY_DATE_NOT_FOUND',
DIARY_DATE_UPDATED: 'ERROR_DIARY_DATE_UPDATED',
DIARY_NO_PARTICIPANTS: 'ERROR_DIARY_NO_PARTICIPANTS',
DIARY_PDF_GENERATION_FAILED: 'ERROR_DIARY_PDF_GENERATION_FAILED',
DIARY_IMAGE_LOAD_FAILED: 'ERROR_DIARY_IMAGE_LOAD_FAILED',
DIARY_STATS_LOAD_FAILED: 'ERROR_DIARY_STATS_LOAD_FAILED',
DIARY_NO_EXERCISE_DATA: 'ERROR_DIARY_NO_EXERCISE_DATA',
DIARY_ACTIVITY_PARTICIPANTS_UPDATE_FAILED: 'ERROR_DIARY_ACTIVITY_PARTICIPANTS_UPDATE_FAILED',
DIARY_GROUP_ASSIGNMENT_UPDATED: 'SUCCESS_DIARY_GROUP_ASSIGNMENT_UPDATED',
DIARY_GROUP_ASSIGNMENT_UPDATE_FAILED: 'ERROR_DIARY_GROUP_ASSIGNMENT_UPDATE_FAILED',
DIARY_ASSIGN_ALL_PARTICIPANTS_FAILED: 'ERROR_DIARY_ASSIGN_ALL_PARTICIPANTS_FAILED',
DIARY_ASSIGN_GROUP_FAILED: 'ERROR_DIARY_ASSIGN_GROUP_FAILED',
DIARY_PARTICIPANT_ASSIGN_FAILED: 'ERROR_DIARY_PARTICIPANT_ASSIGN_FAILED',
DIARY_PARTICIPANT_GROUP_ASSIGNMENT_UPDATE_FAILED: 'ERROR_DIARY_PARTICIPANT_GROUP_ASSIGNMENT_UPDATE_FAILED',
DIARY_MEMBER_CREATED: 'SUCCESS_DIARY_MEMBER_CREATED',
DIARY_MEMBER_CREATE_FAILED: 'ERROR_DIARY_MEMBER_CREATE_FAILED',
// Team Management
TEAM_NOT_LINKED_TO_LEAGUE: 'ERROR_TEAM_NOT_LINKED_TO_LEAGUE',
TEAM_LINK_TO_LEAGUE_REQUIRED: 'ERROR_TEAM_LINK_TO_LEAGUE_REQUIRED',
TEAM_PDF_LOAD_FAILED: 'ERROR_TEAM_PDF_LOAD_FAILED',
TEAM_STATS_LOAD_FAILED: 'ERROR_TEAM_STATS_LOAD_FAILED',
// Aktivitäten
ACTIVITY_IMAGE_DELETE_FAILED: 'ERROR_ACTIVITY_IMAGE_DELETE_FAILED',
// Offizielle Turniere
OFFICIAL_TOURNAMENT_PDF_UPLOAD_SUCCESS: 'SUCCESS_OFFICIAL_TOURNAMENT_PDF_UPLOAD',
OFFICIAL_TOURNAMENT_PDF_UPLOAD_FAILED: 'ERROR_OFFICIAL_TOURNAMENT_PDF_UPLOAD',
// Vereine
CLUB_NOT_FOUND: 'ERROR_CLUB_NOT_FOUND',
CLUB_ALREADY_EXISTS: 'ERROR_CLUB_ALREADY_EXISTS',
CLUB_NAME_REQUIRED: 'ERROR_CLUB_NAME_REQUIRED',
CLUB_NAME_TOO_SHORT: 'ERROR_CLUB_NAME_TOO_SHORT',
// Mitglieder-Übertragung
MEMBER_TRANSFER_BULK_FAILED: 'ERROR_MEMBER_TRANSFER_BULK_FAILED',
// Training
TRAINING_STATS_LOAD_FAILED: 'ERROR_TRAINING_STATS_LOAD_FAILED',
// Logs
LOG_NOT_FOUND: 'ERROR_LOG_NOT_FOUND',
};
/**
* Erstellt ein Fehler-Objekt mit Code und optionalen Parametern
* @param {string} code - Fehlercode aus ERROR_CODES
* @param {object} params - Optionale Parameter für die Fehlermeldung
* @returns {object} Fehler-Objekt mit code und params
*/
export function createError(code, params = null) {
return {
code,
...(params && { params })
};
}

View File

@@ -1,7 +1,7 @@
import diaryService from '../services/diaryService.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
import { emitDiaryDateUpdated, emitDiaryTagAdded, emitDiaryTagRemoved } from '../services/socketService.js';
const getDatesForClub = async (req, res) => {
try {
const { clubId } = req.params;
@@ -43,6 +43,10 @@ const updateTrainingTimes = async (req, res) => {
throw new HttpError('notallfieldsfilled', 400);
}
const updatedDate = await diaryService.updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd);
// Emit Socket-Event
emitDiaryDateUpdated(clubId, dateId, { trainingStart, trainingEnd });
res.status(200).json(updatedDate);
} catch (error) {
console.error('[updateTrainingTimes] - Error:', error);
@@ -79,6 +83,14 @@ const addDiaryTag = async (req, res) => {
const { authcode: userToken } = req.headers;
const { diaryDateId, tagName } = req.body;
const tags = await diaryService.addTagToDate(userToken, diaryDateId, tagName);
// Hole clubId für Event
const { DiaryDate } = await import('../models/index.js');
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId && tags && tags.length > 0) {
emitDiaryTagAdded(diaryDate.clubId, diaryDateId, tags[tags.length - 1]);
}
res.status(201).json(tags);
} catch (error) {
console.error('[addDiaryTag] - Error:', error);
@@ -95,6 +107,12 @@ const addTagToDiaryDate = async (req, res) => {
return res.status(400).json({ message: 'diaryDateId and tagId are required.' });
}
const result = await diaryService.addTagToDiaryDate(userToken, clubId, diaryDateId, tagId);
// Emit Socket-Event
if (result && result.tag) {
emitDiaryTagAdded(clubId, diaryDateId, result.tag);
}
res.status(200).json(result);
} catch (error) {
console.error('[addTagToDiaryDate] - Error:', error);
@@ -106,8 +124,20 @@ const deleteTagFromDiaryDate = async (req, res) => {
try {
const { tagId } = req.query;
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const { clubId } = req.params;
// Hole diaryDateId vor dem Löschen
const { DiaryDateTag } = await import('../models/index.js');
const diaryDateTag = await DiaryDateTag.findByPk(tagId);
const diaryDateId = diaryDateTag?.diaryDateId;
await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId);
// Emit Socket-Event
if (diaryDateId) {
emitDiaryTagRemoved(clubId, diaryDateId, tagId);
}
res.status(200).json({ message: 'Tag deleted' });
} catch (error) {
console.error('[deleteTag] - Error:', error);

View File

@@ -1,4 +1,6 @@
import diaryDateActivityService from '../services/diaryDateActivityService.js';
import { emitActivityChanged } from '../services/socketService.js';
import DiaryDate from '../models/DiaryDates.js';
import { devLog } from '../utils/logger.js';
export const createDiaryDateActivity = async (req, res) => {
@@ -14,6 +16,13 @@ export const createDiaryDateActivity = async (req, res) => {
orderId,
isTimeblock,
});
// Emit Socket-Event
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
res.status(201).json(activityItem);
} catch (error) {
devLog(error);
@@ -34,6 +43,15 @@ export const updateDiaryDateActivity = async (req, res) => {
orderId,
groupId, // Pass groupId to the service
});
// Emit Socket-Event
if (updatedActivity?.diaryDateId) {
const diaryDate = await DiaryDate.findByPk(updatedActivity.diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, updatedActivity.diaryDateId);
}
}
res.status(200).json(updatedActivity);
} catch (error) {
res.status(500).json({ error: 'Error updating activity' });
@@ -44,7 +62,22 @@ export const deleteDiaryDateActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, id } = req.params;
// Hole diaryDateId vor dem Löschen
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
const activity = await DiaryDateActivity.findByPk(id);
const diaryDateId = activity?.diaryDateId;
await diaryDateActivityService.deleteActivity(userToken, clubId, id);
// Emit Socket-Event
if (diaryDateId) {
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
}
res.status(200).json({ message: 'Activity deleted' });
} catch (error) {
res.status(500).json({ error: 'Error deleting activity' });
@@ -57,6 +90,15 @@ export const updateDiaryDateActivityOrder = async (req, res) => {
const { clubId, id } = req.params;
const { orderId } = req.body;
const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId);
// Emit Socket-Event
if (updatedActivity?.diaryDateId) {
const diaryDate = await DiaryDate.findByPk(updatedActivity.diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, updatedActivity.diaryDateId);
}
}
res.status(200).json(updatedActivity);
} catch (error) {
devLog(error);
@@ -79,8 +121,15 @@ export const getDiaryDateActivities = async (req, res) => {
export const addGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateId, groupId, activity, timeblockId } = req.body;
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, timeblockId);
const { clubId, diaryDateId, groupId, activity, predefinedActivityId, timeblockId } = req.body;
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, predefinedActivityId, timeblockId);
// Emit Socket-Event
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
res.status(201).json(activityItem);
} catch (error) {
devLog(error);
@@ -88,11 +137,61 @@ export const addGroupActivity = async(req, res) => {
}
}
export const updateGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupActivityId } = req.params;
const { predefinedActivityId } = req.body;
const activityItem = await diaryDateActivityService.updateGroupActivity(userToken, clubId, groupActivityId, predefinedActivityId);
// Emit Socket-Event
const GroupActivity = (await import('../models/GroupActivity.js')).default;
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
const groupActivity = await GroupActivity.findByPk(groupActivityId);
let diaryDateId = null;
if (groupActivity?.diaryDateActivity) {
const activity = await DiaryDateActivity.findByPk(groupActivity.diaryDateActivity);
diaryDateId = activity?.diaryDateId;
}
if (diaryDateId) {
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
}
res.status(200).json(activityItem);
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Error updating group activity' });
}
}
export const deleteGroupActivity = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupActivityId } = req.params;
// Hole diaryDateId vor dem Löschen
const GroupActivity = (await import('../models/GroupActivity.js')).default;
const DiaryDateActivity = (await import('../models/DiaryDateActivity.js')).default;
const groupActivity = await GroupActivity.findByPk(groupActivityId);
let diaryDateId = null;
if (groupActivity?.diaryDateActivity) {
const activity = await DiaryDateActivity.findByPk(groupActivity.diaryDateActivity);
diaryDateId = activity?.diaryDateId;
}
await diaryDateActivityService.deleteGroupActivity(userToken, clubId, groupActivityId);
// Emit Socket-Event
if (diaryDateId) {
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitActivityChanged(diaryDate.clubId, diaryDateId);
}
}
res.status(200).json({ message: 'Group activity deleted' });
} catch (error) {
devLog(error);

View File

@@ -1,6 +1,9 @@
import DiaryMemberActivity from '../models/DiaryMemberActivity.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import DiaryDates from '../models/DiaryDates.js';
import Participant from '../models/Participant.js';
import { checkAccess } from '../utils/userUtils.js';
import { emitActivityMemberAdded, emitActivityMemberRemoved } from '../services/socketService.js';
export const getMembersForActivity = async (req, res) => {
try {
@@ -31,6 +34,13 @@ export const addMembersToActivity = async (req, res) => {
const validIds = new Set(validParticipants.map(p => p.id));
const created = [];
// Hole clubId und dateId für Events (falls nicht aus params verfügbar)
const activity = await DiaryDateActivity.findByPk(diaryDateActivityId);
const diaryDate = activity ? await DiaryDates.findByPk(activity.diaryDateId) : null;
const eventClubId = diaryDate?.clubId || clubId;
const dateId = diaryDate?.id || null;
for (const pid of participantIds) {
if (!validIds.has(pid)) {
continue;
@@ -39,6 +49,11 @@ export const addMembersToActivity = async (req, res) => {
if (!existing) {
const rec = await DiaryMemberActivity.create({ diaryDateActivityId, participantId: pid });
created.push(rec);
// Emit Socket-Event
if (eventClubId && dateId) {
emitActivityMemberAdded(eventClubId, diaryDateActivityId, pid, dateId);
}
} else {
}
}
@@ -54,7 +69,19 @@ export const removeMemberFromActivity = async (req, res) => {
const { authcode: userToken } = req.headers;
const { clubId, diaryDateActivityId, participantId } = req.params;
await checkAccess(userToken, clubId);
// Hole dateId für Event
const activity = await DiaryDateActivity.findByPk(diaryDateActivityId);
const diaryDate = activity ? await DiaryDates.findByPk(activity.diaryDateId) : null;
const dateId = diaryDate?.id || null;
await DiaryMemberActivity.destroy({ where: { diaryDateActivityId, participantId } });
// Emit Socket-Event
if (dateId) {
emitActivityMemberRemoved(clubId, diaryDateActivityId, participantId, dateId);
}
res.status(200).json({ ok: true });
} catch (e) {
res.status(500).json({ error: 'Error removing member from activity' });

View File

@@ -1,5 +1,6 @@
import { DiaryNote, DiaryTag } from '../models/index.js';
import { DiaryNote, DiaryTag, DiaryDate } from '../models/index.js';
import diaryService from '../services/diaryService.js';
import { emitDiaryNoteAdded, emitDiaryNoteDeleted } from '../services/socketService.js';
export const getNotes = async (req, res) => {
try {
@@ -26,6 +27,9 @@ export const createNote = async (req, res) => {
const newNote = await DiaryNote.create({ memberId, diaryDateId, content });
// Hole DiaryDate für clubId
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (Array.isArray(tags) && tags.length > 0 && typeof newNote.addTags === 'function') {
const tagInstances = await DiaryTag.findAll({ where: { id: tags } });
await newNote.addTags(tagInstances);
@@ -34,9 +38,19 @@ export const createNote = async (req, res) => {
include: [{ model: DiaryTag, as: 'tags', required: false }],
});
// Emit Socket-Event
if (diaryDate?.clubId) {
emitDiaryNoteAdded(diaryDate.clubId, diaryDateId, noteWithTags ?? newNote);
}
return res.status(201).json(noteWithTags ?? newNote);
}
// Emit Socket-Event
if (diaryDate?.clubId) {
emitDiaryNoteAdded(diaryDate.clubId, diaryDateId, newNote);
}
res.status(201).json(newNote);
} catch (error) {
console.error('[createNote] - Error:', error);
@@ -47,7 +61,25 @@ export const createNote = async (req, res) => {
export const deleteNote = async (req, res) => {
try {
const { noteId } = req.params;
// Hole Note für diaryDateId vor dem Löschen
const note = await DiaryNote.findByPk(noteId);
const diaryDateId = note?.diaryDateId;
// Hole DiaryDate für clubId
let clubId = null;
if (diaryDateId) {
const diaryDate = await DiaryDate.findByPk(diaryDateId);
clubId = diaryDate?.clubId;
}
await DiaryNote.destroy({ where: { id: noteId } });
// Emit Socket-Event
if (clubId && diaryDateId) {
emitDiaryNoteDeleted(clubId, diaryDateId, noteId);
}
res.status(200).json({ message: 'Note deleted' });
} catch (error) {
res.status(500).json({ error: 'Error deleting note' });

View File

@@ -1,5 +1,7 @@
import HttpError from '../exceptions/HttpError.js';
import groupService from '../services/groupService.js';
import { emitActivityChanged, emitGroupChanged } from '../services/socketService.js';
import DiaryDate from '../models/DiaryDates.js';
import { devLog } from '../utils/logger.js';
const addGroup = async(req, res) => {
@@ -7,6 +9,15 @@ const addGroup = async(req, res) => {
const { authcode: userToken } = req.headers;
const { clubid: clubId, dateid: dateId, name, lead } = req.body;
const result = await groupService.addGroup(userToken, clubId, dateId, name, lead);
// Emit Socket-Event für Gruppen-Änderungen
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
}
}
res.status(201).json(result);
} catch (error) {
console.error('[addGroup] - Error:', error);
@@ -33,6 +44,15 @@ const changeGroup = async(req, res) => {
const { groupId } = req.params;
const { clubid: clubId, dateid: dateId, name, lead } = req.body;
const result = await groupService.changeGroup(userToken, groupId, clubId, dateId, name, lead);
// Emit Socket-Event für Gruppen-Änderungen
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
}
}
res.status(200).json(result);
} catch (error) {
console.error('[changeGroup] - Error:', error);
@@ -40,4 +60,27 @@ const changeGroup = async(req, res) => {
}
}
export { addGroup, getGroups, changeGroup};
const deleteGroup = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { groupId } = req.params;
const { clubid: clubId, dateid: dateId } = req.body;
const result = await groupService.deleteGroup(userToken, groupId, clubId, dateId);
// Emit Socket-Events für Gruppen- und Aktivitäts-Änderungen (Gruppen werden in Aktivitäten verwendet)
if (dateId) {
const diaryDate = await DiaryDate.findByPk(dateId);
if (diaryDate?.clubId) {
emitGroupChanged(diaryDate.clubId, dateId);
emitActivityChanged(diaryDate.clubId, dateId);
}
}
res.status(200).json(result);
} catch (error) {
console.error('[deleteGroup] - Error:', error);
res.status(error.statusCode || 500).json({ error: error.message });
}
}
export { addGroup, getGroups, changeGroup, deleteGroup};

View File

@@ -49,13 +49,19 @@ export const getMemberActivities = async (req, res) => {
const participantIds = participants.map(p => p.id);
// Get all diary member activities for this member
const whereClause = {
participantId: participantIds
};
// Sammle alle Gruppen-IDs, zu denen der Member gehört
const memberGroupIds = new Set();
participants.forEach(p => {
if (p.groupId !== null && p.groupId !== undefined) {
memberGroupIds.add(p.groupId);
}
});
// 1. Get all diary member activities explicitly assigned to this member
const memberActivities = await DiaryMemberActivity.findAll({
where: whereClause,
where: {
participantId: participantIds
},
include: [
{
model: Participant,
@@ -90,47 +96,186 @@ export const getMemberActivities = async (req, res) => {
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']]
});
// Group activities by name and count occurrences, considering group assignment
// 2. Get all group activities for groups the member belongs to
const groupActivities = [];
if (memberGroupIds.size > 0) {
// Suche direkt nach GroupActivity-Einträgen für die Gruppen des Members
const groupActivitiesData = await GroupActivity.findAll({
where: {
groupId: {
[Op.in]: Array.from(memberGroupIds)
}
},
include: [
{
model: DiaryDateActivity,
as: 'activityGroupActivity',
include: [
{
model: DiaryDates,
as: 'diaryDate',
where: startDate ? {
date: {
[Op.gte]: startDate
}
} : {}
},
{
model: PredefinedActivity,
as: 'predefinedActivity',
required: false
}
]
},
{
model: PredefinedActivity,
as: 'groupPredefinedActivity',
required: false
}
]
});
// Erstelle virtuelle DiaryMemberActivity-Objekte für Gruppen-Aktivitäten
for (const groupActivity of groupActivitiesData) {
if (!groupActivity.activityGroupActivity || !groupActivity.activityGroupActivity.diaryDate) {
continue; // Überspringe, wenn keine DiaryDateActivity oder kein DiaryDate vorhanden
}
const activity = groupActivity.activityGroupActivity;
const diaryDateId = activity.diaryDateId;
// Finde alle relevanten Participants für dieses DiaryDate
const relevantParticipants = participants.filter(p =>
p.diaryDateId === diaryDateId &&
p.groupId === groupActivity.groupId
);
for (const participant of relevantParticipants) {
// Verwende die PredefinedActivity aus GroupActivity, falls vorhanden
// Sonst die aus DiaryDateActivity
const predefinedActivity = groupActivity.groupPredefinedActivity || activity.predefinedActivity;
if (predefinedActivity) {
// Erstelle ein modifiziertes Activity-Objekt
const modifiedActivity = {
...activity.toJSON(),
predefinedActivity: predefinedActivity
};
groupActivities.push({
activity: modifiedActivity,
participant: participant,
id: null // Virtuell, nicht in DB
});
}
}
}
}
// Filter: explizite Zuordnungen sollen nur dann zählen, wenn
// - der Participant keine Gruppe hat UND die Aktivität KEINE Gruppenbindung hat, oder
// - die Aktivität keine Gruppenbindung hat, oder
// - es eine Gruppenbindung gibt, die zur Gruppe des Participants passt.
const filteredMemberActivities = memberActivities.filter((ma) => {
if (!ma?.participant || !ma?.activity) {
return false;
}
const participantGroupId = ma.participant.groupId;
const groupActivitiesForActivity = ma.activity.groupActivities || [];
// Participant ohne Gruppe -> nur Aktivitäten ohne Gruppenbindung zählen
if (participantGroupId === null || participantGroupId === undefined) {
return !groupActivitiesForActivity.length;
}
// Keine Gruppenbindung -> immer zählen
if (!groupActivitiesForActivity.length) {
return true;
}
// Gruppenbindung vorhanden -> nur zählen, wenn die Gruppe passt
return groupActivitiesForActivity.some((ga) => Number(ga.groupId) === Number(participantGroupId));
});
// 3. Kombiniere beide Listen und entferne Duplikate
// Ein Duplikat liegt vor, wenn dieselbe Aktivität für denselben Participant bereits explizit zugeordnet ist
const explicitActivityKeys = new Set();
filteredMemberActivities.forEach(ma => {
if (ma.activity && ma.activity.id && ma.participant && ma.participant.id) {
// Erstelle einen eindeutigen Schlüssel: activityId-participantId
const key = `${ma.activity.id}-${ma.participant.id}`;
explicitActivityKeys.add(key);
}
});
// Filtere Gruppen-Aktivitäten, die bereits explizit zugeordnet sind
const uniqueGroupActivities = groupActivities.filter(ga => {
if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) {
return false;
}
const key = `${ga.activity.id}-${ga.participant.id}`;
return !explicitActivityKeys.has(key);
});
// Kombiniere beide Listen
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
// Group activities by name and count occurrences
// Verwende einen Set pro Aktivität, um eindeutige Datum-Aktivität-Kombinationen zu tracken
const activityMap = new Map();
for (const ma of memberActivities) {
for (const ma of allActivities) {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.participant) {
continue;
}
// Check group assignment
const participantGroupId = ma.participant.groupId;
const activityGroupIds = ma.activity.groupActivities?.map(ga => ga.groupId) || [];
// Filter: Only count if:
// 1. Activity has no group assignment (empty activityGroupIds) - activity is for all groups OR
// 2. Participant's group matches one of the activity's groups
const shouldCount = activityGroupIds.length === 0 ||
(participantGroupId !== null && activityGroupIds.includes(participantGroupId));
if (!shouldCount) {
continue;
}
const activity = ma.activity.predefinedActivity;
const activityName = activity.name;
const activityCode = activity.code || activity.name; // Verwende Code falls vorhanden, sonst Name
const date = ma.activity.diaryDate?.date;
if (!activityMap.has(activityName)) {
activityMap.set(activityName, {
name: activityName,
count: 0,
if (!date) {
continue; // Überspringe Einträge ohne Datum
}
// Verwende Code als Key, falls vorhanden, sonst Name
const key = activityCode;
if (!activityMap.has(key)) {
activityMap.set(key, {
name: activityName, // Vollständiger Name für Tooltip
code: activityCode, // Code/Kürzel für Anzeige
uniqueDates: new Set(), // Set für eindeutige Daten
dates: []
});
}
const activityData = activityMap.get(activityName);
activityData.count++;
if (date) {
const activityData = activityMap.get(key);
// Konvertiere Datum zu String für Set-Vergleich (nur Datum, keine Zeit)
const dateString = date instanceof Date
? date.toISOString().split('T')[0]
: new Date(date).toISOString().split('T')[0];
// Füge Datum nur hinzu, wenn es noch nicht vorhanden ist
if (!activityData.uniqueDates.has(dateString)) {
activityData.uniqueDates.add(dateString);
activityData.dates.push(date);
}
}
// Konvertiere Sets zu Arrays und setze count basierend auf eindeutigen Daten
activityMap.forEach((activityData, key) => {
activityData.count = activityData.uniqueDates.size;
// Sortiere Daten (neueste zuerst)
activityData.dates.sort((a, b) => {
const dateA = new Date(a);
const dateB = new Date(b);
return dateB - dateA;
});
// Entferne uniqueDates, da es nicht an Frontend gesendet werden muss
delete activityData.uniqueDates;
});
// Convert map to array and sort by count
const activities = Array.from(activityMap.values())
.sort((a, b) => b.count - a.count);
@@ -162,7 +307,15 @@ export const getMemberLastParticipations = async (req, res) => {
const participantIds = participants.map(p => p.id);
// Get last participations for this member
// Sammle alle Gruppen-IDs, zu denen der Member gehört
const memberGroupIds = new Set();
participants.forEach(p => {
if (p.groupId !== null && p.groupId !== undefined) {
memberGroupIds.add(p.groupId);
}
});
// 1. Get last participations explicitly assigned to this member
const memberActivities = await DiaryMemberActivity.findAll({
where: {
participantId: participantIds
@@ -196,31 +349,177 @@ export const getMemberLastParticipations = async (req, res) => {
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
limit: parseInt(limit) * 10 // Get more to filter by group
});
// Siehe getMemberActivities(): nur zählen, wenn Gruppenbindung passt (oder keine existiert)
const filteredMemberActivities = memberActivities.filter((ma) => {
if (!ma?.participant || !ma?.activity) {
return false;
}
const participantGroupId = ma.participant.groupId;
const groupActivitiesForActivity = ma.activity.groupActivities || [];
if (!groupActivitiesForActivity.length) {
return true;
}
return groupActivitiesForActivity.some((ga) => Number(ga.groupId) === Number(participantGroupId));
});
// Format the results, considering group assignment
const participations = memberActivities
// 2. Get all group activities for groups the member belongs to
const groupActivities = [];
if (memberGroupIds.size > 0) {
// Suche direkt nach GroupActivity-Einträgen für die Gruppen des Members
const groupActivitiesData = await GroupActivity.findAll({
where: {
groupId: {
[Op.in]: Array.from(memberGroupIds)
}
},
include: [
{
model: DiaryDateActivity,
as: 'activityGroupActivity',
include: [
{
model: DiaryDates,
as: 'diaryDate'
},
{
model: PredefinedActivity,
as: 'predefinedActivity',
required: false
}
]
},
{
model: PredefinedActivity,
as: 'groupPredefinedActivity',
required: false
}
],
order: [[{ model: DiaryDateActivity, as: 'activityGroupActivity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
limit: parseInt(limit) * 10 // Get more to filter
});
// Erstelle virtuelle DiaryMemberActivity-Objekte für Gruppen-Aktivitäten
for (const groupActivity of groupActivitiesData) {
if (!groupActivity.activityGroupActivity || !groupActivity.activityGroupActivity.diaryDate) {
continue; // Überspringe, wenn keine DiaryDateActivity oder kein DiaryDate vorhanden
}
const activity = groupActivity.activityGroupActivity;
const diaryDateId = activity.diaryDateId;
// Finde alle relevanten Participants für dieses DiaryDate
const relevantParticipants = participants.filter(p =>
p.diaryDateId === diaryDateId &&
p.groupId === groupActivity.groupId
);
for (const participant of relevantParticipants) {
// Verwende die PredefinedActivity aus GroupActivity, falls vorhanden
// Sonst die aus DiaryDateActivity
const predefinedActivity = groupActivity.groupPredefinedActivity || activity.predefinedActivity;
if (predefinedActivity) {
// Erstelle ein modifiziertes Activity-Objekt
const modifiedActivity = {
...activity.toJSON(),
predefinedActivity: predefinedActivity
};
groupActivities.push({
activity: modifiedActivity,
participant: participant,
id: null // Virtuell, nicht in DB
});
}
}
}
}
// 3. Kombiniere beide Listen und entferne Duplikate
// Ein Duplikat liegt vor, wenn dieselbe Aktivität für denselben Participant bereits explizit zugeordnet ist
const explicitActivityKeys = new Set();
filteredMemberActivities.forEach(ma => {
if (ma.activity && ma.activity.id && ma.participant && ma.participant.id) {
// Erstelle einen eindeutigen Schlüssel: activityId-participantId
const key = `${ma.activity.id}-${ma.participant.id}`;
explicitActivityKeys.add(key);
}
});
// Filtere Gruppen-Aktivitäten, die bereits explizit zugeordnet sind
const uniqueGroupActivities = groupActivities.filter(ga => {
if (!ga.activity || !ga.activity.id || !ga.participant || !ga.participant.id) {
return false;
}
const key = `${ga.activity.id}-${ga.participant.id}`;
return !explicitActivityKeys.has(key);
});
// Kombiniere beide Listen
const allActivities = [...filteredMemberActivities, ...uniqueGroupActivities];
// Gruppiere nach Datum
const participationsByDate = new Map();
allActivities
.filter(ma => {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.activity.diaryDate || !ma.participant) {
return false;
}
// Check group assignment
const participantGroupId = ma.participant.groupId;
const activityGroupIds = ma.activity.groupActivities?.map(ga => ga.groupId) || [];
// Filter: Only count if:
// 1. Activity has no group assignment (empty activityGroupIds) - activity is for all groups OR
// 2. Participant's group matches one of the activity's groups
return activityGroupIds.length === 0 ||
(participantGroupId !== null && activityGroupIds.includes(participantGroupId));
return true;
})
.slice(0, parseInt(limit)) // Limit after filtering
.map(ma => ({
id: ma.id,
activityName: ma.activity.predefinedActivity.name,
date: ma.activity.diaryDate.date,
diaryDateId: ma.activity.diaryDate.id
}));
.forEach(ma => {
const date = ma.activity.diaryDate.date;
const diaryDateId = ma.activity.diaryDate.id;
const activity = ma.activity.predefinedActivity;
const activityName = activity.name;
const activityCode = activity.code || activity.name;
if (!participationsByDate.has(date)) {
participationsByDate.set(date, {
date: date,
diaryDateId: diaryDateId,
activities: []
});
}
const dateEntry = participationsByDate.get(date);
// Füge Aktivität nur hinzu, wenn sie noch nicht vorhanden ist (vermeide Duplikate)
// Speichere sowohl code als auch name
const activityEntry = {
code: activityCode,
name: activityName
};
if (!dateEntry.activities.find(a => (a.code || a.name) === activityCode)) {
dateEntry.activities.push(activityEntry);
}
});
// Sortiere nach Datum (neueste zuerst) und nehme die letzten N Daten
const sortedDates = Array.from(participationsByDate.values())
.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB - dateA;
})
.slice(0, parseInt(limit));
// Formatiere für das Frontend: Flache Liste mit Datum und Aktivität
const participations = [];
sortedDates.forEach(dateEntry => {
dateEntry.activities.forEach(activity => {
participations.push({
id: null, // Virtuell
activityName: activity.code || activity.name, // Code für Anzeige
activityFullName: activity.name, // Vollständiger Name für Tooltip
date: dateEntry.date,
diaryDateId: dateEntry.diaryDateId
});
});
});
return res.status(200).json(participations);

View File

@@ -1,5 +1,6 @@
import MemberService from "../services/memberService.js";
import MemberTransferService from "../services/memberTransferService.js";
import { emitMemberChanged } from '../services/socketService.js';
import { devLog } from '../utils/logger.js';
const getClubMembers = async(req, res) => {
@@ -32,6 +33,12 @@ const setClubMembers = async (req, res) => {
const { authcode: userToken } = req.headers;
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate,
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts);
// Emit Socket-Event wenn Member erfolgreich erstellt/aktualisiert wurde
if (addResult.status === 200) {
emitMemberChanged(clubId);
}
res.status(addResult.status || 500).json(addResult.response);
} catch (error) {
console.error('[setClubMembers] - Error:', error);
@@ -124,10 +131,14 @@ const generateMemberGallery = async (req, res) => {
const { authcode: userToken } = req.headers;
const size = parseInt(req.query.size) || 200; // Default: 200x200
const format = req.query.format || 'image'; // 'image' or 'json'
const result = await MemberService.generateMemberGallery(userToken, clubId, size);
// Bei format=json wird kein Bild erstellt, nur die Mitgliederliste zurückgegeben
const createImage = format !== 'json';
const result = await MemberService.generateMemberGallery(userToken, clubId, size, createImage);
if (result.status === 200) {
if (format === 'json') {
// Return member information for interactive gallery
// Return member information for interactive gallery (ohne Bild zu erstellen)
return res.status(200).json({
members: result.galleryEntries.map(entry => ({
memberId: entry.memberId,

View File

@@ -1,5 +1,6 @@
import myTischtennisService from '../services/myTischtennisService.js';
import HttpError from '../exceptions/HttpError.js';
import axios from 'axios';
class MyTischtennisController {
/**
@@ -199,6 +200,292 @@ class MyTischtennisController {
next(error);
}
}
/**
* GET /api/mytischtennis/login-page
* Proxy für Login-Seite (für iframe)
* Lädt die Login-Seite von mytischtennis.de und modifiziert sie, sodass Form-Submissions über unseren Proxy gehen
* Authentifizierung ist optional - Token kann als Query-Parameter übergeben werden
*/
async getLoginPage(req, res, next) {
try {
// Versuche, userId aus Token zu bekommen (optional)
let userId = null;
const token = req.query.token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode'];
if (token) {
try {
const jwt = (await import('jsonwebtoken')).default;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
userId = decoded.userId;
} catch (err) {
// Token ungültig - ignorieren
}
}
// Speichere userId im Request für submitLogin
req.userId = userId;
// Lade die Login-Seite von mytischtennis.de
const response = await axios.get('https://www.mytischtennis.de/login?next=%2F', {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
},
maxRedirects: 5,
validateStatus: () => true // Akzeptiere alle Status-Codes
});
// Setze Cookies aus der Response
const setCookieHeaders = response.headers['set-cookie'];
if (setCookieHeaders) {
res.setHeader('Set-Cookie', setCookieHeaders);
}
// Modifiziere HTML: Ändere Form-Action auf unseren Proxy
let html = response.data;
if (typeof html === 'string') {
// Füge Token als Hidden-Input hinzu, damit submitLogin die userId bekommt
const tokenInput = userId ? `<input type="hidden" name="__token" value="${token}" />` : '';
// Ersetze Form-Action URLs und füge Token-Input hinzu
html = html.replace(
/(<form[^>]*action="[^"]*\/login[^"]*"[^>]*>)/g,
`$1${tokenInput}`
);
html = html.replace(
/action="([^"]*\/login[^"]*)"/g,
'action="/api/mytischtennis/login-submit"'
);
// Ersetze auch relative URLs
html = html.replace(
/action="\/login/g,
'action="/api/mytischtennis/login-submit'
);
}
// Setze Content-Type
res.setHeader('Content-Type', response.headers['content-type'] || 'text/html; charset=utf-8');
// Sende den modifizierten HTML-Inhalt
res.status(response.status).send(html);
} catch (error) {
console.error('Fehler beim Laden der Login-Seite:', error);
next(error);
}
}
/**
* POST /api/mytischtennis/login-submit
* Proxy für Login-Form-Submission
* Leitet den Login-Request durch, damit Cookies im Backend-Kontext bleiben
* Authentifizierung ist optional - iframe kann keinen Token mitsenden
*/
async submitLogin(req, res, next) {
try {
// Versuche, userId aus Token zu bekommen (aus Query-Parameter oder Hidden-Input)
let userId = null;
const token = req.query.token || req.body.__token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode'];
if (token) {
try {
const jwt = (await import('jsonwebtoken')).default;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
userId = decoded.userId;
} catch (err) {
// Token ungültig - ignorieren
}
}
// Entferne __token aus req.body, damit es nicht an mytischtennis.de gesendet wird
if (req.body.__token) {
delete req.body.__token;
}
// Hole Cookies aus dem Request
const cookies = req.headers.cookie || '';
// Leite den Login-Request an mytischtennis.de weiter
const response = await axios.post(
'https://www.mytischtennis.de/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
req.body, // Form-Daten
{
headers: {
'Cookie': cookies,
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': '*/*',
'Referer': 'https://www.mytischtennis.de/login?next=%2F'
},
maxRedirects: 0,
validateStatus: () => true
}
);
// Setze Cookies aus der Response
const setCookieHeaders = response.headers['set-cookie'];
if (setCookieHeaders) {
res.setHeader('Set-Cookie', setCookieHeaders);
}
// Setze andere relevante Headers
if (response.headers['content-type']) {
res.setHeader('Content-Type', response.headers['content-type']);
}
if (response.headers['location']) {
res.setHeader('Location', response.headers['location']);
}
// Prüfe, ob Login erfolgreich war (durch Prüfung der Cookies)
const authCookie = setCookieHeaders?.find(cookie => cookie.startsWith('sb-10-auth-token='));
if (authCookie && userId) {
// Login erfolgreich - speichere Session (nur wenn userId vorhanden)
await this.saveSessionFromCookie(userId, authCookie);
}
// Sende Response weiter
res.status(response.status).send(response.data);
} catch (error) {
console.error('Fehler beim Login-Submit:', error);
next(error);
}
}
/**
* Speichere Session-Daten aus Cookie
*/
async saveSessionFromCookie(userId, cookieString) {
try {
const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/);
if (!tokenMatch) {
throw new Error('Token-Format ungültig');
}
const base64Token = tokenMatch[1];
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
const tokenData = JSON.parse(decodedToken);
const MyTischtennis = (await import('../models/MyTischtennis.js')).default;
const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } });
if (myTischtennisAccount) {
myTischtennisAccount.accessToken = tokenData.access_token;
myTischtennisAccount.refreshToken = tokenData.refresh_token;
myTischtennisAccount.expiresAt = tokenData.expires_at;
myTischtennisAccount.cookie = cookieString.split(';')[0].trim();
myTischtennisAccount.userData = tokenData.user;
myTischtennisAccount.lastLoginSuccess = new Date();
myTischtennisAccount.lastLoginAttempt = new Date();
// Hole Club-Informationen
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
const profileResult = await myTischtennisClient.getUserProfile(myTischtennisAccount.cookie);
if (profileResult.success) {
myTischtennisAccount.clubId = profileResult.clubId;
myTischtennisAccount.clubName = profileResult.clubName;
myTischtennisAccount.fedNickname = profileResult.fedNickname;
}
await myTischtennisAccount.save();
}
} catch (error) {
console.error('Fehler beim Speichern der Session:', error);
throw error;
}
}
/**
* POST /api/mytischtennis/extract-session
* Extrahiere Session nach Login im iframe
* Versucht, die Session-Daten aus den Cookies zu extrahieren
* Authentifizierung ist optional - iframe kann keinen Token mitsenden
*/
async extractSession(req, res, next) {
try {
// Versuche, userId aus Token zu bekommen (optional)
let userId = req.user?.id;
// Falls kein Token vorhanden, versuche userId aus Account zu bekommen (falls E-Mail bekannt)
if (!userId) {
// Kann nicht ohne Authentifizierung arbeiten - Session kann nicht gespeichert werden
return res.status(401).json({
error: 'Authentifizierung erforderlich zum Speichern der Session'
});
}
// Hole die Cookies aus dem Request
const cookies = req.headers.cookie || '';
// Versuche, die Session zu verifizieren, indem wir einen Request mit den Cookies machen
const response = await axios.get('https://www.mytischtennis.de/?_data=root', {
headers: {
'Cookie': cookies,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json'
},
validateStatus: () => true
});
// Prüfe, ob wir eingeloggt sind (durch Prüfung der Response)
if (response.status === 200 && response.data?.userProfile) {
// Session erfolgreich - speichere die Daten
const account = await myTischtennisService.getAccount(userId);
if (!account) {
throw new HttpError('Kein myTischtennis-Account verknüpft', 404);
}
// Extrahiere Cookie-String
const cookieString = cookies.split(';').find(c => c.trim().startsWith('sb-10-auth-token='));
if (!cookieString) {
throw new HttpError('Kein Auth-Token in Cookies gefunden', 400);
}
// Parse Token aus Cookie
const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/);
if (!tokenMatch) {
throw new HttpError('Token-Format ungültig', 400);
}
const base64Token = tokenMatch[1];
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
const tokenData = JSON.parse(decodedToken);
// Aktualisiere Account mit Session-Daten
const MyTischtennis = (await import('../models/MyTischtennis.js')).default;
const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } });
if (myTischtennisAccount) {
myTischtennisAccount.accessToken = tokenData.access_token;
myTischtennisAccount.refreshToken = tokenData.refresh_token;
myTischtennisAccount.expiresAt = tokenData.expires_at;
myTischtennisAccount.cookie = cookieString.trim();
myTischtennisAccount.userData = tokenData.user;
myTischtennisAccount.lastLoginSuccess = new Date();
myTischtennisAccount.lastLoginAttempt = new Date();
// Hole Club-Informationen
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
const profileResult = await myTischtennisClient.getUserProfile(cookieString.trim());
if (profileResult.success) {
myTischtennisAccount.clubId = profileResult.clubId;
myTischtennisAccount.clubName = profileResult.clubName;
myTischtennisAccount.fedNickname = profileResult.fedNickname;
}
await myTischtennisAccount.save();
}
res.status(200).json({
success: true,
message: 'Session erfolgreich extrahiert und gespeichert'
});
} else {
throw new HttpError('Nicht eingeloggt oder Session ungültig', 401);
}
} catch (error) {
console.error('Fehler beim Extrahieren der Session:', error);
next(error);
}
}
}
export default new MyTischtennisController();

View File

@@ -233,9 +233,11 @@ export const listOfficialTournaments = async (req, res) => {
const { clubId } = req.params;
await checkAccess(userToken, clubId);
const list = await OfficialTournament.findAll({ where: { clubId } });
res.status(200).json(list);
res.status(200).json(Array.isArray(list) ? list : []);
} catch (e) {
res.status(500).json({ error: 'Failed to list tournaments' });
console.error('[listOfficialTournaments] Error:', e);
const errorMessage = e.message || 'Failed to list tournaments';
res.status(e.statusCode || 500).json({ error: errorMessage });
}
};

View File

@@ -1,6 +1,7 @@
import Participant from '../models/Participant.js';
import DiaryDates from '../models/DiaryDates.js';
import { devLog } from '../utils/logger.js';
import { emitParticipantAdded, emitParticipantRemoved, emitParticipantUpdated } from '../services/socketService.js';
export const getParticipants = async (req, res) => {
try {
const { dateId } = req.params;
@@ -24,7 +25,12 @@ export const updateParticipantGroup = async (req, res) => {
where: {
diaryDateId: dateId,
memberId: memberId
}
},
include: [{
model: DiaryDates,
as: 'diaryDate',
attributes: ['clubId']
}]
});
if (!participant) {
@@ -34,7 +40,25 @@ export const updateParticipantGroup = async (req, res) => {
participant.groupId = groupId || null;
await participant.save();
res.status(200).json(participant);
// Lade den Participant erneut aus der DB, um sicherzustellen, dass wir den aktuellen Wert haben
const updatedParticipant = await Participant.findOne({
where: {
diaryDateId: dateId,
memberId: memberId
},
include: [{
model: DiaryDates,
as: 'diaryDate',
attributes: ['clubId']
}]
});
// Emit Socket-Event mit dem aktualisierten Participant
if (updatedParticipant?.diaryDate?.clubId) {
emitParticipantUpdated(updatedParticipant.diaryDate.clubId, dateId, updatedParticipant);
}
res.status(200).json(updatedParticipant || participant);
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Fehler beim Aktualisieren der Teilnehmer-Gruppenzuordnung' });
@@ -45,6 +69,13 @@ export const addParticipant = async (req, res) => {
try {
const { diaryDateId, memberId } = req.body;
const participant = await Participant.create({ diaryDateId, memberId });
// Hole DiaryDate für clubId
const diaryDate = await DiaryDates.findByPk(diaryDateId);
if (diaryDate?.clubId) {
emitParticipantAdded(diaryDate.clubId, diaryDateId, participant);
}
res.status(201).json(participant);
} catch (error) {
devLog(error);
@@ -55,7 +86,18 @@ export const addParticipant = async (req, res) => {
export const removeParticipant = async (req, res) => {
try {
const { diaryDateId, memberId } = req.body;
// Hole DiaryDate für clubId vor dem Löschen
const diaryDate = await DiaryDates.findByPk(diaryDateId);
const clubId = diaryDate?.clubId;
await Participant.destroy({ where: { diaryDateId, memberId } });
// Emit Socket-Event
if (clubId) {
emitParticipantRemoved(clubId, diaryDateId, memberId);
}
res.status(200).json({ message: 'Teilnehmer entfernt' });
} catch (error) {
devLog(error);

View File

@@ -1,5 +1,8 @@
// controllers/tournamentController.js
import tournamentService from "../services/tournamentService.js";
import { emitTournamentChanged } from '../services/socketService.js';
import TournamentClass from '../models/TournamentClass.js';
import HttpError from '../exceptions/HttpError.js';
// 1. Alle Turniere eines Vereins
export const getTournaments = async (req, res) => {
@@ -10,6 +13,11 @@ export const getTournaments = async (req, res) => {
res.status(200).json(tournaments);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
res.set('x-debug-tournament-clubid', String(clubId));
res.set('x-debug-tournament-clubid-num', String(Number(clubId)));
return res.status(error.statusCode || 500).json({ error: error.message });
}
res.status(500).json({ error: error.message });
}
};
@@ -17,36 +25,66 @@ export const getTournaments = async (req, res) => {
// 2. Neues Turnier anlegen
export const addTournament = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentName, date } = req.body;
const { clubId, tournamentName, date, winningSets, allowsExternal } = req.body;
try {
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date);
const tournament = await tournamentService.addTournament(token, clubId, tournamentName, date, winningSets, allowsExternal);
// Emit Socket-Event
if (clubId && tournament && tournament.id) {
emitTournamentChanged(clubId, tournament.id);
}
res.status(201).json(tournament);
} catch (error) {
console.error(error);
console.error('[addTournament] Error:', error);
res.status(500).json({ error: error.message });
}
};
// 3. Teilnehmer hinzufügen
// 3. Teilnehmer hinzufügen - klassengebunden
export const addParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participant: participantId } = req.body;
const { clubId, classId, participant: participantId, tournamentId } = req.body;
try {
await tournamentService.addParticipant(token, clubId, tournamentId, participantId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
// Payloads:
// - Mit Klasse (klassengebunden): { clubId, classId, participant }
// - Ohne Klasse (turnierweit): { clubId, tournamentId, participant, classId: null }
if (!participantId) {
return res.status(400).json({ error: 'Teilnehmer-ID ist erforderlich' });
}
// Allow adding a participant either to a specific class (classId) or to the whole tournament (no class)
if (!classId && !tournamentId) {
return res.status(400).json({ error: 'Klasse oder tournamentId ist erforderlich' });
}
// Pass through to service. If classId is present it will be used, otherwise the service should add the participant with classId = null for the given tournamentId
await tournamentService.addParticipant(token, clubId, classId || null, participantId, tournamentId || null);
// Determine tournamentId for response and event emission
let respTournamentId = tournamentId;
if (classId && !respTournamentId) {
const tournamentClass = await TournamentClass.findByPk(classId);
if (!tournamentClass) {
return res.status(404).json({ error: 'Klasse nicht gefunden' });
}
respTournamentId = tournamentClass.tournamentId;
}
// Fetch updated participants for the (optional) class or whole tournament
const participants = await tournamentService.getParticipants(token, clubId, respTournamentId, classId || null);
// Emit Socket-Event
if (respTournamentId) emitTournamentChanged(clubId, respTournamentId);
res.status(200).json(participants);
} catch (error) {
console.error(error);
console.error('[addParticipant] Error:', error);
res.status(500).json({ error: error.message });
}
};
// 4. Teilnehmerliste abrufen
// 4. Teilnehmerliste abrufen - nach Klasse oder Turnier
export const getParticipants = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
const { clubId, tournamentId, classId } = req.body;
try {
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId, classId || null);
res.status(200).json(participants);
} catch (error) {
console.error(error);
@@ -60,6 +98,8 @@ export const setModus = async (req, res) => {
const { clubId, tournamentId, type, numberOfGroups, advancingPerGroup } = req.body;
try {
await tournamentService.setModus(token, clubId, tournamentId, type, numberOfGroups, advancingPerGroup);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
@@ -70,9 +110,48 @@ export const setModus = async (req, res) => {
// 6. Gruppen-Strukturen anlegen (leere Gruppen)
export const createGroups = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
const { clubId, tournamentId, numberOfGroups } = req.body;
try {
await tournamentService.createGroups(token, clubId, tournamentId);
// DEBUG: Eingehende Daten sichtbar machen (temporär)
console.log('[tournamentController.createGroups] body:', req.body);
console.log('[tournamentController.createGroups] types:', {
clubId: typeof clubId,
tournamentId: typeof tournamentId,
numberOfGroups: typeof numberOfGroups,
});
// Turniere ohne Klassen: `numberOfGroups: 0` kommt aus der UI (Default) vor.
// Statt „nichts passiert“ normalisieren wir auf mindestens 1 Gruppe.
let normalizedNumberOfGroups = numberOfGroups;
if (normalizedNumberOfGroups !== undefined && normalizedNumberOfGroups !== null) {
const n = Number(normalizedNumberOfGroups);
console.log('[tournamentController.createGroups] parsed numberOfGroups:', n);
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
return res.status(400).json({ error: 'numberOfGroups muss eine ganze Zahl >= 0 sein' });
}
normalizedNumberOfGroups = Math.max(1, n);
}
console.log('[tournamentController.createGroups] normalizedNumberOfGroups:', normalizedNumberOfGroups);
await tournamentService.createGroups(token, clubId, tournamentId, normalizedNumberOfGroups);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
};
// 6b. Gruppen-Strukturen pro Klasse anlegen
export const createGroupsPerClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, groupsPerClass } = req.body;
try {
await tournamentService.createGroupsPerClass(token, clubId, tournamentId, groupsPerClass);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
@@ -86,6 +165,8 @@ export const fillGroups = async (req, res) => {
const { clubId, tournamentId } = req.body;
try {
const updatedMembers = await tournamentService.fillGroups(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(updatedMembers);
} catch (error) {
console.error(error);
@@ -93,6 +174,21 @@ export const fillGroups = async (req, res) => {
}
};
// 7b. Gruppenspiele erstellen ohne Gruppenzuordnungen zu ändern
export const createGroupMatches = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.body;
try {
await tournamentService.createGroupMatches(token, clubId, tournamentId, classId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (error) {
console.error(error);
res.status(500).json({ error: error.message });
}
};
// 8. Gruppen mit ihren Teilnehmern abfragen
export const getGroups = async (req, res) => {
const { authcode: token } = req.headers;
@@ -119,6 +215,23 @@ export const getTournament = async (req, res) => {
}
};
// Update Turnier
export const updateTournament = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
const { name, date, winningSets } = req.body;
try {
const tournament = await tournamentService.updateTournament(token, clubId, tournamentId, name, date, winningSets);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournament);
} catch (error) {
console.error('[updateTournament] Error:', error);
const status = error.message.includes('existiert bereits') ? 400 : 500;
res.status(status).json({ error: error.message });
}
};
// 10. Alle Spiele eines Turniers abfragen
export const getTournamentMatches = async (req, res) => {
const { authcode: token } = req.headers;
@@ -138,6 +251,8 @@ export const addMatchResult = async (req, res) => {
const { clubId, tournamentId, matchId, set, result } = req.body;
try {
await tournamentService.addMatchResult(token, clubId, tournamentId, matchId, set, result);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "Result added successfully" });
} catch (error) {
console.error(error);
@@ -151,6 +266,8 @@ export const finishMatch = async (req, res) => {
const { clubId, tournamentId, matchId } = req.body;
try {
await tournamentService.finishMatch(token, clubId, tournamentId, matchId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "Match finished successfully" });
} catch (error) {
console.error(error);
@@ -164,6 +281,8 @@ export const startKnockout = async (req, res) => {
try {
await tournamentService.startKnockout(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "K.o.-Runde erfolgreich gestartet" });
} catch (error) {
const status = /Gruppenmodus|Zu wenige Qualifikanten/.test(error.message) ? 400 : 500;
@@ -190,6 +309,8 @@ export const manualAssignGroups = async (req, res) => {
numberOfGroups, // neu
maxGroupSize // neu
);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(groupsWithParts);
} catch (error) {
console.error('Error in manualAssignGroups:', error);
@@ -197,11 +318,35 @@ export const manualAssignGroups = async (req, res) => {
}
};
export const assignParticipantToGroup = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId, groupNumber, isExternal } = req.body;
try {
const groups = await tournamentService.assignParticipantToGroup(
token,
clubId,
tournamentId,
participantId,
groupNumber,
isExternal || false
);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(groups);
} catch (error) {
console.error('Error in assignParticipantToGroup:', error);
res.status(500).json({ error: error.message });
}
};
export const resetGroups = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
try {
await tournamentService.resetGroups(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (err) {
console.error(err);
@@ -211,9 +356,11 @@ export const resetGroups = async (req, res) => {
export const resetMatches = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
const { clubId, tournamentId, classId } = req.body;
try {
await tournamentService.resetMatches(token, clubId, tournamentId);
await tournamentService.resetMatches(token, clubId, tournamentId, classId || null);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.sendStatus(204);
} catch (err) {
console.error(err);
@@ -227,6 +374,8 @@ export const removeParticipant = async (req, res) => {
try {
await tournamentService.removeParticipant(token, clubId, tournamentId, participantId);
const participants = await tournamentService.getParticipants(token, clubId, tournamentId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(participants);
} catch (err) {
console.error(err);
@@ -234,6 +383,21 @@ export const removeParticipant = async (req, res) => {
}
};
export const updateParticipantSeeded = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.params;
const { seeded } = req.body;
try {
await tournamentService.updateParticipantSeeded(token, clubId, tournamentId, participantId, seeded);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Gesetzt-Status aktualisiert' });
} catch (err) {
console.error('[updateParticipantSeeded] Error:', err);
res.status(500).json({ error: err.message });
}
};
export const deleteMatchResult = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, matchId, set } = req.body;
@@ -245,6 +409,8 @@ export const deleteMatchResult = async (req, res) => {
matchId,
set
);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Einzelsatz gelöscht' });
} catch (error) {
console.error('Error in deleteMatchResult:', error);
@@ -258,6 +424,8 @@ export const reopenMatch = async (req, res) => {
const { clubId, tournamentId, matchId } = req.body;
try {
await tournamentService.reopenMatch(token, clubId, tournamentId, matchId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
// Gib optional das aktualisierte Match zurück
res.status(200).json({ message: "Match reopened" });
} catch (error) {
@@ -268,13 +436,210 @@ export const reopenMatch = async (req, res) => {
export const deleteKnockoutMatches = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.body;
const { clubId, tournamentId, classId } = req.body;
try {
await tournamentService.resetKnockout(token, clubId, tournamentId);
await tournamentService.resetKnockout(token, clubId, tournamentId, classId);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: "K.o.-Runde gelöscht" });
} catch (error) {
console.error("Error in deleteKnockoutMatches:", error);
res.status(500).json({ error: error.message });
}
};
export const setMatchActive = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, matchId } = req.params;
const { isActive } = req.body;
try {
await tournamentService.setMatchActive(token, clubId, tournamentId, matchId, isActive);
// Emit Socket-Event
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Match-Status aktualisiert' });
} catch (err) {
console.error('[setMatchActive] Error:', err);
res.status(500).json({ error: err.message });
}
};
// Externe Teilnehmer hinzufügen
export const addExternalParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId, firstName, lastName, club, birthDate, gender } = req.body;
try {
await tournamentService.addExternalParticipant(token, clubId, classId, firstName, lastName, club, birthDate, gender);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Externer Teilnehmer hinzugefügt' });
} catch (error) {
console.error('[addExternalParticipant] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Externe Teilnehmer abrufen - nach Klasse oder Turnier
export const getExternalParticipants = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.body;
try {
const participants = await tournamentService.getExternalParticipants(token, clubId, tournamentId, classId || null);
res.status(200).json(participants);
} catch (error) {
console.error('[getExternalParticipants] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Externe Teilnehmer löschen
export const removeExternalParticipant = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.body;
try {
await tournamentService.removeExternalParticipant(token, clubId, tournamentId, participantId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Externer Teilnehmer entfernt' });
} catch (error) {
console.error('[removeExternalParticipant] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Gesetzt-Status für externe Teilnehmer aktualisieren
export const updateExternalParticipantSeeded = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.params;
const { seeded } = req.body;
try {
await tournamentService.updateExternalParticipantSeeded(token, clubId, tournamentId, participantId, seeded);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Gesetzt-Status aktualisiert' });
} catch (error) {
console.error('[updateExternalParticipantSeeded] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Tournament Classes
export const getTournamentClasses = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
try {
const classes = await tournamentService.getTournamentClasses(token, clubId, tournamentId);
res.status(200).json(classes);
} catch (error) {
console.error('[getTournamentClasses] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const addTournamentClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.params;
const { name, isDoubles, gender, minBirthYear } = req.body;
try {
const tournamentClass = await tournamentService.addTournamentClass(token, clubId, tournamentId, name, isDoubles, gender, minBirthYear);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournamentClass);
} catch (error) {
console.error('[addTournamentClass] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const updateTournamentClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
const { name, sortOrder, isDoubles, gender, minBirthYear } = req.body;
try {
console.log('[updateTournamentClass] Request body:', { name, sortOrder, isDoubles, gender, minBirthYear });
const tournamentClass = await tournamentService.updateTournamentClass(token, clubId, tournamentId, classId, name, sortOrder, isDoubles, gender, minBirthYear);
console.log('[updateTournamentClass] Updated class:', JSON.stringify(tournamentClass.toJSON(), null, 2));
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(tournamentClass);
} catch (error) {
console.error('[updateTournamentClass] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const deleteTournamentClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
try {
await tournamentService.deleteTournamentClass(token, clubId, tournamentId, classId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Klasse gelöscht' });
} catch (error) {
console.error('[deleteTournamentClass] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const updateParticipantClass = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, participantId } = req.params;
const { classId, isExternal } = req.body;
try {
await tournamentService.updateParticipantClass(token, clubId, tournamentId, participantId, classId, isExternal);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Klasse aktualisiert' });
} catch (error) {
console.error('[updateParticipantClass] Error:', error);
res.status(500).json({ error: error.message });
}
};
// Tournament Pairings
export const getPairings = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
try {
const pairings = await tournamentService.getPairings(token, clubId, tournamentId, classId);
res.status(200).json(pairings);
} catch (error) {
console.error('[getPairings] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const createPairing = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, classId } = req.params;
const { player1Type, player1Id, player2Type, player2Id, seeded, groupId } = req.body;
try {
const pairing = await tournamentService.createPairing(token, clubId, tournamentId, classId, player1Type, player1Id, player2Type, player2Id, seeded, groupId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(pairing);
} catch (error) {
console.error('[createPairing] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const updatePairing = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, pairingId } = req.params;
const { player1Type, player1Id, player2Type, player2Id, seeded, groupId } = req.body;
try {
const pairing = await tournamentService.updatePairing(token, clubId, tournamentId, pairingId, player1Type, player1Id, player2Type, player2Id, seeded, groupId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json(pairing);
} catch (error) {
console.error('[updatePairing] Error:', error);
res.status(500).json({ error: error.message });
}
};
export const deletePairing = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, pairingId } = req.params;
try {
await tournamentService.deletePairing(token, clubId, tournamentId, pairingId);
emitTournamentChanged(clubId, tournamentId);
res.status(200).json({ message: 'Paarung gelöscht' });
} catch (error) {
console.error('[deletePairing] Error:', error);
res.status(500).json({ error: error.message });
}
};

View File

@@ -0,0 +1,70 @@
import tournamentService from '../services/tournamentService.js';
import HttpError from '../exceptions/HttpError.js';
export const getStages = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId } = req.query;
try {
if (clubId == null || tournamentId == null) {
return res.status(400).json({ error: 'clubId und tournamentId sind erforderlich.' });
}
const data = await tournamentService.getTournamentStages(token, Number(clubId), Number(tournamentId));
res.status(200).json(data);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
// Debug-Hilfe: zeigt, welche IDs tatsächlich am Endpoint ankamen (ohne sensible Daten)
res.set('x-debug-stages-clubid', String(clubId));
res.set('x-debug-stages-tournamentid', String(tournamentId));
res.set('x-debug-stages-clubid-num', String(Number(clubId)));
return res.status(error.statusCode || 500).json({ error: error.message });
}
res.status(500).json({ error: error.message });
}
};
export const upsertStages = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, stages, advancement, advancements } = req.body;
try {
const data = await tournamentService.upsertTournamentStages(
token,
Number(clubId),
Number(tournamentId),
stages,
advancement,
advancements
);
res.status(200).json(data);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
res.set('x-debug-stages-clubid', String(clubId));
res.set('x-debug-stages-tournamentid', String(tournamentId));
res.set('x-debug-stages-clubid-num', String(Number(clubId)));
return res.status(error.statusCode || 500).json({ error: error.message });
}
res.status(500).json({ error: error.message });
}
};
export const advanceStage = async (req, res) => {
const { authcode: token } = req.headers;
const { clubId, tournamentId, fromStageIndex, toStageIndex } = req.body;
try {
const data = await tournamentService.advanceTournamentStage(
token,
Number(clubId),
Number(tournamentId),
Number(fromStageIndex || 1),
(toStageIndex == null ? null : Number(toStageIndex))
);
res.status(200).json(data);
} catch (error) {
console.error(error);
if (error instanceof HttpError) {
return res.status(error.statusCode || 500).json({ error: error.message });
}
res.status(500).json({ error: error.message });
}
};

View File

@@ -0,0 +1,128 @@
import trainingGroupService from '../services/trainingGroupService.js';
import { getSafeErrorMessage } from '../utils/errorUtils.js';
export const getTrainingGroups = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const groups = await trainingGroupService.getTrainingGroups(userToken, clubId);
res.status(200).json(groups);
} catch (error) {
console.error('[getTrainingGroups] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingsgruppen');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const createTrainingGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const { name, sortOrder } = req.body;
const group = await trainingGroupService.createTrainingGroup(userToken, clubId, name, sortOrder);
res.status(201).json(group);
} catch (error) {
console.error('[createTrainingGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingsgruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const updateTrainingGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupId } = req.params;
const { name, sortOrder } = req.body;
const group = await trainingGroupService.updateTrainingGroup(userToken, clubId, groupId, name, sortOrder);
res.status(200).json(group);
} catch (error) {
console.error('[updateTrainingGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Trainingsgruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const deleteTrainingGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupId } = req.params;
await trainingGroupService.deleteTrainingGroup(userToken, clubId, groupId);
res.status(200).json({ success: true });
} catch (error) {
console.error('[deleteTrainingGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingsgruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const addMemberToGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupId, memberId } = req.params;
const memberGroup = await trainingGroupService.addMemberToGroup(userToken, clubId, groupId, memberId);
res.status(201).json(memberGroup);
} catch (error) {
console.error('[addMemberToGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Hinzufügen des Mitglieds zur Gruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const removeMemberFromGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, groupId, memberId } = req.params;
await trainingGroupService.removeMemberFromGroup(userToken, clubId, groupId, memberId);
res.status(200).json({ success: true });
} catch (error) {
console.error('[removeMemberFromGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Entfernen des Mitglieds aus der Gruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const getMemberGroups = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
const groups = await trainingGroupService.getMemberGroups(userToken, clubId, memberId);
res.status(200).json(groups);
} catch (error) {
console.error('[getMemberGroups] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Gruppen des Mitglieds');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const ensurePresetGroups = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const groups = await trainingGroupService.ensurePresetGroups(userToken, clubId);
res.status(200).json({
message: 'Preset-Gruppen wurden erstellt/überprüft',
groups: groups.length
});
} catch (error) {
console.error('[ensurePresetGroups] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Preset-Gruppen');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const enablePresetGroup = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, presetType } = req.params;
const group = await trainingGroupService.enablePresetGroup(userToken, clubId, presetType);
res.status(200).json({
message: 'Preset-Gruppe wurde aktiviert',
group
});
} catch (error) {
console.error('[enablePresetGroup] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Aktivieren der Preset-Gruppe');
res.status(error.statusCode || 500).json({ error: msg });
}
};

View File

@@ -0,0 +1,80 @@
import trainingTimeService from '../services/trainingTimeService.js';
import { getSafeErrorMessage } from '../utils/errorUtils.js';
export const getTrainingTimes = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const groups = await trainingTimeService.getTrainingTimes(userToken, clubId);
res.status(200).json(groups);
} catch (error) {
console.error('[getTrainingTimes] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Laden der Trainingszeiten');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const createTrainingTime = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const { trainingGroupId, weekday, startTime, endTime } = req.body;
if (!trainingGroupId || weekday === undefined || !startTime || !endTime) {
return res.status(400).json({ error: 'Alle Felder müssen ausgefüllt sein' });
}
const trainingTime = await trainingTimeService.createTrainingTime(
userToken,
clubId,
trainingGroupId,
weekday,
startTime,
endTime
);
res.status(201).json(trainingTime);
} catch (error) {
console.error('[createTrainingTime] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Erstellen der Trainingszeit');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const updateTrainingTime = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, timeId } = req.params;
const { weekday, startTime, endTime } = req.body;
const trainingTime = await trainingTimeService.updateTrainingTime(
userToken,
clubId,
timeId,
weekday,
startTime,
endTime
);
res.status(200).json(trainingTime);
} catch (error) {
console.error('[updateTrainingTime] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Aktualisieren der Trainingszeit');
res.status(error.statusCode || 500).json({ error: msg });
}
};
export const deleteTrainingTime = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, timeId } = req.params;
const result = await trainingTimeService.deleteTrainingTime(userToken, clubId, timeId);
res.status(200).json(result);
} catch (error) {
console.error('[deleteTrainingTime] - Error:', error);
const msg = getSafeErrorMessage(error, 'Fehler beim Löschen der Trainingszeit');
res.status(error.statusCode || 500).json({ error: msg });
}
};

View File

@@ -1,10 +1,54 @@
/**
* HttpError mit Unterstützung für Fehlercodes
*
* Verwendung:
* - new HttpError('Fehlermeldung', 400) - Legacy, wird weiterhin unterstützt
* - new HttpError({ code: 'ERROR_USER_NOT_FOUND' }, 404) - Mit Fehlercode
* - new HttpError({ code: 'ERROR_MEMBER_NOT_FOUND', params: { memberId: 123 } }, 404) - Mit Parametern
*/
class HttpError extends Error {
constructor(message, statusCode) {
super(message);
constructor(messageOrError, statusCode) {
// Unterstützung für beide Formate:
// 1. Legacy: new HttpError('Fehlermeldung', 400)
// 2. Neu: new HttpError({ code: 'ERROR_CODE', params: {...} }, 400)
if (typeof messageOrError === 'string') {
// Legacy-Format
super(messageOrError);
this.errorCode = null;
this.errorParams = null;
} else if (messageOrError && typeof messageOrError === 'object' && messageOrError.code) {
// Neues Format mit Fehlercode
super(messageOrError.code); // Für Stack-Trace
this.errorCode = messageOrError.code;
this.errorParams = messageOrError.params || null;
} else {
// Fallback
super('Unknown error');
this.errorCode = null;
this.errorParams = null;
}
this.name = this.constructor.name;
this.statusCode = statusCode;
this.statusCode = statusCode || 500;
Error.captureStackTrace(this, this.constructor);
}
/**
* Gibt das Fehler-Objekt für die API-Antwort zurück
* @returns {object} Fehler-Objekt mit code und optional params
*/
toJSON() {
if (this.errorCode) {
return {
code: this.errorCode,
...(this.errorParams && { params: this.errorParams })
};
}
// Legacy: Gib die Nachricht zurück
return {
message: this.message
};
}
}
export default HttpError;

View File

@@ -1,87 +1,13 @@
import ApiLog from '../models/ApiLog.js';
/**
* Middleware to log all API requests and responses
* Should be added early in the middleware chain, but after authentication
*
* HINWEIS: Logging wurde deaktiviert - keine API-Requests werden mehr geloggt
* (früher wurden nur MyTischtennis-Requests geloggt, dies wurde entfernt)
*/
export const requestLoggingMiddleware = async (req, res, next) => {
const startTime = Date.now();
const originalSend = res.send;
// Get request body (but limit size for sensitive data)
let requestBody = null;
if (req.body && Object.keys(req.body).length > 0) {
const bodyStr = JSON.stringify(req.body);
// Truncate very long bodies
requestBody = bodyStr.length > 10000 ? bodyStr.substring(0, 10000) + '... (truncated)' : bodyStr;
}
// Capture response
let responseBody = null;
res.send = function(data) {
// Try to parse response as JSON
try {
const parsed = JSON.parse(data);
const responseStr = JSON.stringify(parsed);
// Truncate very long responses
responseBody = responseStr.length > 10000 ? responseStr.substring(0, 10000) + '... (truncated)' : responseStr;
} catch (e) {
// Not JSON, just use raw data (truncated)
responseBody = typeof data === 'string' ? data.substring(0, 1000) : String(data).substring(0, 1000);
}
// Restore original send
res.send = originalSend;
return res.send.apply(res, arguments);
};
// Log after response is sent
res.on('finish', async () => {
const executionTime = Date.now() - startTime;
const ipAddress = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
const path = req.path || req.url;
// Nur myTischtennis-Requests loggen
// Skip logging for non-data endpoints (Status-Checks, Health-Checks, etc.)
// Exclude any endpoint containing 'status' or root paths
if (
path.includes('/status') ||
path === '/' ||
path === '/health' ||
path.endsWith('/status') ||
path.includes('/scheduler-status')
) {
return;
}
// Nur myTischtennis-Endpunkte loggen (z.B. /api/mytischtennis/*)
if (!path.includes('/mytischtennis')) {
return;
}
// Get user ID if available (wird von authMiddleware gesetzt)
const userId = req.user?.id || null;
try {
await ApiLog.create({
userId,
method: req.method,
path: path,
statusCode: res.statusCode,
requestBody,
responseBody,
executionTime,
errorMessage: res.statusCode >= 400 ? `HTTP ${res.statusCode}` : null,
ipAddress,
userAgent: req.headers['user-agent'],
logType: 'api_request'
});
} catch (error) {
// Don't let logging errors break the request
console.error('Error logging API request:', error);
}
});
// Logging wurde deaktiviert - keine API-Requests werden mehr geloggt
// (früher wurden nur MyTischtennis-Requests geloggt, dies wurde entfernt)
next();
};

View File

@@ -0,0 +1,58 @@
-- Adds multi-stage tournaments (rounds) support
-- MariaDB/MySQL compatible migration (manual execution)
-- 1) New table: tournament_stage
CREATE TABLE IF NOT EXISTS tournament_stage (
id INT NOT NULL AUTO_INCREMENT,
tournament_id INT NOT NULL,
stage_index INT NOT NULL,
name VARCHAR(255) NULL,
type VARCHAR(32) NOT NULL, -- 'groups' | 'knockout'
number_of_groups INT NULL,
advancing_per_group INT NULL,
max_group_size INT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT fk_tournament_stage_tournament
FOREIGN KEY (tournament_id) REFERENCES tournament(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE INDEX idx_tournament_stage_tournament_id ON tournament_stage (tournament_id);
CREATE UNIQUE INDEX uq_tournament_stage_tournament_id_index ON tournament_stage (tournament_id, stage_index);
-- 2) New table: tournament_stage_advancement
CREATE TABLE IF NOT EXISTS tournament_stage_advancement (
id INT NOT NULL AUTO_INCREMENT,
tournament_id INT NOT NULL,
from_stage_id INT NOT NULL,
to_stage_id INT NOT NULL,
mode VARCHAR(32) NOT NULL DEFAULT 'pools',
config JSON NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
CONSTRAINT fk_tournament_stage_adv_tournament
FOREIGN KEY (tournament_id) REFERENCES tournament(id)
ON DELETE CASCADE,
CONSTRAINT fk_tournament_stage_adv_from
FOREIGN KEY (from_stage_id) REFERENCES tournament_stage(id)
ON DELETE CASCADE,
CONSTRAINT fk_tournament_stage_adv_to
FOREIGN KEY (to_stage_id) REFERENCES tournament_stage(id)
ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE INDEX idx_tournament_stage_adv_tournament_id ON tournament_stage_advancement (tournament_id);
CREATE INDEX idx_tournament_stage_adv_from_stage_id ON tournament_stage_advancement (from_stage_id);
CREATE INDEX idx_tournament_stage_adv_to_stage_id ON tournament_stage_advancement (to_stage_id);
-- 3) Add stage_id to tournament_group and tournament_match
-- MariaDB has no IF NOT EXISTS for columns; run each ALTER once.
-- If you rerun, comment out the ALTERs or check INFORMATION_SCHEMA first.
ALTER TABLE tournament_group ADD COLUMN stage_id INT NULL;
ALTER TABLE tournament_match ADD COLUMN stage_id INT NULL;
CREATE INDEX idx_tournament_group_tournament_stage ON tournament_group (tournament_id, stage_id);
CREATE INDEX idx_tournament_match_tournament_stage ON tournament_match (tournament_id, stage_id);

View File

@@ -0,0 +1,16 @@
-- Allow NULL placeholders for KO (e.g. "Spiel um Platz 3")
-- MariaDB/MySQL manual migration
--
-- Background: We create placeholder matches with player1_id/player2_id = NULL.
-- Some prod DBs still have NOT NULL on these columns.
-- 1) Make player columns nullable
ALTER TABLE tournament_match MODIFY COLUMN player1_id INT NULL;
ALTER TABLE tournament_match MODIFY COLUMN player2_id INT NULL;
-- 2) (Optional) If you have foreign keys to tournament_member/external participant IDs,
-- ensure they also allow NULL. (Not adding here because not all installations have FKs.)
-- 3) Verify
-- SHOW COLUMNS FROM tournament_match LIKE 'player1_id';
-- SHOW COLUMNS FROM tournament_match LIKE 'player2_id';

View File

@@ -0,0 +1,77 @@
# Liste aller Tabellen im Trainingstagebuch-Projekt
## Basis-Tabellen
1. `user` - Benutzer
2. `user_club` - Verknüpfung Benutzer ↔ Verein
3. `user_token` - Authentifizierungs-Tokens
4. `clubs` - Vereine
5. `log` - System-Logs
## Mitglieder-Verwaltung
6. `member` - Mitglieder
7. `member_contact` - Kontaktdaten der Mitglieder (Telefon, E-Mail)
8. `member_image` - Bilder der Mitglieder
9. `member_notes` - Notizen zu Mitgliedern
10. `member_transfer_config` - Konfiguration für Mitgliederübertragung
## Trainingsgruppen (NEU)
11. `training_group` - Trainingsgruppen
12. `member_training_group` - Verknüpfung Mitglied ↔ Trainingsgruppe
13. `club_disabled_preset_groups` - Deaktivierte Preset-Gruppen pro Verein
14. `training_times` - Trainingszeiten pro Gruppe (NEU)
## Tagebuch
15. `diary_dates` - Trainingstage
16. `participants` - Teilnehmer an Trainingstagen
17. `activities` - Aktivitäten
18. `diary_notes` - Notizen zu Trainingstagen
19. `diary_tags` - Tags für Tagebuch
20. `member_diary_tags` - Verknüpfung Mitglied ↔ Tagebuch-Tag
21. `diary_date_tags` - Verknüpfung Trainingstag ↔ Tag
22. `diary_member_notes` - Notizen zu Mitgliedern an Trainingstagen
23. `diary_member_tags` - Tags für Mitglieder an Trainingstagen
24. `diary_date_activities` - Aktivitäten an Trainingstagen
25. `diary_member_activities` - Verknüpfung Teilnehmer ↔ Aktivität
26. `group` - Gruppen (für Trainingsplan)
27. `group_activity` - Gruppenaktivitäten
## Vordefinierte Aktivitäten
28. `predefined_activities` - Vordefinierte Aktivitäten
29. `predefined_activity_images` - Bilder zu vordefinierten Aktivitäten
## Unfälle
30. `accident` - Unfälle
## Teams & Ligen
31. `season` - Saisons
32. `league` - Ligen
33. `team` - Teams
34. `club_team` - Verknüpfung Verein ↔ Team
35. `team_document` - Dokumente zu Teams
36. `match` - Spiele
37. `location` - Spielorte
## Turniere
38. `tournament` - Turniere
39. `tournament_class` - Turnierklassen
40. `tournament_group` - Turniergruppen
41. `tournament_member` - Teilnehmer an Turnieren
42. `tournament_match` - Spiele in Turnieren
43. `tournament_result` - Ergebnisse von Turnierspielen
44. `external_tournament_participant` - Externe Teilnehmer an Turnieren
## Offizielle Turniere (myTischtennis)
45. `official_tournaments` - Offizielle Turniere
46. `official_competitions` - Wettbewerbe in offiziellen Turnieren
47. `official_competition_members` - Teilnehmer an offiziellen Wettbewerben
## myTischtennis Integration
48. `my_tischtennis` - myTischtennis-Verbindungen
49. `my_tischtennis_update_history` - Update-Historie
50. `my_tischtennis_fetch_log` - Fetch-Logs
## API & Logging
51. `api_log` - API-Logs
## Gesamt: 51 Tabellen

View File

@@ -0,0 +1,22 @@
-- Migration: Add 'allows_external' column to tournament table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament';
SET @columnname = 'allows_external';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `winning_sets`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,27 @@
-- Migration: Add 'class_id' column to external_tournament_participant table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'external_tournament_participant';
SET @columnname = 'class_id';
-- Check if column exists
SET @column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
);
-- Add column if it doesn't exist
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `external_tournament_participant` ADD COLUMN `class_id` INT(11) NULL AFTER `seeded`',
'SELECT 1 AS column_already_exists'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,27 @@
-- Migration: Add 'class_id' column to tournament_group table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_group';
SET @columnname = 'class_id';
-- Check if column exists
SET @column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
);
-- Add column if it doesn't exist
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `tournament_group` ADD COLUMN `class_id` INT(11) NULL AFTER `tournament_id`',
'SELECT 1 AS column_already_exists'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,27 @@
-- Migration: Add 'class_id' column to tournament_match table
-- Date: 2025-01-16
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_match';
SET @columnname = 'class_id';
-- Check if column exists
SET @column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
);
-- Add column if it doesn't exist
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `tournament_match` ADD COLUMN `class_id` INT(11) NULL AFTER `group_id`',
'SELECT 1 AS column_already_exists'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,22 @@
-- Migration: Add 'class_id' column to tournament_member table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_member';
SET @columnname = 'class_id';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` INT(11) NULL AFTER `seeded`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,8 @@
-- Migration: Geschlecht zu externen Turnierteilnehmern hinzufügen
-- Datum: 2025-01-XX
ALTER TABLE `external_tournament_participant`
ADD COLUMN `gender` ENUM('male', 'female', 'diverse', 'unknown') NULL DEFAULT 'unknown' AFTER `birth_date`;

View File

@@ -0,0 +1,8 @@
-- Migration: Geschlecht zu Turnierklassen hinzufügen
-- Datum: 2025-01-XX
ALTER TABLE `tournament_class`
ADD COLUMN `gender` ENUM('male', 'female', 'mixed') NULL DEFAULT NULL AFTER `is_doubles`;

View File

@@ -0,0 +1,22 @@
-- Migration: Add 'is_active' column to tournament_match table
-- Date: 2025-01-14
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_match';
SET @columnname = 'is_active';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `is_finished`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,7 @@
-- Migration: Add is_doubles column to tournament_class table
-- Date: 2025-01-23
-- For MariaDB/MySQL
ALTER TABLE `tournament_class`
ADD COLUMN `is_doubles` TINYINT(1) NOT NULL DEFAULT 0 AFTER `sort_order`;

View File

@@ -0,0 +1,27 @@
-- Migration: Geburtsjahr-Beschränkung zu Turnierklassen hinzufügen
-- Datum: 2025-01-XX
-- Beschreibung: Fügt max_birth_year Feld hinzu für "geboren im Jahr X oder früher" (<=)
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_class';
SET @columnname = 'max_birth_year';
-- Check if column exists
SET @column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
);
-- Add column if it doesn't exist
SET @sql = IF(@column_exists = 0,
'ALTER TABLE `tournament_class` ADD COLUMN `max_birth_year` INT(11) NULL DEFAULT NULL AFTER `gender`',
'SELECT 1 AS column_already_exists'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,29 @@
-- Migration: Add name column to tournament table
-- Date: 2025-01-13
-- For MariaDB/MySQL
-- Add name column if it doesn't exist
-- Check if column exists and add it if not
SET @dbname = DATABASE();
SET @tablename = 'tournament';
SET @columnname = 'name';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` VARCHAR(255) NOT NULL DEFAULT "" AFTER `id`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
-- Update existing tournaments: set name to formatted date if name is empty
UPDATE `tournament`
SET `name` = DATE_FORMAT(`date`, '%d.%m.%Y')
WHERE `name` = '' OR `name` IS NULL;

View File

@@ -0,0 +1,24 @@
-- Migration: Add seeded column to tournament_member table
-- Date: 2025-01-13
-- For MariaDB/MySQL
-- Add seeded column if it doesn't exist
-- Check if column exists and add it if not
SET @dbname = DATABASE();
SET @tablename = 'tournament_member';
SET @columnname = 'seeded';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` TINYINT(1) NOT NULL DEFAULT 0 AFTER `club_member_id`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,22 @@
-- Migration: Add 'winning_sets' column to tournament table
-- Date: 2025-01-14
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament';
SET @columnname = 'winning_sets';
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @columnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @columnname, '` INT NOT NULL DEFAULT 3 AFTER `advancing_per_group`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,41 @@
-- Migration: Change 'ttr' column to 'birth_date' in external_tournament_participant table
-- Date: 2025-01-15
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'external_tournament_participant';
SET @oldcolumnname = 'ttr';
SET @newcolumnname = 'birth_date';
-- Check if old column exists
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @oldcolumnname)
) > 0,
CONCAT('ALTER TABLE `', @tablename, '` CHANGE COLUMN `', @oldcolumnname, '` `', @newcolumnname, '` VARCHAR(255) NULL AFTER `club`'),
'SELECT 1'
));
PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;
-- If old column didn't exist, check if new column exists and add it if not
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @newcolumnname)
) > 0,
'SELECT 1',
CONCAT('ALTER TABLE `', @tablename, '` ADD COLUMN `', @newcolumnname, '` VARCHAR(255) NULL AFTER `club`')
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;

View File

@@ -0,0 +1,62 @@
-- Diagnose-Skript: Prüfe Seasons und Teams auf dem Server
-- Führe diese Queries auf dem Server aus, um das Problem zu identifizieren
-- 1. Prüfe, ob die season-Tabelle existiert und Daten enthält
SELECT '=== SEASONS ===' as info;
SELECT * FROM `season` ORDER BY `id` DESC;
-- 2. Prüfe, ob die club_team-Tabelle existiert und welche season_id verwendet wird
SELECT '=== CLUB_TEAMS ===' as info;
SELECT
id,
name,
club_id,
season_id,
league_id,
created_at,
updated_at
FROM `club_team`
ORDER BY `id`;
-- 3. Prüfe, ob es Teams gibt, die auf nicht-existierende Seasons verweisen
SELECT '=== TEAMS MIT FEHLENDEN SEASONS ===' as info;
SELECT
ct.id,
ct.name,
ct.season_id,
s.season
FROM `club_team` ct
LEFT JOIN `season` s ON ct.season_id = s.id
WHERE s.id IS NULL;
-- 4. Prüfe, ob es Teams gibt, die keine season_id haben
SELECT '=== TEAMS OHNE SEASON_ID ===' as info;
SELECT
id,
name,
club_id,
season_id
FROM `club_team`
WHERE season_id IS NULL;
-- 5. Prüfe die Struktur der club_team-Tabelle
SELECT '=== CLUB_TEAM TABELLENSTRUKTUR ===' as info;
DESCRIBE `club_team`;
-- 6. Prüfe die Struktur der season-Tabelle
SELECT '=== SEASON TABELLENSTRUKTUR ===' as info;
DESCRIBE `season`;
-- 7. Prüfe Foreign Key Constraints
SELECT '=== FOREIGN KEYS ===' as info;
SELECT
CONSTRAINT_NAME,
TABLE_NAME,
COLUMN_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND (TABLE_NAME = 'club_team' OR TABLE_NAME = 'season')
AND REFERENCED_TABLE_NAME IS NOT NULL;

View File

@@ -0,0 +1,30 @@
-- Vereinfachtes Diagnose-Skript: Prüfe nur die wichtigsten Punkte
-- 1. Gibt es Seasons in der Datenbank?
SELECT 'SEASONS:' as check_type, COUNT(*) as count FROM `season`;
SELECT * FROM `season` ORDER BY `id` DESC;
-- 2. Gibt es Teams in der Datenbank?
SELECT 'CLUB_TEAMS:' as check_type, COUNT(*) as count FROM `club_team`;
SELECT id, name, club_id, season_id, league_id FROM `club_team` ORDER BY `id`;
-- 3. Haben alle Teams eine season_id?
SELECT 'TEAMS OHNE SEASON_ID:' as check_type, COUNT(*) as count
FROM `club_team` WHERE season_id IS NULL;
-- 4. Verweisen alle Teams auf existierende Seasons?
SELECT 'TEAMS MIT FEHLENDEN SEASONS:' as check_type, COUNT(*) as count
FROM `club_team` ct
LEFT JOIN `season` s ON ct.season_id = s.id
WHERE s.id IS NULL;
-- 5. Welche season_id verwenden die Teams?
SELECT 'SEASON_ID VERWENDUNG:' as check_type, season_id, COUNT(*) as team_count
FROM `club_team`
GROUP BY season_id;
-- 6. Welche Seasons existieren?
SELECT 'EXISTIERENDE SEASONS:' as check_type, id, season
FROM `season`
ORDER BY id;

View File

@@ -0,0 +1,17 @@
-- Migration: Create club_disabled_preset_groups table
-- Date: 2025-01-16
-- For MariaDB/MySQL
-- Stores which preset groups are disabled for each club
CREATE TABLE IF NOT EXISTS `club_disabled_preset_groups` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`club_id` INT(11) NOT NULL,
`preset_type` ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe') NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_club_preset_type` (`club_id`, `preset_type`),
KEY `club_id` (`club_id`),
CONSTRAINT `club_disabled_preset_groups_ibfk_1` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,22 @@
-- Migration: Create external_tournament_participant table
-- Date: 2025-01-15
-- For MariaDB/MySQL
CREATE TABLE IF NOT EXISTS `external_tournament_participant` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`tournament_id` INT(11) NOT NULL,
`group_id` INT(11) NULL,
`first_name` VARCHAR(255) NOT NULL,
`last_name` VARCHAR(255) NOT NULL,
`club` VARCHAR(255) NULL,
`birth_date` VARCHAR(255) NULL,
`seeded` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
INDEX `idx_tournament_id` (`tournament_id`),
INDEX `idx_group_id` (`group_id`),
CONSTRAINT `fk_external_participant_tournament` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_external_participant_group` FOREIGN KEY (`group_id`) REFERENCES `tournament_group` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,16 @@
-- Migration: Create tournament_class table
-- Date: 2025-01-15
-- For MariaDB/MySQL
CREATE TABLE IF NOT EXISTS `tournament_class` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`tournament_id` INT(11) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `tournament_id` (`tournament_id`),
CONSTRAINT `tournament_class_ibfk_1` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,33 @@
-- Migration: Create tournament_pairing table
-- Date: 2025-01-23
-- For MariaDB/MySQL
CREATE TABLE IF NOT EXISTS `tournament_pairing` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`tournament_id` INT(11) NOT NULL,
`class_id` INT(11) NOT NULL,
`group_id` INT(11) NULL,
`member1_id` INT(11) NULL,
`external1_id` INT(11) NULL,
`member2_id` INT(11) NULL,
`external2_id` INT(11) NULL,
`seeded` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `tournament_id` (`tournament_id`),
KEY `class_id` (`class_id`),
KEY `group_id` (`group_id`),
KEY `member1_id` (`member1_id`),
KEY `member2_id` (`member2_id`),
KEY `external1_id` (`external1_id`),
KEY `external2_id` (`external2_id`),
CONSTRAINT `tournament_pairing_ibfk_1` FOREIGN KEY (`tournament_id`) REFERENCES `tournament` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `tournament_pairing_ibfk_2` FOREIGN KEY (`class_id`) REFERENCES `tournament_class` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `tournament_pairing_ibfk_3` FOREIGN KEY (`group_id`) REFERENCES `tournament_group` (`id`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,36 @@
-- Migration: Create training_group and member_training_group tables
-- Date: 2025-01-16
-- For MariaDB/MySQL
-- Create training_group table
CREATE TABLE IF NOT EXISTS `training_group` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`club_id` INT(11) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`is_preset` TINYINT(1) NOT NULL DEFAULT 0,
`preset_type` ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe') NULL,
`sort_order` INT(11) NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `club_id` (`club_id`),
CONSTRAINT `training_group_ibfk_1` FOREIGN KEY (`club_id`) REFERENCES `clubs` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Create member_training_group junction table
CREATE TABLE IF NOT EXISTS `member_training_group` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`member_id` INT(11) NOT NULL,
`training_group_id` INT(11) NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_member_group` (`member_id`, `training_group_id`),
KEY `member_id` (`member_id`),
KEY `training_group_id` (`training_group_id`),
CONSTRAINT `member_training_group_ibfk_1` FOREIGN KEY (`member_id`) REFERENCES `member` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `member_training_group_ibfk_2` FOREIGN KEY (`training_group_id`) REFERENCES `training_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,19 @@
-- Migration: Create training_times table
-- Date: 2025-01-16
-- For MariaDB/MySQL
-- Stores training times for training groups
CREATE TABLE IF NOT EXISTS `training_times` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`training_group_id` INT(11) NOT NULL,
`weekday` TINYINT(1) NOT NULL COMMENT '0 = Sunday, 1 = Monday, ..., 6 = Saturday',
`start_time` TIME NOT NULL,
`end_time` TIME NOT NULL,
`sort_order` INT(11) NOT NULL DEFAULT 0 COMMENT 'Order for displaying multiple times on the same weekday',
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY (`id`),
KEY `training_group_id` (`training_group_id`),
CONSTRAINT `training_times_ibfk_1` FOREIGN KEY (`training_group_id`) REFERENCES `training_group` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,92 @@
-- Fix-Skript: Behebt häufige Probleme mit Seasons und Teams
-- Führe dieses Skript auf dem Server aus, wenn die Diagnose Probleme zeigt
-- 1. Stelle sicher, dass die season-Tabelle existiert und die richtige Struktur hat
-- (Falls die Tabelle nicht existiert, wird sie erstellt)
CREATE TABLE IF NOT EXISTS `season` (
`id` INT NOT NULL AUTO_INCREMENT,
`season` VARCHAR(255) NOT NULL UNIQUE,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 2. Stelle sicher, dass die club_team-Tabelle die season_id-Spalte hat
-- (Falls die Spalte nicht existiert, wird sie hinzugefügt)
ALTER TABLE `club_team`
ADD COLUMN IF NOT EXISTS `season_id` INT NULL;
-- 3. Erstelle die Seasons, falls sie fehlen
INSERT IGNORE INTO `season` (`season`) VALUES ('2024/2025');
INSERT IGNORE INTO `season` (`season`) VALUES ('2025/2026');
-- 4. Aktualisiere Teams ohne season_id auf die aktuelle Saison
-- (Verwendet die neueste Saison basierend auf dem aktuellen Datum)
UPDATE `club_team`
SET `season_id` = (
SELECT `id` FROM `season`
WHERE `season` = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
)
WHERE `season_id` IS NULL;
-- 5. Falls keine aktuelle Saison existiert, erstelle sie
INSERT IGNORE INTO `season` (`season`) VALUES (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
);
-- 6. Aktualisiere Teams mit ungültigen season_id auf die aktuelle Saison
UPDATE `club_team` ct
LEFT JOIN `season` s ON ct.season_id = s.id
SET ct.season_id = (
SELECT `id` FROM `season`
WHERE `season` = (
CASE
WHEN MONTH(CURDATE()) >= 7 THEN CONCAT(YEAR(CURDATE()), '/', YEAR(CURDATE()) + 1)
ELSE CONCAT(YEAR(CURDATE()) - 1, '/', YEAR(CURDATE()))
END
)
LIMIT 1
)
WHERE s.id IS NULL;
-- 7. Füge Foreign Key Constraint hinzu, falls er fehlt
-- (Hinweis: MySQL/MariaDB unterstützt "IF NOT EXISTS" nicht für Constraints,
-- daher müssen wir prüfen, ob der Constraint bereits existiert)
SET @constraint_exists = (
SELECT COUNT(*)
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'club_team'
AND CONSTRAINT_NAME = 'club_team_season_id_foreign_idx'
AND REFERENCED_TABLE_NAME = 'season'
);
SET @sql = IF(@constraint_exists = 0,
'ALTER TABLE `club_team` ADD CONSTRAINT `club_team_season_id_foreign_idx` FOREIGN KEY (`season_id`) REFERENCES `season` (`id`) ON DELETE CASCADE ON UPDATE CASCADE',
'SELECT "Foreign key constraint already exists" as message'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 8. Zeige die Ergebnisse
SELECT '=== ERGEBNIS ===' as info;
SELECT
ct.id,
ct.name,
ct.season_id,
s.season
FROM `club_team` ct
LEFT JOIN `season` s ON ct.season_id = s.id
ORDER BY ct.id;

View File

@@ -0,0 +1,41 @@
-- Migration: Umbenennen von max_birth_year zu min_birth_year
-- Datum: 2025-01-XX
-- Beschreibung: Ändert die Logik von "geboren <= X" zu "geboren >= X"
-- For MariaDB/MySQL
SET @dbname = DATABASE();
SET @tablename = 'tournament_class';
SET @oldcolumnname = 'max_birth_year';
SET @newcolumnname = 'min_birth_year';
-- Check if old column exists
SET @old_column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @oldcolumnname)
);
-- Check if new column already exists
SET @new_column_exists = (
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(TABLE_SCHEMA = @dbname)
AND (TABLE_NAME = @tablename)
AND (COLUMN_NAME = @newcolumnname)
);
-- Rename column if old exists and new doesn't
SET @sql = IF(@old_column_exists > 0 AND @new_column_exists = 0,
CONCAT('ALTER TABLE `', @tablename, '` CHANGE COLUMN `', @oldcolumnname, '` `', @newcolumnname, '` INT(11) NULL DEFAULT NULL AFTER `gender`'),
IF(@new_column_exists > 0,
'SELECT 1 AS column_already_renamed',
'SELECT 1 AS old_column_not_found'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

View File

@@ -0,0 +1,39 @@
-- Migration: Update my_tischtennis table TEXT fields to LONGTEXT for encrypted data
-- Date: 2025-11-21
-- For MariaDB/MySQL
--
-- Problem: Encrypted data can be very long, and TEXT fields (max 65KB) are too small
-- Solution: Change to LONGTEXT (max 4GB) for all encrypted fields
-- Update user_data to LONGTEXT
ALTER TABLE `my_tischtennis`
MODIFY COLUMN `user_data` LONGTEXT NULL;
-- Update access_token to LONGTEXT
ALTER TABLE `my_tischtennis`
MODIFY COLUMN `access_token` LONGTEXT NULL;
-- Update refresh_token to LONGTEXT
ALTER TABLE `my_tischtennis`
MODIFY COLUMN `refresh_token` LONGTEXT NULL;
-- Update cookie to LONGTEXT
ALTER TABLE `my_tischtennis`
MODIFY COLUMN `cookie` LONGTEXT NULL;
-- Update encrypted_password to LONGTEXT
ALTER TABLE `my_tischtennis`
MODIFY COLUMN `encrypted_password` LONGTEXT NULL;
-- Update club_id to LONGTEXT (was VARCHAR, but encrypted data can be longer)
ALTER TABLE `my_tischtennis`
MODIFY COLUMN `club_id` LONGTEXT NULL;
-- Update club_name to LONGTEXT (was VARCHAR, but encrypted data can be longer)
ALTER TABLE `my_tischtennis`
MODIFY COLUMN `club_name` LONGTEXT NULL;
-- Update fed_nickname to LONGTEXT (was VARCHAR, but encrypted data can be longer)
ALTER TABLE `my_tischtennis`
MODIFY COLUMN `fed_nickname` LONGTEXT NULL;

View File

@@ -0,0 +1,33 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
const ClubDisabledPresetGroup = sequelize.define('ClubDisabledPresetGroup', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
},
presetType: {
type: DataTypes.ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe'),
allowNull: false,
comment: 'Type of preset group that is disabled for this club'
}
}, {
tableName: 'club_disabled_preset_groups',
underscored: true,
timestamps: true,
});
export default ClubDisabledPresetGroup;

View File

@@ -0,0 +1,94 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const ExternalTournamentParticipant = sequelize.define('ExternalTournamentParticipant', {
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
},
groupId: {
type: DataTypes.INTEGER,
autoIncrement: false,
allowNull: true
},
firstName: {
type: DataTypes.STRING,
allowNull: false,
set(value) {
const encryptedValue = encryptData(value);
this.setDataValue('firstName', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('firstName');
return decryptData(encryptedValue);
}
},
lastName: {
type: DataTypes.STRING,
allowNull: false,
set(value) {
const encryptedValue = encryptData(value);
this.setDataValue('lastName', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('lastName');
return decryptData(encryptedValue);
}
},
club: {
type: DataTypes.STRING,
allowNull: true,
set(value) {
if (!value) {
this.setDataValue('club', null);
return;
}
const encryptedValue = encryptData(value);
this.setDataValue('club', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('club');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
birthDate: {
type: DataTypes.STRING,
allowNull: true,
set(value) {
if (!value) {
this.setDataValue('birthDate', null);
return;
}
const encryptedValue = encryptData(value || '');
this.setDataValue('birthDate', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('birthDate');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
gender: {
type: DataTypes.ENUM('male', 'female', 'diverse', 'unknown'),
allowNull: true,
defaultValue: 'unknown'
},
seeded: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
classId: {
type: DataTypes.INTEGER,
allowNull: true
}
}, {
underscored: true,
tableName: 'external_tournament_participant',
timestamps: true,
});
export default ExternalTournamentParticipant;

View File

@@ -0,0 +1,40 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Member from './Member.js';
import TrainingGroup from './TrainingGroup.js';
const MemberTrainingGroup = sequelize.define('MemberTrainingGroup', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
memberId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Member,
key: 'id',
},
onDelete: 'CASCADE',
},
trainingGroupId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: TrainingGroup,
key: 'id',
},
onDelete: 'CASCADE',
}
}, {
tableName: 'member_training_group',
underscored: true,
timestamps: true,
});
export default MemberTrainingGroup;

View File

@@ -22,9 +22,17 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
email: {
type: DataTypes.STRING,
allowNull: false,
set(value) {
const encryptedValue = encryptData(value);
this.setDataValue('email', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('email');
return decryptData(encryptedValue);
}
},
encryptedPassword: {
type: DataTypes.TEXT,
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data
allowNull: true,
field: 'encrypted_password'
},
@@ -41,14 +49,40 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
field: 'auto_update_ratings'
},
accessToken: {
type: DataTypes.TEXT,
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data
allowNull: true,
field: 'access_token'
field: 'access_token',
set(value) {
if (value === null || value === undefined) {
this.setDataValue('accessToken', null);
} else {
const encryptedValue = encryptData(value);
this.setDataValue('accessToken', encryptedValue);
}
},
get() {
const encryptedValue = this.getDataValue('accessToken');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
refreshToken: {
type: DataTypes.TEXT,
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data
allowNull: true,
field: 'refresh_token'
field: 'refresh_token',
set(value) {
if (value === null || value === undefined) {
this.setDataValue('refreshToken', null);
} else {
const encryptedValue = encryptData(value);
this.setDataValue('refreshToken', encryptedValue);
}
},
get() {
const encryptedValue = this.getDataValue('refreshToken');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
expiresAt: {
type: DataTypes.BIGINT,
@@ -56,28 +90,100 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
field: 'expires_at'
},
cookie: {
type: DataTypes.TEXT,
allowNull: true
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data
allowNull: true,
set(value) {
if (value === null || value === undefined) {
this.setDataValue('cookie', null);
} else {
const encryptedValue = encryptData(value);
this.setDataValue('cookie', encryptedValue);
}
},
get() {
const encryptedValue = this.getDataValue('cookie');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
userData: {
type: DataTypes.JSON,
type: DataTypes.TEXT('long'), // Use LONGTEXT to support very long encrypted strings
allowNull: true,
field: 'user_data'
field: 'user_data',
set(value) {
if (value === null || value === undefined) {
this.setDataValue('userData', null);
} else {
const jsonString = typeof value === 'string' ? value : JSON.stringify(value);
const encryptedValue = encryptData(jsonString);
this.setDataValue('userData', encryptedValue);
}
},
get() {
const encryptedValue = this.getDataValue('userData');
if (!encryptedValue) return null;
try {
const decryptedString = decryptData(encryptedValue);
return JSON.parse(decryptedString);
} catch (error) {
console.error('Error decrypting/parsing userData:', error);
return null;
}
}
},
clubId: {
type: DataTypes.STRING,
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data (can be longer than VARCHAR)
allowNull: true,
field: 'club_id'
field: 'club_id',
set(value) {
if (value === null || value === undefined) {
this.setDataValue('clubId', null);
} else {
const encryptedValue = encryptData(value);
this.setDataValue('clubId', encryptedValue);
}
},
get() {
const encryptedValue = this.getDataValue('clubId');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
clubName: {
type: DataTypes.STRING,
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data (can be longer than VARCHAR)
allowNull: true,
field: 'club_name'
field: 'club_name',
set(value) {
if (value === null || value === undefined) {
this.setDataValue('clubName', null);
} else {
const encryptedValue = encryptData(value);
this.setDataValue('clubName', encryptedValue);
}
},
get() {
const encryptedValue = this.getDataValue('clubName');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
fedNickname: {
type: DataTypes.STRING,
type: DataTypes.TEXT('long'), // Use LONGTEXT for encrypted data (can be longer than VARCHAR)
allowNull: true,
field: 'fed_nickname'
field: 'fed_nickname',
set(value) {
if (value === null || value === undefined) {
this.setDataValue('fedNickname', null);
} else {
const encryptedValue = encryptData(value);
this.setDataValue('fedNickname', encryptedValue);
}
},
get() {
const encryptedValue = this.getDataValue('fedNickname');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
lastLoginAttempt: {
type: DataTypes.DATE,

View File

@@ -17,6 +17,7 @@ const Tournament = sequelize.define('Tournament', {
advancingPerGroup: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 1,
},
numberOfGroups: {
type: DataTypes.INTEGER,
@@ -28,7 +29,16 @@ const Tournament = sequelize.define('Tournament', {
allowNull: false,
defaultValue: 1
},
advancingPerGroup: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 1 },
winningSets: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 3,
},
allowsExternal: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
}, {
underscored: true,
tableName: 'tournament',

View File

@@ -0,0 +1,55 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Tournament from './Tournament.js';
const TournamentClass = sequelize.define('TournamentClass', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Tournament,
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
name: {
type: DataTypes.STRING,
allowNull: false
},
sortOrder: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
isDoubles: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
gender: {
type: DataTypes.ENUM('male', 'female', 'mixed'),
allowNull: true,
defaultValue: null
},
minBirthYear: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null,
field: 'min_birth_year',
comment: 'Geboren im Jahr X oder später (>=)'
}
}, {
underscored: true,
tableName: 'tournament_class',
timestamps: true
});
export default TournamentClass;

View File

@@ -8,10 +8,18 @@ const TournamentGroup = sequelize.define('TournamentGroup', {
autoIncrement: true,
allowNull: false
},
stageId: {
type: DataTypes.INTEGER,
allowNull: true,
},
tournamentId : {
type: DataTypes.INTEGER,
allowNull: false
},
classId: {
type: DataTypes.INTEGER,
allowNull: true
},
}, {
underscored: true,
tableName: 'tournament_group',

View File

@@ -5,6 +5,10 @@ import Tournament from './Tournament.js';
import TournamentGroup from './TournamentGroup.js';
const TournamentMatch = sequelize.define('TournamentMatch', {
stageId: {
type: DataTypes.INTEGER,
allowNull: true,
},
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
@@ -25,6 +29,10 @@ const TournamentMatch = sequelize.define('TournamentMatch', {
onDelete: 'SET NULL',
onUpdate: 'CASCADE'
},
classId: {
type: DataTypes.INTEGER,
allowNull: true,
},
groupRound: {
type: DataTypes.INTEGER,
allowNull: true,
@@ -35,17 +43,22 @@ const TournamentMatch = sequelize.define('TournamentMatch', {
},
player1Id: {
type: DataTypes.INTEGER,
allowNull: false,
allowNull: true,
},
player2Id: {
type: DataTypes.INTEGER,
allowNull: false,
allowNull: true,
},
isFinished: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
isActive: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
result: {
type: DataTypes.STRING,
allowNull: true,

View File

@@ -16,6 +16,15 @@ const TournamentMember = sequelize.define('TournamentMember', {
type: DataTypes.INTEGER,
autoIncrement: false,
allowNull: false
},
seeded: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
},
classId: {
type: DataTypes.INTEGER,
allowNull: true
}
}, {
underscored: true,

View File

@@ -0,0 +1,71 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Tournament from './Tournament.js';
import TournamentClass from './TournamentClass.js';
const TournamentPairing = sequelize.define('TournamentPairing', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Tournament,
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
classId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: TournamentClass,
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
groupId: {
type: DataTypes.INTEGER,
allowNull: true
},
// Player 1: entweder Mitglied oder externer Teilnehmer
member1Id: {
type: DataTypes.INTEGER,
allowNull: true
},
external1Id: {
type: DataTypes.INTEGER,
allowNull: true
},
// Player 2: entweder Mitglied oder externer Teilnehmer
member2Id: {
type: DataTypes.INTEGER,
allowNull: true
},
external2Id: {
type: DataTypes.INTEGER,
allowNull: true
},
seeded: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false
}
}, {
underscored: true,
tableName: 'tournament_pairing',
timestamps: true
});
export default TournamentPairing;

View File

@@ -0,0 +1,46 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const TournamentStage = sequelize.define('TournamentStage', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
},
index: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'stage_index',
},
name: {
type: DataTypes.STRING,
allowNull: true,
},
type: {
type: DataTypes.STRING,
allowNull: false, // 'groups' | 'knockout'
},
numberOfGroups: {
type: DataTypes.INTEGER,
allowNull: true,
},
advancingPerGroup: {
type: DataTypes.INTEGER,
allowNull: true,
},
maxGroupSize: {
type: DataTypes.INTEGER,
allowNull: true,
},
}, {
underscored: true,
tableName: 'tournament_stage',
timestamps: true,
});
export default TournamentStage;

View File

@@ -0,0 +1,40 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const TournamentStageAdvancement = sequelize.define('TournamentStageAdvancement', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
tournamentId: {
type: DataTypes.INTEGER,
allowNull: false,
},
fromStageId: {
type: DataTypes.INTEGER,
allowNull: false,
},
toStageId: {
type: DataTypes.INTEGER,
allowNull: false,
},
mode: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'pools',
},
config: {
// JSON: { pools: [{ fromPlaces:[1,2], target:{ type:'groups', groupCount:2 }}, ...] }
type: DataTypes.JSON,
allowNull: false,
defaultValue: {},
},
}, {
underscored: true,
tableName: 'tournament_stage_advancement',
timestamps: true,
});
export default TournamentStageAdvancement;

View File

@@ -0,0 +1,51 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
const TrainingGroup = sequelize.define('TrainingGroup', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: Club,
key: 'id',
},
onDelete: 'CASCADE',
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
isPreset: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'True if this is a preset group (Anfänger, Fortgeschrittene, etc.)'
},
presetType: {
type: DataTypes.ENUM('anfaenger', 'fortgeschrittene', 'erwachsene', 'nachwuchs', 'leistungsgruppe'),
allowNull: true,
comment: 'Type of preset group'
},
sortOrder: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Order for displaying groups'
}
}, {
tableName: 'training_group',
underscored: true,
timestamps: true,
});
export default TrainingGroup;

View File

@@ -0,0 +1,47 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import TrainingGroup from './TrainingGroup.js';
const TrainingTime = sequelize.define('TrainingTime', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
trainingGroupId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: TrainingGroup,
key: 'id',
},
onDelete: 'CASCADE',
},
weekday: {
type: DataTypes.INTEGER,
allowNull: false,
comment: '0 = Sunday, 1 = Monday, ..., 6 = Saturday'
},
startTime: {
type: DataTypes.TIME,
allowNull: false,
},
endTime: {
type: DataTypes.TIME,
allowNull: false,
},
sortOrder: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
comment: 'Order for displaying multiple times on the same weekday'
}
}, {
tableName: 'training_times',
underscored: true,
timestamps: true,
});
export default TrainingTime;

View File

@@ -27,9 +27,14 @@ import Group from './Group.js';
import GroupActivity from './GroupActivity.js';
import Tournament from './Tournament.js';
import TournamentGroup from './TournamentGroup.js';
import TournamentClass from './TournamentClass.js';
import TournamentMember from './TournamentMember.js';
import TournamentMatch from './TournamentMatch.js';
import TournamentResult from './TournamentResult.js';
import ExternalTournamentParticipant from './ExternalTournamentParticipant.js';
import TournamentPairing from './TournamentPairing.js';
import TournamentStage from './TournamentStage.js';
import TournamentStageAdvancement from './TournamentStageAdvancement.js';
import Accident from './Accident.js';
import UserToken from './UserToken.js';
import OfficialTournament from './OfficialTournament.js';
@@ -42,6 +47,10 @@ import ApiLog from './ApiLog.js';
import MemberTransferConfig from './MemberTransferConfig.js';
import MemberContact from './MemberContact.js';
import MemberImage from './MemberImage.js';
import TrainingGroup from './TrainingGroup.js';
import MemberTrainingGroup from './MemberTrainingGroup.js';
import ClubDisabledPresetGroup from './ClubDisabledPresetGroup.js';
import TrainingTime from './TrainingTime.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
@@ -185,6 +194,13 @@ Club.hasMany(Tournament, { foreignKey: 'clubId', as: 'tournaments' });
TournamentGroup.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournaments' });
Tournament.hasMany(TournamentGroup, { foreignKey: 'tournamentId', as: 'tournamentGroups' });
// Tournament Stages
TournamentStage.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentStage, { foreignKey: 'tournamentId', as: 'stages' });
TournamentStageAdvancement.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentStageAdvancement, { foreignKey: 'tournamentId', as: 'stageAdvancements' });
TournamentMember.belongsTo(TournamentGroup, {
foreignKey: 'groupId',
targetKey: 'id',
@@ -201,6 +217,15 @@ Member.hasMany(TournamentMember, { foreignKey: 'clubMemberId', as: 'tournamentGr
TournamentMember.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentMember, { foreignKey: 'tournamentId', as: 'tournamentMembers' });
TournamentMember.belongsTo(TournamentClass, {
foreignKey: 'classId',
as: 'class',
constraints: false
});
TournamentClass.hasMany(TournamentMember, {
foreignKey: 'classId',
as: 'members'
});
TournamentMatch.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentMatch, { foreignKey: 'tournamentId', as: 'tournamentMatches' });
@@ -227,6 +252,68 @@ TournamentMatch.belongsTo(TournamentMember, { foreignKey: 'player2Id', as: 'play
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player1Id', as: 'player1Matches' });
TournamentMember.hasMany(TournamentMatch, { foreignKey: 'player2Id', as: 'player2Matches' });
// Tournament Classes
TournamentClass.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentClass, { foreignKey: 'tournamentId', as: 'classes' });
// External Tournament Participants
ExternalTournamentParticipant.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(ExternalTournamentParticipant, { foreignKey: 'tournamentId', as: 'externalParticipants' });
ExternalTournamentParticipant.belongsTo(TournamentGroup, {
foreignKey: 'groupId',
targetKey: 'id',
as: 'group',
constraints: false
});
TournamentGroup.hasMany(ExternalTournamentParticipant, {
foreignKey: 'groupId',
as: 'externalGroupMembers'
});
ExternalTournamentParticipant.belongsTo(TournamentClass, {
foreignKey: 'classId',
as: 'class',
constraints: false
});
TournamentClass.hasMany(ExternalTournamentParticipant, {
foreignKey: 'classId',
as: 'externalParticipants'
});
// Tournament Pairings
TournamentPairing.belongsTo(Tournament, { foreignKey: 'tournamentId', as: 'tournament' });
Tournament.hasMany(TournamentPairing, { foreignKey: 'tournamentId', as: 'pairings' });
TournamentPairing.belongsTo(TournamentClass, { foreignKey: 'classId', as: 'class' });
TournamentClass.hasMany(TournamentPairing, { foreignKey: 'classId', as: 'pairings' });
TournamentPairing.belongsTo(TournamentGroup, {
foreignKey: 'groupId',
as: 'group',
constraints: false
});
TournamentGroup.hasMany(TournamentPairing, {
foreignKey: 'groupId',
as: 'pairings'
});
TournamentPairing.belongsTo(TournamentMember, {
foreignKey: 'member1Id',
as: 'member1',
constraints: false
});
TournamentPairing.belongsTo(TournamentMember, {
foreignKey: 'member2Id',
as: 'member2',
constraints: false
});
TournamentPairing.belongsTo(ExternalTournamentParticipant, {
foreignKey: 'external1Id',
as: 'external1',
constraints: false
});
TournamentPairing.belongsTo(ExternalTournamentParticipant, {
foreignKey: 'external2Id',
as: 'external2',
constraints: false
});
Accident.belongsTo(Member, { foreignKey: 'memberId', as: 'members' });
Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
@@ -254,6 +341,31 @@ MemberContact.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
Member.hasMany(MemberImage, { foreignKey: 'memberId', as: 'images' });
MemberImage.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
// Training Groups
Club.hasMany(TrainingGroup, { foreignKey: 'clubId', as: 'trainingGroups' });
TrainingGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Member.belongsToMany(TrainingGroup, {
through: MemberTrainingGroup,
foreignKey: 'memberId',
otherKey: 'trainingGroupId',
as: 'trainingGroups'
});
TrainingGroup.belongsToMany(Member, {
through: MemberTrainingGroup,
foreignKey: 'trainingGroupId',
otherKey: 'memberId',
as: 'members'
});
// Club Disabled Preset Groups
Club.hasMany(ClubDisabledPresetGroup, { foreignKey: 'clubId', as: 'disabledPresetGroups' });
ClubDisabledPresetGroup.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
// Training Times
TrainingGroup.hasMany(TrainingTime, { foreignKey: 'trainingGroupId', as: 'trainingTimes' });
TrainingTime.belongsTo(TrainingGroup, { foreignKey: 'trainingGroupId', as: 'trainingGroup' });
export {
User,
Log,
@@ -283,9 +395,12 @@ export {
GroupActivity,
Tournament,
TournamentGroup,
TournamentClass,
TournamentMember,
TournamentMatch,
TournamentResult,
ExternalTournamentParticipant,
TournamentPairing,
Accident,
UserToken,
OfficialTournament,
@@ -298,4 +413,8 @@ export {
MemberTransferConfig,
MemberContact,
MemberImage,
TrainingGroup,
MemberTrainingGroup,
ClubDisabledPresetGroup,
TrainingTime,
};

View File

@@ -432,6 +432,12 @@
"linux"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"dev": true,
@@ -455,6 +461,15 @@
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"license": "MIT",
@@ -807,6 +822,15 @@
],
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/bcrypt": {
"version": "5.1.1",
"hasInstallScript": true,
@@ -1490,6 +1514,67 @@
"once": "^1.4.0"
}
},
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/env-paths": {
"version": "2.2.1",
"dev": true,
@@ -2585,7 +2670,9 @@
"license": "ISC"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -4305,6 +4392,116 @@
"npm": ">= 3.0.0"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-adapter/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socks": {
"version": "2.8.7",
"dev": true,
@@ -5191,6 +5388,27 @@
"version": "1.0.2",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"license": "MIT",

View File

@@ -28,7 +28,8 @@
"pdf-parse": "^1.1.1",
"pdfjs-dist": "^5.4.394",
"sequelize": "^6.37.3",
"sharp": "^0.33.5"
"sharp": "^0.33.5",
"socket.io": "^4.8.1"
},
"devDependencies": {
"cross-env": "^7.0.3",
@@ -467,6 +468,12 @@
"linux"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"dev": true,
@@ -490,6 +497,15 @@
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/cors": {
"version": "2.8.19",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"license": "MIT",
@@ -842,6 +858,15 @@
],
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/bcrypt": {
"version": "5.1.1",
"hasInstallScript": true,
@@ -1525,6 +1550,67 @@
"once": "^1.4.0"
}
},
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/env-paths": {
"version": "2.2.1",
"dev": true,
@@ -2620,7 +2706,9 @@
"license": "ISC"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -4340,6 +4428,116 @@
"npm": ">= 3.0.0"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-adapter/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/socks": {
"version": "2.8.7",
"dev": true,
@@ -5226,6 +5424,27 @@
"version": "1.0.2",
"license": "ISC"
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"license": "MIT",

View File

@@ -33,7 +33,8 @@
"pdf-parse": "^1.1.1",
"pdfjs-dist": "^5.4.394",
"sequelize": "^6.37.3",
"sharp": "^0.33.5"
"sharp": "^0.33.5",
"socket.io": "^4.8.1"
},
"devDependencies": {
"cross-env": "^7.0.3",

View File

@@ -6,6 +6,7 @@ import {
updateDiaryDateActivityOrder,
getDiaryDateActivities,
addGroupActivity,
updateGroupActivity,
deleteGroupActivity,
} from '../controllers/diaryDateActivityController.js';
import { authenticate } from '../middleware/authMiddleware.js';
@@ -15,6 +16,7 @@ const router = express.Router();
router.use(authenticate);
router.post('/group', addGroupActivity);
router.put('/group/:clubId/:groupActivityId', updateGroupActivity);
router.delete('/group/:clubId/:groupActivityId', deleteGroupActivity);
router.post('/:clubId/', createDiaryDateActivity);
router.put('/:clubId/:id/order', updateDiaryDateActivityOrder);

View File

@@ -1,6 +1,6 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import { addGroup, getGroups, changeGroup } from '../controllers/groupController.js';
import { addGroup, getGroups, changeGroup, deleteGroup } from '../controllers/groupController.js';
const router = express.Router();
@@ -9,5 +9,6 @@ router.use(authenticate);
router.post('/', addGroup);
router.get('/:clubId/:dateId', getGroups);
router.put('/:groupId', changeGroup);
router.delete('/:groupId', deleteGroup);
export default router;

View File

@@ -6,7 +6,17 @@ import { authorize } from '../middleware/authorizationMiddleware.js';
const router = express.Router();
// All routes require authentication
// Login-Page und Login-Submit müssen VOR authenticate stehen, da iframe keinen Token mitsenden kann
// GET /api/mytischtennis/login-page - Proxy für Login-Seite (für iframe)
router.get('/login-page', myTischtennisController.getLoginPage);
// POST /api/mytischtennis/login-submit - Proxy für Login-Form-Submission
router.post('/login-submit', myTischtennisController.submitLogin);
// POST /api/mytischtennis/extract-session - Extrahiere Session nach Login im iframe
router.post('/extract-session', myTischtennisController.extractSession);
// All other routes require authentication
router.use(authenticate);
// GET /api/mytischtennis/account - Get account (alle dürfen lesen)

View File

@@ -2,11 +2,13 @@ import express from 'express';
import {
getTournaments,
addTournament,
updateTournament,
addParticipant,
getParticipants,
setModus,
createGroups,
fillGroups,
createGroupMatches,
getGroups,
getTournament,
getTournamentMatches,
@@ -17,10 +19,32 @@ import {
resetGroups,
resetMatches,
removeParticipant,
updateParticipantSeeded,
deleteMatchResult,
reopenMatch,
deleteKnockoutMatches,
setMatchActive,
addExternalParticipant,
getExternalParticipants,
removeExternalParticipant,
updateExternalParticipantSeeded,
getTournamentClasses,
addTournamentClass,
updateTournamentClass,
deleteTournamentClass,
updateParticipantClass,
createGroupsPerClass,
assignParticipantToGroup,
getPairings,
createPairing,
updatePairing,
deletePairing,
} from '../controllers/tournamentController.js';
import {
getStages,
upsertStages,
advanceStage,
} from '../controllers/tournamentStagesController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
@@ -28,22 +52,53 @@ const router = express.Router();
router.post('/participant', authenticate, addParticipant);
router.post('/participants', authenticate, getParticipants);
router.delete('/participant', authenticate, removeParticipant);
router.put('/participant/:clubId/:tournamentId/:participantId/seeded', authenticate, updateParticipantSeeded);
router.post('/modus', authenticate, setModus);
router.post('/groups/reset', authenticate, resetGroups);
router.post('/matches/reset', authenticate, resetMatches);
router.put('/groups', authenticate, createGroups);
router.post('/groups/create', authenticate, createGroupsPerClass);
router.post('/groups', authenticate, fillGroups);
router.post('/matches/create', authenticate, createGroupMatches);
router.get('/groups', authenticate, getGroups);
router.post('/match/result', authenticate, addMatchResult);
router.delete('/match/result', authenticate, deleteMatchResult);
router.post("/match/reopen", reopenMatch);
router.post("/match/reopen", authenticate, reopenMatch);
router.post('/match/finish', authenticate, finishMatch);
router.put('/match/:clubId/:tournamentId/:matchId/active', authenticate, setMatchActive);
router.get('/matches/:clubId/:tournamentId', authenticate, getTournamentMatches);
router.get('/:clubId/:tournamentId', authenticate, getTournament);
router.get('/:clubId', authenticate, getTournaments);
router.post('/knockout', authenticate, startKnockout);
router.delete("/matches/knockout", deleteKnockoutMatches);
router.delete("/matches/knockout", authenticate, deleteKnockoutMatches);
router.post('/groups/manual', authenticate, manualAssignGroups);
router.put('/participant/group', authenticate, assignParticipantToGroup); // Muss VOR /:clubId/:tournamentId stehen!
router.put('/:clubId/:tournamentId', authenticate, updateTournament);
router.get('/:clubId/:tournamentId', authenticate, getTournament);
// Externe Teilnehmer
router.post('/external-participant', authenticate, addExternalParticipant);
router.post('/external-participants', authenticate, getExternalParticipants);
router.delete('/external-participant', authenticate, removeExternalParticipant);
router.put('/external-participant/:clubId/:tournamentId/:participantId/seeded', authenticate, updateExternalParticipantSeeded);
// Tournament Classes
router.get('/classes/:clubId/:tournamentId', authenticate, getTournamentClasses);
router.post('/class/:clubId/:tournamentId', authenticate, addTournamentClass);
router.put('/class/:clubId/:tournamentId/:classId', authenticate, updateTournamentClass);
router.delete('/class/:clubId/:tournamentId/:classId', authenticate, deleteTournamentClass);
router.put('/participant/:clubId/:tournamentId/:participantId/class', authenticate, updateParticipantClass);
// Tournament Pairings
router.get('/pairings/:clubId/:tournamentId/:classId', authenticate, getPairings);
router.post('/pairing/:clubId/:tournamentId/:classId', authenticate, createPairing);
router.put('/pairing/:clubId/:tournamentId/:pairingId', authenticate, updatePairing);
router.delete('/pairing/:clubId/:tournamentId/:pairingId', authenticate, deletePairing);
// Tournament Stages (mehrere Runden)
router.get('/stages', authenticate, getStages);
router.put('/stages', authenticate, upsertStages);
router.post('/stages/advance', authenticate, advanceStage);
// Muss NACH allen festen Pfaden stehen, sonst matcht z.B. '/stages' als clubId='stages'
router.get('/:clubId', authenticate, getTournaments);
router.post('/', authenticate, addTournament);
export default router;

View File

@@ -0,0 +1,33 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getTrainingGroups,
createTrainingGroup,
updateTrainingGroup,
deleteTrainingGroup,
addMemberToGroup,
removeMemberFromGroup,
getMemberGroups,
ensurePresetGroups,
enablePresetGroup,
} from '../controllers/trainingGroupController.js';
const router = express.Router();
router.use(authenticate);
// Spezifischere Routen zuerst (mit /member/ im Pfad)
router.get('/:clubId/member/:memberId', getMemberGroups);
router.post('/:clubId/:groupId/member/:memberId', addMemberToGroup);
router.delete('/:clubId/:groupId/member/:memberId', removeMemberFromGroup);
// Allgemeinere Routen danach
router.post('/:clubId/ensure-preset-groups', ensurePresetGroups);
router.post('/:clubId/enable-preset-group/:presetType', enablePresetGroup);
router.get('/:clubId', getTrainingGroups);
router.post('/:clubId', createTrainingGroup);
router.put('/:clubId/:groupId', updateTrainingGroup);
router.delete('/:clubId/:groupId', deleteTrainingGroup);
export default router;

View File

@@ -0,0 +1,19 @@
import express from 'express';
import { authenticate } from '../middleware/authMiddleware.js';
import {
getTrainingTimes,
createTrainingTime,
updateTrainingTime,
deleteTrainingTime
} from '../controllers/trainingTimeController.js';
const router = express.Router();
router.use(authenticate);
router.get('/:clubId', getTrainingTimes);
router.post('/:clubId', createTrainingTime);
router.put('/:clubId/:timeId', updateTrainingTime);
router.delete('/:clubId/:timeId', deleteTrainingTime);
export default router;

View File

@@ -0,0 +1,107 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { development } from '../config.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load .env from backend directory
dotenv.config({ path: join(__dirname, '..', '.env') });
const dbConfig = {
host: process.env.DB_HOST || development.host || 'localhost',
user: process.env.DB_USER || development.username || 'root',
password: process.env.DB_PASSWORD || development.password || '',
database: process.env.DB_NAME || development.database || 'trainingdiary',
};
async function checkDatabase() {
let connection;
try {
console.log('=== Datenbank-Konfiguration ===');
console.log('Host:', dbConfig.host);
console.log('User:', dbConfig.user);
console.log('Database:', dbConfig.database);
console.log('Password:', dbConfig.password ? '***' : '(leer)');
console.log('');
connection = await mysql.createConnection({
host: dbConfig.host,
user: dbConfig.user,
password: dbConfig.password,
});
// Zeige alle verfügbaren Datenbanken
const [databases] = await connection.execute('SHOW DATABASES');
console.log('=== Verfügbare Datenbanken ===');
databases.forEach(db => {
const dbName = db.Database;
const isCurrent = dbName === dbConfig.database;
console.log(`${isCurrent ? '→' : ' '} ${dbName}${isCurrent ? ' (VERWENDET)' : ''}`);
});
console.log('');
// Verbinde mit der konfigurierten Datenbank
await connection.end();
connection = await mysql.createConnection(dbConfig);
// Prüfe, ob die Tabellen existieren
const [tables] = await connection.execute('SHOW TABLES');
console.log(`=== Tabellen in "${dbConfig.database}" ===`);
console.log(`Anzahl: ${tables.length}`);
console.log('');
// Prüfe spezifische Tabellen
const tableNames = tables.map(t => Object.values(t)[0]);
const requiredTables = [
'training_group',
'member_training_group',
'club_disabled_preset_groups',
'training_times',
'official_tournaments',
];
console.log('=== Prüfung wichtiger Tabellen ===');
for (const table of requiredTables) {
const exists = tableNames.includes(table);
console.log(`${exists ? '✓' : '✗'} ${table}`);
}
console.log('');
// Prüfe official_tournaments Daten
if (tableNames.includes('official_tournaments')) {
const [count] = await connection.execute('SELECT COUNT(*) as count FROM official_tournaments');
console.log(`=== official_tournaments Daten ===`);
console.log(`Anzahl Einträge: ${count[0].count}`);
if (count[0].count > 0) {
// Prüfe zuerst die Spaltennamen
const [columns] = await connection.execute('SHOW COLUMNS FROM official_tournaments');
console.log('Spalten in official_tournaments:');
columns.forEach(col => {
console.log(` - ${col.Field} (${col.Type})`);
});
console.log('');
const [rows] = await connection.execute('SELECT * FROM official_tournaments LIMIT 5');
console.log('Erste Einträge:');
rows.forEach(row => {
console.log(` - ID: ${row.id}, Daten:`, JSON.stringify(row, null, 2));
});
}
}
} catch (error) {
console.error('Fehler:', error.message);
if (error.code) {
console.error('Error Code:', error.code);
}
} finally {
if (connection) await connection.end();
}
}
checkDatabase();

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env node
/**
* Prüft die Berechtigungen für SSL-Zertifikate
*/
import { readFileSync, statSync } from 'fs';
import { execSync } from 'child_process';
const certPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
console.log('🔍 Prüfe SSL-Zertifikat-Berechtigungen...\n');
// Prüfe, welcher Benutzer den Service ausführt
try {
const serviceUser = execSync('systemctl show -p User tt-tagebuch.service 2>/dev/null | cut -d= -f2', { encoding: 'utf-8' }).trim();
console.log(`📋 Service-Benutzer: ${serviceUser}`);
// Prüfe Gruppen des Service-Benutzers
const groups = execSync(`groups ${serviceUser} 2>/dev/null || id -Gn ${serviceUser} 2>/dev/null`, { encoding: 'utf-8' }).trim();
console.log(`📋 Gruppen von ${serviceUser}: ${groups}`);
if (groups.includes('ssl-cert')) {
console.log('✅ Service-Benutzer ist in der ssl-cert-Gruppe');
} else {
console.log('❌ Service-Benutzer ist NICHT in der ssl-cert-Gruppe!');
console.log(' → Führe aus: sudo ./scripts/fixCertPermissions.sh');
}
} catch (err) {
console.error('⚠️ Konnte Service-Benutzer nicht ermitteln:', err.message);
}
// Prüfe Dateiberechtigungen
try {
const stats = statSync(certPath);
console.log(`\n📜 Dateiberechtigungen für ${certPath}:`);
console.log(` Owner UID: ${stats.uid}`);
console.log(` Group GID: ${stats.gid}`);
console.log(` Mode: ${stats.mode.toString(8)}`);
// Prüfe, ob Datei lesbar ist
try {
readFileSync(certPath);
console.log('✅ Datei ist lesbar');
} catch (err) {
console.error('❌ Datei ist NICHT lesbar:', err.message);
}
} catch (err) {
console.error(`❌ Konnte Datei nicht prüfen: ${err.message}`);
}
// Prüfe Gruppen-Berechtigungen
try {
const groupInfo = execSync('getent group ssl-cert 2>/dev/null', { encoding: 'utf-8' }).trim();
console.log(`\n👥 ssl-cert-Gruppe: ${groupInfo}`);
} catch (err) {
console.error('❌ ssl-cert-Gruppe existiert nicht!');
}

View File

@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* Prüft den Status des Backend-Servers
*/
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
console.log('🔍 Prüfe Backend-Server-Status...\n');
// Prüfe, ob Port 3005 (HTTP) läuft
try {
const { stdout } = await execAsync(`netstat -tlnp 2>/dev/null | grep :3005 || ss -tlnp 2>/dev/null | grep :3005 || echo "Port 3005 nicht gefunden"`);
if (stdout.includes(':3005')) {
console.log('✅ HTTP-Server (Port 3005) läuft');
console.log(` ${stdout.trim()}`);
} else {
console.log('❌ HTTP-Server (Port 3005) läuft nicht');
}
} catch (err) {
console.error('⚠️ Konnte Port 3005 nicht prüfen:', err.message);
}
// Prüfe, ob Port 3051 (HTTPS) läuft
try {
const { stdout } = await execAsync(`netstat -tlnp 2>/dev/null | grep :3051 || ss -tlnp 2>/dev/null | grep :3051 || echo "Port 3051 nicht gefunden"`);
if (stdout.includes(':3051')) {
console.log('\n✅ HTTPS-Server (Port 3051) läuft');
console.log(` ${stdout.trim()}`);
} else {
console.log('\n❌ HTTPS-Server (Port 3051) läuft NICHT');
console.log(' → Prüfe Backend-Logs auf Fehler');
}
} catch (err) {
console.error('⚠️ Konnte Port 3051 nicht prüfen:', err.message);
}
// Prüfe SSL-Zertifikate
import { readFileSync, existsSync } from 'fs';
const keyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
const certPath = '/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem';
console.log('\n📜 Prüfe SSL-Zertifikate...');
if (existsSync(keyPath) && existsSync(certPath)) {
console.log('✅ SSL-Zertifikate gefunden');
try {
readFileSync(keyPath);
readFileSync(certPath);
console.log('✅ SSL-Zertifikate sind lesbar');
} catch (err) {
console.error('❌ SSL-Zertifikate können nicht gelesen werden:', err.message);
console.error(' → Prüfe Dateiberechtigungen');
}
} else {
console.error('❌ SSL-Zertifikate nicht gefunden');
console.error(` Erwartete Pfade:`);
console.error(` - ${keyPath}`);
console.error(` - ${certPath}`);
}
// Prüfe systemd-Service-Status
try {
const { stdout } = await execAsync('systemctl is-active tt-tagebuch 2>/dev/null || echo "inactive"');
if (stdout.trim() === 'active') {
console.log('\n✅ systemd-Service "tt-tagebuch" ist aktiv');
} else {
console.log('\n⚠ systemd-Service "tt-tagebuch" ist nicht aktiv');
}
} catch (err) {
console.log('\n⚠ Konnte systemd-Service-Status nicht prüfen');
}
console.log('\n📋 Nächste Schritte:');
console.log(' 1. Prüfe Backend-Logs: sudo journalctl -u tt-tagebuch -n 50');
console.log(' 2. Prüfe, ob HTTPS-Server gestartet wurde (suche nach "HTTPS-Server für Socket.IO")');
console.log(' 3. Prüfe auf Fehler (suche nach "HTTPS-Server konnte nicht gestartet werden")');

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* Prüft, ob der Socket.IO HTTPS-Server auf Port 3051 läuft
*/
import https from 'https';
import { readFileSync } from 'fs';
const httpsPort = process.env.HTTPS_PORT || 3051;
console.log(`🔍 Prüfe Socket.IO HTTPS-Server auf Port ${httpsPort}...\n`);
// Prüfe, ob Zertifikate existieren
try {
const keyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
const certPath = '/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem';
readFileSync(keyPath);
readFileSync(certPath);
console.log('✅ SSL-Zertifikate gefunden');
} catch (err) {
console.error('❌ SSL-Zertifikate nicht gefunden:', err.message);
console.error(' Erwartete Pfade:');
console.error(' - /etc/letsencrypt/live/tt-tagebuch.de/privkey.pem');
console.error(' - /etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem');
process.exit(1);
}
// Prüfe, ob Port geöffnet ist
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
try {
const { stdout } = await execAsync(`netstat -tlnp 2>/dev/null | grep :${httpsPort} || ss -tlnp 2>/dev/null | grep :${httpsPort} || echo "Port nicht gefunden"`);
if (stdout.includes(':' + httpsPort)) {
console.log(`✅ Port ${httpsPort} ist geöffnet und lauscht`);
console.log(` ${stdout.trim()}`);
} else {
console.log(`❌ Port ${httpsPort} ist nicht geöffnet oder lauscht nicht`);
console.log(' → Backend-Server muss neu gestartet werden');
}
} catch (err) {
console.error('⚠️ Konnte Port-Status nicht prüfen:', err.message);
}
// Versuche Verbindung zum Server
console.log(`\n🔌 Versuche Verbindung zu https://localhost:${httpsPort}/socket.io/...`);
const options = {
hostname: 'localhost',
port: httpsPort,
path: '/socket.io/?EIO=4&transport=polling',
method: 'GET',
rejectUnauthorized: false
};
const req = https.request(options, (res) => {
console.log(`✅ Verbindung erfolgreich! Status: ${res.statusCode}`);
res.on('data', (chunk) => {
console.log(` Response: ${chunk.toString().substring(0, 100)}...`);
});
res.on('end', () => {
console.log('\n✅ Socket.IO HTTPS-Server läuft korrekt!');
});
});
req.on('error', (err) => {
console.error(`❌ Verbindung fehlgeschlagen: ${err.message}`);
console.error(' → Backend-Server läuft möglicherweise nicht');
console.error(' → Oder HTTPS-Server wurde nicht gestartet');
process.exit(1);
});
req.setTimeout(5000, () => {
console.error('❌ Timeout beim Verbindungsversuch');
req.destroy();
process.exit(1);
});
req.end();

View File

@@ -1,15 +1,22 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { development } from '../config.js';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load .env from backend directory
dotenv.config({ path: join(__dirname, '..', '.env') });
const report = [];
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'trainingdiary',
host: process.env.DB_HOST || development.host || 'localhost',
user: process.env.DB_USER || development.username || 'root',
password: process.env.DB_PASSWORD || development.password || '',
database: process.env.DB_NAME || development.database || 'trainingdiary',
};
async function getTables(connection) {

View File

@@ -0,0 +1,97 @@
#!/bin/bash
# Skript zum Fixen der SSL-Zertifikat-Berechtigungen für Node.js
CERT_DIR="/etc/letsencrypt/live/tt-tagebuch.de"
CERT_GROUP="ssl-cert" # Standard-Gruppe für SSL-Zertifikate
# Prüfe, ob Zertifikate existieren (mit sudo, da normaler Benutzer keinen Zugriff hat)
if ! sudo test -d "$CERT_DIR"; then
echo "❌ Zertifikat-Verzeichnis nicht gefunden: $CERT_DIR"
exit 1
fi
# Prüfe, ob ssl-cert-Gruppe existiert
if ! sudo getent group "$CERT_GROUP" > /dev/null 2>&1; then
echo "⚠️ Gruppe '$CERT_GROUP' existiert nicht. Erstelle sie..."
sudo groupadd "$CERT_GROUP"
fi
# Prüfe, welcher Benutzer den systemd-Service ausführt
SERVICE_USER=$(sudo systemctl show -p User tt-tagebuch.service 2>/dev/null | cut -d= -f2)
# Wenn kein User definiert ist oder "nobody", verwende www-data
if [ -z "$SERVICE_USER" ] || [ "$SERVICE_USER" = "nobody" ]; then
echo "⚠️ Service-Benutzer ist '$SERVICE_USER' oder nicht definiert."
echo " Verwende 'www-data' als Standard (empfohlen für Webserver-Services)."
SERVICE_USER="www-data"
# Prüfe, ob www-data existiert
if ! id "$SERVICE_USER" &>/dev/null; then
echo "❌ Benutzer '$SERVICE_USER' existiert nicht!"
exit 1
fi
fi
echo "🔧 Konfiguriere SSL-Zertifikat-Berechtigungen..."
echo " Service-Benutzer: $SERVICE_USER"
echo " Zertifikat-Verzeichnis: $CERT_DIR"
# Füge Service-Benutzer zur ssl-cert-Gruppe hinzu
sudo usermod -a -G "$CERT_GROUP" "$SERVICE_USER"
# Setze Gruppen-Berechtigungen für Zertifikate
echo "📜 Setze Berechtigungen für Zertifikate..."
# Setze Gruppe für das Verzeichnis
sudo chgrp -R "$CERT_GROUP" "$CERT_DIR"
# Setze Berechtigungen: Owner (root) kann lesen/schreiben, Gruppe kann lesen
# WICHTIG: Verwende find, um auch die Symlinks zu behandeln
sudo find "$CERT_DIR" -name "privkey.pem" -exec chmod 640 {} \;
sudo find "$CERT_DIR" -name "fullchain.pem" -exec chmod 644 {} \;
sudo find "$CERT_DIR" -name "cert.pem" -exec chmod 644 {} \;
sudo find "$CERT_DIR" -name "chain.pem" -exec chmod 644 {} \;
# Setze auch für das archive-Verzeichnis (wo die Symlinks hinzeigen)
ARCHIVE_DIR="/etc/letsencrypt/archive/tt-tagebuch.de"
if sudo test -d "$ARCHIVE_DIR"; then
echo "📜 Setze Berechtigungen für archive-Verzeichnis..."
# WICHTIG: Setze Verzeichnis-Berechtigungen, damit Gruppe in Verzeichnis navigieren kann
# Setze execute-Bit für Gruppe auf allen Verzeichnissen im Pfad
sudo chgrp -R "$CERT_GROUP" "/etc/letsencrypt/archive"
sudo chmod 750 "/etc/letsencrypt/archive"
sudo chmod 750 "/etc/letsencrypt/archive/tt-tagebuch.de"
# Setze Gruppe für alle Dateien
sudo chgrp -R "$CERT_GROUP" "$ARCHIVE_DIR"
# Setze Berechtigungen für alle privkey-Dateien (privkey.pem, privkey1.pem, privkey8.pem, etc.)
sudo find "$ARCHIVE_DIR" -type f -name "privkey*.pem" -exec chmod 640 {} \;
sudo find "$ARCHIVE_DIR" -type f -name "fullchain*.pem" -exec chmod 644 {} \;
sudo find "$ARCHIVE_DIR" -type f -name "cert*.pem" -exec chmod 644 {} \;
sudo find "$ARCHIVE_DIR" -type f -name "chain*.pem" -exec chmod 644 {} \;
# Zeige, welche Dateien gefunden wurden
echo " Gefundene privkey-Dateien:"
sudo find "$ARCHIVE_DIR" -type f -name "privkey*.pem" -exec ls -la {} \;
fi
# WICHTIG: Setze auch Verzeichnis-Berechtigungen für live-Verzeichnis
LIVE_DIR="/etc/letsencrypt/live"
if sudo test -d "$LIVE_DIR"; then
echo "📜 Setze Verzeichnis-Berechtigungen für live-Verzeichnis..."
sudo chgrp -R "$CERT_GROUP" "$LIVE_DIR"
sudo chmod 750 "$LIVE_DIR"
sudo chmod 750 "$LIVE_DIR/tt-tagebuch.de"
fi
echo "✅ Berechtigungen gesetzt!"
echo ""
echo "📋 Prüfe Berechtigungen:"
sudo ls -la "$CERT_DIR/privkey.pem"
sudo ls -la "$CERT_DIR/fullchain.pem"
echo ""
echo "⚠️ WICHTIG: Der Service muss neu gestartet werden, damit die Gruppenänderung wirksam wird:"
echo " sudo systemctl restart tt-tagebuch"

View File

@@ -0,0 +1,171 @@
/**
* Migration Script: Verschlüsselung für bestehende MyTischtennis-Daten
*
* WICHTIG: Dieses Script verschlüsselt bestehende unverschlüsselte MyTischtennis-Daten.
* Es sollte NUR EINMAL ausgeführt werden, nachdem das Model aktualisiert wurde.
*
* Vorsicht: Wenn Daten bereits verschlüsselt sind, wird dieses Script sie doppelt verschlüsseln!
*
* Usage: node backend/scripts/migrateMyTischtennisEncryption.js
*/
import sequelize from '../database.js';
import MyTischtennis from '../models/MyTischtennis.js';
import { encryptData } from '../utils/encrypt.js';
async function migrateMyTischtennisEncryption() {
console.log('🔄 Starte Migration: Verschlüsselung für MyTischtennis-Daten\n');
try {
// Hole alle MyTischtennis-Einträge mit raw: true, um unverschlüsselte Daten zu bekommen
const accounts = await MyTischtennis.findAll({
raw: true,
attributes: ['id', 'email', 'access_token', 'refresh_token', 'cookie', 'user_data', 'club_id', 'club_name', 'fed_nickname']
});
console.log(`📊 Gefundene Einträge: ${accounts.length}\n`);
if (accounts.length === 0) {
console.log('✅ Keine Einträge gefunden. Migration nicht erforderlich.');
return;
}
let migrated = 0;
let skipped = 0;
let errors = 0;
for (const account of accounts) {
try {
// Prüfe, ob Daten bereits verschlüsselt sind
// Verschlüsselte Daten sind hex-Strings und haben eine bestimmte Länge
// Unverschlüsselte E-Mail-Adressen enthalten normalerweise @
const emailIsEncrypted = !account.email.includes('@') && account.email.length > 32;
if (emailIsEncrypted) {
console.log(`⏭️ Eintrag ${account.id}: Bereits verschlüsselt, überspringe...`);
skipped++;
continue;
}
console.log(`🔐 Verschlüssele Eintrag ${account.id}...`);
// Verschlüssele alle Felder direkt in der Datenbank
const updateData = {};
if (account.email && account.email.includes('@')) {
updateData.email = encryptData(account.email);
}
if (account.access_token && !account.access_token.startsWith('encrypted_')) {
// Prüfe, ob es bereits verschlüsselt aussieht (hex-String)
const looksEncrypted = /^[0-9a-f]+$/i.test(account.access_token) && account.access_token.length > 32;
if (!looksEncrypted) {
updateData.access_token = encryptData(account.access_token);
}
}
if (account.refresh_token && !account.refresh_token.startsWith('encrypted_')) {
const looksEncrypted = /^[0-9a-f]+$/i.test(account.refresh_token) && account.refresh_token.length > 32;
if (!looksEncrypted) {
updateData.refresh_token = encryptData(account.refresh_token);
}
}
if (account.cookie && !account.cookie.startsWith('encrypted_')) {
const looksEncrypted = /^[0-9a-f]+$/i.test(account.cookie) && account.cookie.length > 32;
if (!looksEncrypted) {
updateData.cookie = encryptData(account.cookie);
}
}
if (account.user_data) {
// user_data ist JSON, muss zuerst zu String konvertiert werden
try {
const userDataStr = typeof account.user_data === 'string'
? account.user_data
: JSON.stringify(account.user_data);
// Prüfe, ob bereits verschlüsselt
const looksEncrypted = /^[0-9a-f]+$/i.test(userDataStr) && userDataStr.length > 32;
if (!looksEncrypted) {
updateData.user_data = encryptData(userDataStr);
}
} catch (e) {
console.error(` ⚠️ Fehler bei user_data für Eintrag ${account.id}:`, e.message);
}
}
if (account.club_id && account.club_id.length > 0 && !account.club_id.startsWith('encrypted_')) {
const looksEncrypted = /^[0-9a-f]+$/i.test(account.club_id) && account.club_id.length > 32;
if (!looksEncrypted) {
updateData.club_id = encryptData(account.club_id);
}
}
if (account.club_name && account.club_name.length > 0 && !account.club_name.startsWith('encrypted_')) {
const looksEncrypted = /^[0-9a-f]+$/i.test(account.club_name) && account.club_name.length > 32;
if (!looksEncrypted) {
updateData.club_name = encryptData(account.club_name);
}
}
if (account.fed_nickname && account.fed_nickname.length > 0 && !account.fed_nickname.startsWith('encrypted_')) {
const looksEncrypted = /^[0-9a-f]+$/i.test(account.fed_nickname) && account.fed_nickname.length > 32;
if (!looksEncrypted) {
updateData.fed_nickname = encryptData(account.fed_nickname);
}
}
// Update nur, wenn es etwas zu aktualisieren gibt
if (Object.keys(updateData).length > 0) {
await sequelize.query(
`UPDATE my_tischtennis SET ${Object.keys(updateData).map(key => `\`${key}\` = :${key}`).join(', ')} WHERE id = :id`,
{
replacements: { ...updateData, id: account.id },
type: sequelize.QueryTypes.UPDATE
}
);
migrated++;
console.log(` ✅ Eintrag ${account.id} erfolgreich verschlüsselt`);
} else {
skipped++;
console.log(` ⏭️ Eintrag ${account.id}: Keine unverschlüsselten Daten gefunden`);
}
} catch (error) {
errors++;
console.error(` ❌ Fehler bei Eintrag ${account.id}:`, error.message);
}
}
console.log('\n📊 Migrations-Zusammenfassung:');
console.log(` ✅ Migriert: ${migrated}`);
console.log(` ⏭️ Übersprungen: ${skipped}`);
console.log(` ❌ Fehler: ${errors}`);
if (errors === 0) {
console.log('\n✅ Migration erfolgreich abgeschlossen!');
} else {
console.log('\n⚠ Migration abgeschlossen, aber es gab Fehler. Bitte prüfen Sie die Logs.');
}
} catch (error) {
console.error('❌ Kritischer Fehler bei Migration:', error);
throw error;
} finally {
await sequelize.close();
}
}
// Script ausführen
migrateMyTischtennisEncryption()
.then(() => {
console.log('\n✅ Script beendet.');
process.exit(0);
})
.catch((error) => {
console.error('\n❌ Script fehlgeschlagen:', error);
process.exit(1);
});

View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Testet, ob www-data Zugriff auf die SSL-Zertifikate hat
CERT_PATH="/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem"
echo "🔍 Teste Zugriff auf SSL-Zertifikate als www-data...\n"
# Prüfe, ob die Datei existiert
if ! sudo test -f "$CERT_PATH"; then
echo "❌ Datei nicht gefunden: $CERT_PATH"
exit 1
fi
# Prüfe Berechtigungen
echo "📋 Dateiberechtigungen:"
sudo ls -la "$CERT_PATH"
# Prüfe, auf welche Datei der Symlink zeigt
REAL_PATH=$(sudo readlink -f "$CERT_PATH")
echo "\n📋 Symlink zeigt auf: $REAL_PATH"
echo "📋 Berechtigungen der echten Datei:"
sudo ls -la "$REAL_PATH"
# Teste, ob www-data die Datei lesen kann
echo "\n🔍 Teste Lesezugriff als www-data..."
if sudo -u www-data test -r "$CERT_PATH"; then
echo "✅ www-data kann die Datei lesen!"
else
echo "❌ www-data kann die Datei NICHT lesen!"
echo "\n📋 Prüfe Gruppen-Berechtigungen:"
sudo stat -c "%A %U:%G" "$REAL_PATH"
echo "\n📋 Prüfe, ob www-data in ssl-cert-Gruppe ist:"
sudo groups www-data
fi
# Versuche, die Datei als www-data zu lesen
echo "\n🔍 Versuche, Datei als www-data zu öffnen..."
if sudo -u www-data cat "$CERT_PATH" > /dev/null 2>&1; then
echo "✅ www-data kann die Datei erfolgreich öffnen!"
else
echo "❌ www-data kann die Datei NICHT öffnen!"
echo "\n💡 Mögliche Lösungen:"
echo " 1. Prüfe, ob die Datei im archive-Verzeichnis die richtige Gruppe hat:"
echo " sudo ls -la $REAL_PATH"
echo " 2. Setze Gruppe und Berechtigungen erneut:"
echo " sudo chgrp ssl-cert $REAL_PATH"
echo " sudo chmod 640 $REAL_PATH"
fi

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env node
/**
* Test-Script zum Prüfen, ob der WebSocket-Server lokal erreichbar ist
*
* Verwendung:
* node scripts/testWebSocket.js
* node scripts/testWebSocket.js localhost 3050
*/
import http from 'http';
const host = process.argv[2] || 'localhost';
const port = process.argv[3] || '3050';
const url = `http://${host}:${port}`;
console.log(`🔌 Teste Socket.IO-Server auf ${url}...\n`);
// Test 1: HTTP-Polling (Socket.IO Handshake)
console.log('1⃣ Teste HTTP-Polling (Socket.IO Handshake)...');
const pollingUrl = `${url}/socket.io/?EIO=4&transport=polling`;
const pollingReq = http.get(pollingUrl, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode === 200) {
console.log(' ✅ HTTP-Polling erfolgreich!');
console.log(` Status: ${res.statusCode}`);
console.log(` Response: ${data.substring(0, 200)}...`);
// Versuche Session-ID zu extrahieren
let sessionId = null;
try {
const jsonMatch = data.match(/\{.*\}/);
if (jsonMatch) {
const json = JSON.parse(jsonMatch[0]);
if (json.sid) {
sessionId = json.sid;
console.log(` Session ID: ${sessionId}`);
}
}
} catch (e) {
// Ignoriere JSON-Parse-Fehler
}
// Test 2: WebSocket-Upgrade
console.log('\n2⃣ Teste WebSocket-Upgrade...');
testWebSocketUpgrade(host, port, sessionId);
} else {
console.error(` ❌ HTTP-Polling fehlgeschlagen: Status ${res.statusCode}`);
process.exit(1);
}
});
});
pollingReq.on('error', (error) => {
console.error(' ❌ HTTP-Polling Fehler:');
console.error(` ${error.message}`);
if (error.code === 'ECONNREFUSED') {
console.error(' → Server läuft möglicherweise nicht oder ist nicht erreichbar');
console.error(` → Prüfe: netstat -tlnp | grep ${port}`);
}
process.exit(1);
});
pollingReq.setTimeout(5000, () => {
pollingReq.destroy();
console.error(' ❌ Timeout: Keine Antwort innerhalb von 5 Sekunden');
process.exit(1);
});
function testWebSocketUpgrade(host, port, sessionId) {
// WebSocket-Upgrade-Request
// Sec-WebSocket-Key muss 16 Bytes (128 Bits) sein, base64-encoded
const wsKey = Buffer.allocUnsafe(16);
for (let i = 0; i < 16; i++) {
wsKey[i] = Math.floor(Math.random() * 256);
}
const wsKeyBase64 = wsKey.toString('base64');
const path = sessionId
? `/socket.io/?EIO=4&transport=websocket&sid=${sessionId}`
: '/socket.io/?EIO=4&transport=websocket';
const options = {
hostname: host,
port: port,
path: path,
method: 'GET',
headers: {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Key': wsKeyBase64,
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Protocol': 'chat, superchat'
}
};
console.log(` Request-Path: ${path}`);
console.log(` Sec-WebSocket-Key: ${wsKeyBase64.substring(0, 20)}...`);
const wsReq = http.request(options, (res) => {
console.log(` Response Status: ${res.statusCode}`);
console.log(` Response Headers:`, JSON.stringify(res.headers, null, 2));
if (res.statusCode === 101) {
console.log(' ✅ WebSocket-Upgrade erfolgreich!');
console.log(` Status: ${res.statusCode} (Switching Protocols)`);
console.log(` Upgrade Header: ${res.headers.upgrade}`);
console.log(` Connection Header: ${res.headers.connection}`);
console.log('\n✅ Socket.IO-Server ist erreichbar und unterstützt WebSockets!');
process.exit(0);
} else {
console.log(` ⚠️ WebSocket-Upgrade: Status ${res.statusCode} (erwartet: 101)`);
console.log(` → Server antwortet, aber Upgrade nicht erfolgreich`);
// Lese Response-Body für weitere Informationen
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
if (body) {
console.log(` Response Body: ${body.substring(0, 200)}`);
}
if (res.statusCode === 400) {
console.log(` → Socket.IO lehnt den Request ab (Bad Request)`);
console.log(` → Möglicherweise fehlt die Session-ID oder sie ist ungültig`);
}
process.exit(0);
});
}
});
wsReq.on('error', (error) => {
console.error(' ❌ WebSocket-Upgrade Fehler:');
console.error(` ${error.message}`);
console.error(` Error Code: ${error.code}`);
console.log('\n⚠ HTTP-Polling funktioniert, aber WebSocket-Upgrade schlägt fehl.');
process.exit(0);
});
wsReq.setTimeout(5000, () => {
wsReq.destroy();
console.error(' ❌ Timeout: Keine Antwort innerhalb von 5 Sekunden');
console.error(' → Möglicherweise hängt der Server oder die Verbindung wird nicht akzeptiert');
process.exit(1);
});
wsReq.end();
}

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env node
/**
* Test-Script zum Prüfen, ob WebSocket über Apache erreichbar ist
*
* Verwendung:
* node scripts/testWebSocketApache.js
* node scripts/testWebSocketApache.js https://tt-tagebuch.de
*/
import https from 'https';
import http from 'http';
const url = process.argv[2] || 'https://tt-tagebuch.de';
const isHttps = url.startsWith('https://');
const client = isHttps ? https : http;
console.log(`🔌 Teste Socket.IO-Server über ${isHttps ? 'Apache (HTTPS)' : 'HTTP'} auf ${url}...\n`);
// Test 1: HTTP-Polling (Socket.IO Handshake)
console.log('1⃣ Teste HTTP-Polling (Socket.IO Handshake)...');
const pollingUrl = `${url}/socket.io/?EIO=4&transport=polling`;
const pollingReq = client.get(pollingUrl, {
rejectUnauthorized: false // Für selbst-signierte Zertifikate
}, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode === 200) {
console.log(' ✅ HTTP-Polling erfolgreich!');
console.log(` Status: ${res.statusCode}`);
console.log(` Response: ${data.substring(0, 200)}...`);
// Versuche Session-ID zu extrahieren
try {
const jsonMatch = data.match(/\{.*\}/);
if (jsonMatch) {
const json = JSON.parse(jsonMatch[0]);
if (json.sid) {
console.log(` Session ID: ${json.sid}`);
// Test 2: WebSocket-Upgrade
console.log('\n2⃣ Teste WebSocket-Upgrade...');
testWebSocketUpgrade(url, json.sid, isHttps);
}
}
} catch (e) {
console.log(' ⚠️ Konnte Session-ID nicht extrahieren');
console.log('\n2⃣ Teste WebSocket-Upgrade ohne Session-ID...');
testWebSocketUpgrade(url, null, isHttps);
}
} else {
console.error(` ❌ HTTP-Polling fehlgeschlagen: Status ${res.statusCode}`);
process.exit(1);
}
});
});
pollingReq.on('error', (error) => {
console.error(' ❌ HTTP-Polling Fehler:');
console.error(` ${error.message}`);
if (error.code === 'ECONNREFUSED') {
console.error(' → Server läuft möglicherweise nicht oder ist nicht erreichbar');
}
process.exit(1);
});
pollingReq.setTimeout(10000, () => {
pollingReq.destroy();
console.error(' ❌ Timeout: Keine Antwort innerhalb von 10 Sekunden');
process.exit(1);
});
function testWebSocketUpgrade(baseUrl, sessionId, useHttps) {
// WebSocket-Upgrade-Request
// Sec-WebSocket-Key muss 16 Bytes (128 Bits) sein, base64-encoded
const wsKey = Buffer.allocUnsafe(16);
for (let i = 0; i < 16; i++) {
wsKey[i] = Math.floor(Math.random() * 256);
}
const wsKeyBase64 = wsKey.toString('base64');
const path = sessionId
? `/socket.io/?EIO=4&transport=websocket&sid=${sessionId}`
: `/socket.io/?EIO=4&transport=websocket`;
const urlObj = new URL(baseUrl);
const options = {
hostname: urlObj.hostname,
port: urlObj.port || (useHttps ? 443 : 80),
path: path,
method: 'GET',
headers: {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Key': wsKeyBase64,
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Protocol': 'chat, superchat',
'Origin': baseUrl
},
rejectUnauthorized: false // Für selbst-signierte Zertifikate
};
const client = useHttps ? https : http;
const wsReq = client.request(options, (res) => {
console.log(` Status: ${res.statusCode}`);
console.log(` Upgrade Header: ${res.headers.upgrade || 'nicht gesetzt'}`);
console.log(` Connection Header: ${res.headers.connection || 'nicht gesetzt'}`);
if (res.statusCode === 101) {
console.log(' ✅ WebSocket-Upgrade erfolgreich!');
console.log(` Status: ${res.statusCode} (Switching Protocols)`);
console.log('\n✅ Socket.IO-Server ist über Apache erreichbar und unterstützt WebSockets!');
process.exit(0);
} else if (res.statusCode === 400) {
console.log(` ⚠️ WebSocket-Upgrade: Status ${res.statusCode} (Bad Request)`);
console.log(` → Apache leitet weiter, aber Backend akzeptiert den Request nicht`);
console.log(` → Möglicherweise fehlen Header oder die Session-ID ist ungültig`);
} else {
console.log(` ⚠️ WebSocket-Upgrade: Status ${res.statusCode} (erwartet: 101)`);
console.log(` → Server antwortet, aber Upgrade nicht erfolgreich`);
}
// Lese Response-Body für weitere Informationen
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
if (body) {
console.log(` Response Body: ${body.substring(0, 200)}`);
}
console.log('\n⚠ HTTP-Polling funktioniert, aber WebSocket-Upgrade schlägt fehl.');
console.log(' → Das könnte ein Problem mit der Apache-Konfiguration sein.');
process.exit(0);
});
});
wsReq.on('error', (error) => {
console.error(' ❌ WebSocket-Upgrade Fehler:');
console.error(` ${error.message}`);
if (error.code === 'ECONNREFUSED') {
console.error(' → Verbindung wurde abgelehnt');
} else if (error.code === 'ENOTFOUND') {
console.error(' → Hostname nicht gefunden');
}
console.log('\n⚠ HTTP-Polling funktioniert, aber WebSocket-Upgrade schlägt fehl.');
console.log(' → Das könnte ein Problem mit der Apache-Konfiguration sein.');
process.exit(0);
});
wsReq.setTimeout(10000, () => {
wsReq.destroy();
console.error(' ❌ Timeout: Keine Antwort innerhalb von 10 Sekunden');
process.exit(1);
});
wsReq.end();
}

View File

@@ -1,8 +1,13 @@
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import { createServer } from 'http';
import https from 'https';
import fs from 'fs';
import { exec } from 'child_process';
import sequelize from './database.js';
import cors from 'cors';
import { initializeSocketIO } from './services/socketService.js';
import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
@@ -44,8 +49,11 @@ import memberActivityRoutes from './routes/memberActivityRoutes.js';
import permissionRoutes from './routes/permissionRoutes.js';
import apiLogRoutes from './routes/apiLogRoutes.js';
import memberTransferConfigRoutes from './routes/memberTransferConfigRoutes.js';
import trainingGroupRoutes from './routes/trainingGroupRoutes.js';
import trainingTimeRoutes from './routes/trainingTimeRoutes.js';
import schedulerService from './services/schedulerService.js';
import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.js';
import HttpError from './exceptions/HttpError.js';
const app = express();
const port = process.env.PORT || 3005;
@@ -53,12 +61,14 @@ const port = process.env.PORT || 3005;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// CORS-Konfiguration - Socket.IO hat seine eigene CORS-Konfiguration
app.use(cors({
origin: true,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid']
}));
app.use(express.json());
// Request Logging Middleware - loggt alle API-Requests
@@ -73,31 +83,6 @@ process.on('unhandledRejection', (reason, promise) => {
console.error('[unhandledRejection]', reason);
});
// Globale Fehlerbehandlung für API-Routen
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
const status = err?.statusCode || err?.status || 500;
const message = err?.message || 'Interner Serverfehler';
const response = {
success: false,
message,
error: message
};
if (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') {
response.debug = {
stack: err?.stack || null
};
}
console.error('[ExpressError]', err);
res.status(status).json(response);
});
app.use('/api/auth', authRoutes);
app.use('/api/clubs', clubRoutes);
app.use('/api/clubmembers', memberRoutes);
@@ -130,12 +115,93 @@ app.use('/api/member-activities', memberActivityRoutes);
app.use('/api/permissions', permissionRoutes);
app.use('/api/logs', apiLogRoutes);
app.use('/api/member-transfer-config', memberTransferConfigRoutes);
app.use('/api/training-groups', trainingGroupRoutes);
app.use('/api/training-times', trainingTimeRoutes);
// Middleware für dynamischen kanonischen Tag (vor express.static)
const setCanonicalTag = (req, res, next) => {
// Socket.IO-Requests komplett ignorieren
if (req.path.startsWith('/socket.io/')) {
return next();
}
// Nur für HTML-Anfragen (nicht für API, Assets, etc.)
if (req.path.startsWith('/api') || req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|mp3|webmanifest|xml|txt)$/)) {
return next();
}
// Prüfe, ob die Datei als statische Datei existiert (außer index.html)
const staticPath = path.join(__dirname, '../frontend/dist', req.path);
fs.access(staticPath, fs.constants.F_OK, (err) => {
if (!err && req.path !== '/' && req.path !== '/index.html') {
// Datei existiert und ist nicht index.html, lasse express.static sie servieren
return next();
}
// Datei existiert nicht oder ist index.html, serviere index.html mit dynamischem kanonischen Tag
const indexPath = path.join(__dirname, '../frontend/dist/index.html');
fs.readFile(indexPath, 'utf8', (err, data) => {
if (err) {
return next();
}
// Bestimme die kanonische URL (bevorzuge non-www)
const protocol = req.protocol || 'https';
const host = req.get('host') || 'tt-tagebuch.de';
const canonicalHost = host.replace(/^www\./, ''); // Entferne www falls vorhanden
const canonicalUrl = `${protocol}://${canonicalHost}${req.path === '/' ? '' : req.path}`;
// Ersetze den kanonischen Tag
const updatedData = data.replace(
/<link rel="canonical" href="[^"]*" \/>/,
`<link rel="canonical" href="${canonicalUrl}" />`
);
res.setHeader('Content-Type', 'text/html');
res.send(updatedData);
});
});
};
app.use(setCanonicalTag);
app.use(express.static(path.join(__dirname, '../frontend/dist')));
// Catch-All Handler für Frontend-Routen (muss nach den API-Routen stehen)
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
// Globale Fehlerbehandlung für API-Routen (MUSS nach allen Routes sein!)
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
const status = err?.statusCode || err?.status || 500;
// Unterstützung für Fehlercodes
let errorResponse;
if (err instanceof HttpError && err.errorCode) {
// Neues Format mit Fehlercode
errorResponse = err.toJSON();
} else {
// Legacy-Format: String-Nachricht
const message = err?.message || 'Interner Serverfehler';
errorResponse = {
message
};
}
const response = {
success: false,
...errorResponse,
// Für Rückwärtskompatibilität: error-Feld mit Nachricht
error: errorResponse.message || errorResponse.code || 'Interner Serverfehler'
};
if (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') {
response.debug = {
stack: err?.stack || null
};
}
console.error('[ExpressError]', err);
res.status(status).json(response);
});
(async () => {
@@ -256,7 +322,99 @@ app.get('*', (req, res) => {
// Start scheduler service
schedulerService.start();
app.listen(port);
// Erstelle HTTP-Server für API
const httpServer = createServer(app);
// WICHTIG: Socket.IO muss VOR dem Server-Start initialisiert werden
// damit es Upgrade-Requests abfangen kann
let socketIOInitialized = false;
// Erstelle HTTPS-Server für Socket.IO (direkt mit SSL)
const httpsPort = process.env.HTTPS_PORT || 3051;
// Prüfe, ob SSL-Zertifikate vorhanden sind
const sslKeyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
const sslCertPath = '/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem';
if (fs.existsSync(sslKeyPath) && fs.existsSync(sslCertPath)) {
try {
console.log('📜 Lade SSL-Zertifikate...');
const httpsOptions = {
key: fs.readFileSync(sslKeyPath),
cert: fs.readFileSync(sslCertPath)
};
console.log('✅ SSL-Zertifikate erfolgreich geladen');
// Erstelle HTTPS-Server mit Express-App
const httpsServer = https.createServer(httpsOptions, app);
// Initialisiere Socket.IO auf HTTPS-Server VOR dem Listen
initializeSocketIO(httpsServer);
socketIOInitialized = true;
// Prüfe, ob Port bereits belegt ist
httpsServer.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`❌ Port ${httpsPort} ist bereits belegt!`);
console.error(' → Möglicherweise läuft bereits ein anderer Server auf diesem Port');
console.error(' → Prüfe mit: lsof -i :3051 oder netstat -tlnp | grep 3051');
socketIOInitialized = false;
} else {
console.error('❌ HTTPS-Server Error:', err.message);
console.error(' Code:', err.code);
socketIOInitialized = false;
}
});
httpsServer.on('clientError', (err, socket) => {
if (socket && !socket.destroyed) {
console.error('❌ HTTPS-Server Client Error:', err.message);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
}
});
// Starte HTTPS-Server
httpsServer.listen(httpsPort, '0.0.0.0', () => {
console.log(`🚀 HTTPS-Server für Socket.IO läuft auf Port ${httpsPort}`);
console.log(` Socket.IO Endpoint: https://tt-tagebuch.de:${httpsPort}/socket.io/`);
console.log(` Prüfe mit: lsof -i :${httpsPort} oder netstat -tlnp | grep ${httpsPort}`);
});
// Prüfe nach kurzer Verzögerung, ob Server wirklich läuft
setTimeout(() => {
if (socketIOInitialized) {
exec(`lsof -i :${httpsPort} || netstat -tlnp 2>/dev/null | grep :${httpsPort} || echo "Port nicht gefunden"`, (error, stdout) => {
if (stdout && !stdout.includes('Port nicht gefunden')) {
console.log(`✅ Port ${httpsPort} ist aktiv und erreichbar`);
} else {
console.warn(`⚠️ Port ${httpsPort} scheint nicht aktiv zu sein - prüfe Server-Logs`);
}
});
}
}, 2000);
} catch (err) {
console.error('⚠️ HTTPS-Server konnte nicht gestartet werden:', err.message);
console.error(' Stack:', err.stack);
console.log(' → Socket.IO läuft auf HTTP-Server (nur für Entwicklung)');
socketIOInitialized = false;
}
} else {
console.log(' SSL-Zertifikate nicht gefunden - Socket.IO läuft auf HTTP-Server (nur für Entwicklung)');
console.log(` Erwartete Pfade: ${sslKeyPath}, ${sslCertPath}`);
console.log(` Prüfe mit: ls -la ${sslKeyPath} ${sslCertPath}`);
}
// Fallback: Socket.IO auf HTTP-Server (wenn noch nicht initialisiert)
// WICHTIG: VOR dem httpServer.listen() initialisieren
if (!socketIOInitialized) {
initializeSocketIO(httpServer);
console.log(' ✅ Socket.IO erfolgreich auf HTTP-Server initialisiert');
}
// HTTP-Server starten NACH Socket.IO-Initialisierung
httpServer.listen(port, () => {
console.log(`🚀 HTTP-Server läuft auf Port ${port}`);
});
} catch (err) {
console.error('Unable to synchronize the database:', err);
}

View File

@@ -1,9 +1,11 @@
import ApiLog from '../models/ApiLog.js';
import { Op } from 'sequelize';
import { sanitizeLogData, truncateString, sanitizeIpAddress, sanitizeUserAgent } from '../utils/logDataSanitizer.js';
class ApiLogService {
/**
* Log an API request/response
* DSGVO-konform: Personenbezogene Daten werden nur bei Fehlern geloggt und dann verschlüsselt/gekürzt
*/
async logRequest(options) {
try {
@@ -22,24 +24,53 @@ class ApiLogService {
schedulerJobType = null
} = options;
// Truncate long fields (raise limits to fit typical API JSON bodies)
const truncate = (str, maxLen = 64000) => {
if (!str) return null;
const strVal = typeof str === 'string' ? str : JSON.stringify(str);
return strVal.length > maxLen ? strVal.substring(0, maxLen) + '... (truncated)' : strVal;
};
// Wenn kein Statuscode übergeben wurde, behandeln wir den Logeintrag als Fehler,
// damit Request-/Response-Bodies für Debugging/Testzwecke gespeichert werden.
// (Historisch haben Tests/Callsites logRequest ohne statusCode genutzt.)
const isError = statusCode === null || statusCode === undefined ? true : statusCode >= 400;
// DSGVO-konform: Nur bei Fehlern Request/Response-Bodies loggen
let sanitizedRequestBody = null;
let sanitizedResponseBody = null;
if (isError) {
// Bei Fehlern: Sanitize personenbezogene Daten
if (requestBody) {
sanitizedRequestBody = sanitizeLogData(requestBody, true); // Verschlüssele sensible Daten
// Prüfe, ob Ergebnis bereits ein String ist (sanitizeLogData kann String oder Objekt zurückgeben)
const requestBodyStr = typeof sanitizedRequestBody === 'string'
? sanitizedRequestBody
: JSON.stringify(sanitizedRequestBody);
// Für Diagnosezwecke etwas großzügiger als 2000 Zeichen, aber weiterhin begrenzt.
sanitizedRequestBody = truncateString(requestBodyStr, 64020);
}
if (responseBody) {
sanitizedResponseBody = sanitizeLogData(responseBody, true); // Verschlüssele sensible Daten
// Prüfe, ob Ergebnis bereits ein String ist (sanitizeLogData kann String oder Objekt zurückgeben)
const responseBodyStr = typeof sanitizedResponseBody === 'string'
? sanitizedResponseBody
: JSON.stringify(sanitizedResponseBody);
sanitizedResponseBody = truncateString(responseBodyStr, 64020);
}
}
// Bei Erfolg: Keine Bodies loggen (Datenminimierung)
// IP-Adresse und User-Agent sanitizen
const sanitizedIp = sanitizeIpAddress(ipAddress);
const sanitizedUA = sanitizeUserAgent(userAgent);
await ApiLog.create({
userId,
method,
path,
statusCode,
requestBody: truncate(requestBody, 64000),
responseBody: truncate(responseBody, 64000),
requestBody: sanitizedRequestBody,
responseBody: sanitizedResponseBody,
executionTime,
errorMessage: truncate(errorMessage, 5000),
ipAddress,
userAgent,
errorMessage: errorMessage ? truncateString(errorMessage, 5000) : null,
ipAddress: sanitizedIp,
userAgent: sanitizedUA,
logType,
schedulerJobType
});
@@ -157,6 +188,8 @@ class ApiLogService {
'id', 'userId', 'method', 'path', 'statusCode',
'executionTime', 'errorMessage', 'ipAddress', 'logType',
'schedulerJobType', 'createdAt'
// requestBody und responseBody werden NICHT zurückgegeben (DSGVO: Datenminimierung)
// Nur bei expliziter Anfrage über getLogById verfügbar
]
});

View File

@@ -78,6 +78,9 @@ class AutoFetchMatchResultsService {
const loginResult = await myTischtennisClient.login(account.email, password);
if (!loginResult.success) {
if (loginResult.requiresCaptcha) {
throw new Error(`Re-login failed: CAPTCHA erforderlich. Bitte loggen Sie sich einmal direkt auf mytischtennis.de ein, um das CAPTCHA zu lösen.`);
}
throw new Error(`Re-login failed: ${loginResult.error}`);
}

View File

@@ -70,6 +70,9 @@ class AutoUpdateRatingsService {
const loginResult = await myTischtennisClient.login(account.email, password);
if (!loginResult.success) {
if (loginResult.requiresCaptcha) {
throw new Error(`Re-login failed: CAPTCHA erforderlich. Bitte loggen Sie sich einmal direkt auf mytischtennis.de ein, um das CAPTCHA zu lösen.`);
}
throw new Error(`Re-login failed: ${loginResult.error}`);
}

View File

@@ -5,6 +5,7 @@ import Member from '../models/Member.js';
import { Op, fn, where, col } from 'sequelize';
import { checkAccess } from '../utils/userUtils.js';
import permissionService from './permissionService.js';
import trainingGroupService from './trainingGroupService.js';
class ClubService {
async getAllClubs() {
@@ -18,7 +19,10 @@ class ClubService {
}
async createClub(clubName) {
return await Club.create({ name: clubName });
const club = await Club.create({ name: clubName });
// Erstelle automatisch die Vorgaben-Gruppen
await trainingGroupService.createPresetGroups(club.id);
return club;
}
async addUserToClub(userId, clubId, isOwner = false) {

View File

@@ -166,6 +166,12 @@ class DiaryDateActivityService {
{
model: PredefinedActivity,
as: 'groupPredefinedActivity',
include: [
{
model: PredefinedActivityImage,
as: 'images'
}
]
},
],
}
@@ -178,20 +184,46 @@ class DiaryDateActivityService {
const activityData = activity.toJSON();
if (activityData.predefinedActivity) {
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
// Hole alle Images aus der Datenbank
const allImages = await PredefinedActivityImage.findAll({
where: { predefinedActivityId: activityData.predefinedActivity.id },
order: [['createdAt', 'ASC']]
});
// Konvertiere Images zu JSON und parse drawingData falls vorhanden
const imagesWithParsedData = allImages.map(img => {
const imgData = img.toJSON();
if (imgData.drawingData) {
try {
imgData.drawingData = JSON.parse(imgData.drawingData);
} catch (error) {
console.error(`Image ${imgData.id}: Error parsing drawingData:`, error);
}
}
return imgData;
});
// Setze images Array
activityData.predefinedActivity.images = imagesWithParsedData;
const firstImage = allImages.length > 0 ? allImages[0] : null;
// Füge Zeichnungsdaten hinzu, falls vorhanden
if (firstImage && firstImage.drawingData) {
// Priorität: 1. drawingData direkt auf PredefinedActivity, 2. drawingData aus firstImage
if (activityData.predefinedActivity.drawingData) {
// drawingData ist bereits vorhanden (aus dem Model)
try {
if (typeof activityData.predefinedActivity.drawingData === 'string') {
activityData.predefinedActivity.drawingData = JSON.parse(activityData.predefinedActivity.drawingData);
}
} catch (error) {
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
}
} else if (firstImage && firstImage.drawingData) {
try {
activityData.predefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
} catch (error) {
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData:`, error);
console.error(`Activity ${activityData.predefinedActivity.id}: Error parsing drawingData from image:`, error);
}
}
@@ -221,12 +253,48 @@ class DiaryDateActivityService {
});
for (const groupActivity of activityData.groupActivities) {
if (groupActivity.groupPredefinedActivity) {
// Hole die erste verfügbare Image-ID direkt aus der Datenbank
const firstImage = await PredefinedActivityImage.findOne({
// Hole alle Images aus der Datenbank
const allImages = await PredefinedActivityImage.findAll({
where: { predefinedActivityId: groupActivity.groupPredefinedActivity.id },
order: [['createdAt', 'ASC']]
});
// Konvertiere Images zu JSON und parse drawingData falls vorhanden
const imagesWithParsedData = allImages.map(img => {
const imgData = img.toJSON();
if (imgData.drawingData) {
try {
imgData.drawingData = JSON.parse(imgData.drawingData);
} catch (error) {
console.error(`Image ${imgData.id}: Error parsing drawingData:`, error);
}
}
return imgData;
});
// Setze images Array
groupActivity.groupPredefinedActivity.images = imagesWithParsedData;
const firstImage = allImages.length > 0 ? allImages[0] : null;
// Füge Zeichnungsdaten hinzu, falls vorhanden
// Priorität: 1. drawingData direkt auf PredefinedActivity, 2. drawingData aus firstImage
if (groupActivity.groupPredefinedActivity.drawingData) {
try {
if (typeof groupActivity.groupPredefinedActivity.drawingData === 'string') {
groupActivity.groupPredefinedActivity.drawingData = JSON.parse(groupActivity.groupPredefinedActivity.drawingData);
}
} catch (error) {
console.error(`GroupActivity ${groupActivity.groupPredefinedActivity.id}: Error parsing drawingData:`, error);
}
} else if (firstImage && firstImage.drawingData) {
try {
groupActivity.groupPredefinedActivity.drawingData = JSON.parse(firstImage.drawingData);
} catch (error) {
console.error(`GroupActivity ${groupActivity.groupPredefinedActivity.id}: Error parsing drawingData from image:`, error);
}
}
if (firstImage) {
groupActivity.groupPredefinedActivity.imageUrl = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
groupActivity.groupPredefinedActivity.imageLink = `/api/predefined-activities/${groupActivity.groupPredefinedActivity.id}/image/${firstImage.id}`;
@@ -242,7 +310,7 @@ class DiaryDateActivityService {
return activitiesWithImages;
}
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, timeblockId = null) {
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity, predefinedActivityId = null, timeblockId = null) {
await checkAccess(userToken, clubId);
let diaryDateActivity;
@@ -282,11 +350,37 @@ class DiaryDateActivityService {
console.error('Group diaryDateId:', group.diaryDateId, 'Activity diaryDateId:', diaryDateActivity.diaryDateId);
throw new Error('Group isn\'t related to date');
}
const [predefinedActivity, created] = await PredefinedActivity.findOrCreate({
where: {
name: activity
let predefinedActivity = null;
// 1. Versuche zuerst, die PredefinedActivity per ID zu finden
if (predefinedActivityId) {
predefinedActivity = await PredefinedActivity.findByPk(predefinedActivityId);
}
// 2. Falls nicht gefunden, suche nach Name oder Code
if (!predefinedActivity) {
const normalized = (activity || '').trim();
if (normalized) {
predefinedActivity = await PredefinedActivity.findOne({
where: {
[Op.or]: [
{ name: normalized },
{ code: normalized }
]
}
});
}
});
}
// 3. Falls immer noch nicht gefunden, erstelle eine neue
if (!predefinedActivity) {
predefinedActivity = await PredefinedActivity.create({
name: activity || '',
code: activity || '',
});
}
devLog(predefinedActivity);
const activityData = {
diaryDateActivity: diaryDateActivity.id,
@@ -297,6 +391,24 @@ class DiaryDateActivityService {
return await GroupActivity.create(activityData);
}
async updateGroupActivity(userToken, clubId, groupActivityId, predefinedActivityId) {
await checkAccess(userToken, clubId);
const groupActivity = await GroupActivity.findByPk(groupActivityId);
if (!groupActivity) {
throw new Error('Group activity not found');
}
// Prüfe, ob die PredefinedActivity existiert
const predefinedActivity = await PredefinedActivity.findByPk(predefinedActivityId);
if (!predefinedActivity) {
throw new Error('Predefined activity not found');
}
// Aktualisiere die customActivity (die auf die PredefinedActivity verweist)
groupActivity.customActivity = predefinedActivityId;
return await groupActivity.save();
}
async deleteGroupActivity(userToken, clubId, groupActivityId) {
await checkAccess(userToken, clubId);
const groupActivity = await GroupActivity.findByPk(groupActivityId);

View File

@@ -58,6 +58,22 @@ class GroupService {
await group.save();
return group;
}
async deleteGroup(userToken, groupId, clubId, dateId) {
await checkAccess(userToken, clubId);
await this.checkDiaryDateToClub(clubId, dateId);
const group = await Group.findOne({
where: {
id: groupId,
diaryDateId: dateId
}
});
if (!group) {
throw new HttpError('Gruppe nicht gefunden oder passt nicht zum angegebenen Datum und Verein', 404);
}
await group.destroy();
return { success: true };
}
}
export default new GroupService();

View File

@@ -1086,7 +1086,7 @@ class MemberService {
}
}
async generateMemberGallery(userToken, clubId, size = 200) {
async generateMemberGallery(userToken, clubId, size = 200, createImage = true) {
try {
await checkAccess(userToken, clubId);
@@ -1153,6 +1153,15 @@ class MemberService {
error: 'Keine aktiven Mitglieder mit Bildern gefunden'
};
}
// Wenn kein Bild erstellt werden soll (z.B. bei format=json), nur die Liste zurückgeben
if (!createImage) {
return {
status: 200,
galleryEntries
};
}
// Maximale Breite für die Galerie (Dialog-Breite 900px - 32px Padding = 868px)
const maxGalleryWidth = 868;
// Berechne maximale Anzahl Spalten, die in die Breite passen

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