210 Commits

Author SHA1 Message Date
Torsten Schulz (local)
cf3bd3cd6d Enhance member training participation tracking and UI feedback
This commit adds functionality to track training participations for members in the backend, updating the MemberService to include a count of training participations. The frontend components, DiaryView and MembersView, have been updated to display warning icons based on the number of training participations, providing visual feedback for members with significant participation. CSS styles for warning icons have also been refined to improve visibility and user experience.
2025-11-13 12:12:49 +01:00
Torsten Schulz (local)
4c1a919d17 Add club settings link for admin users and remove test PIN references in MatchReportApiDialog
This commit introduces a new link to the club settings in the App.vue component, visible only to admin users. Additionally, it removes references to a test PIN ('1234') in the MatchReportApiDialog.vue, enhancing security and streamlining the user experience by eliminating unnecessary hints and validation logic.
2025-11-13 09:27:08 +01:00
Torsten Schulz (local)
eb9748dd89 Enhance group assignment functionality in DiaryView with improved reactivity
This commit updates the DiaryView component to enhance the group assignment feature for participants. It adds an @input event listener to the group selection dropdown for immediate UI updates and refines the logic for updating the memberGroupsMap to ensure reactivity in Vue 3. Additionally, it includes error handling improvements to reload participant data upon failure, ensuring data consistency and a better user experience.
2025-11-12 16:21:08 +01:00
Torsten Schulz (local)
e295657621 Add validation endpoint for meeting reports and enhance frontend functionality
This commit introduces a new API endpoint in the backend for validating meeting reports, allowing for improved error handling and cookie management. The frontend has been updated to include a validation function that checks the report data before submission, providing users with feedback on validation warnings and errors. These changes enhance the overall user experience by ensuring data integrity and improving interaction with the meeting report feature.
2025-11-12 16:13:04 +01:00
Torsten Schulz (local)
da351b40b2 Add node-fetch dependency and implement meeting report submission endpoint
This commit introduces the node-fetch package to facilitate HTTP requests in the backend. Additionally, a new API endpoint for submitting meeting reports has been implemented in the nuscoreApiRoutes. The endpoint handles report data submission, cookie management, and error handling, enhancing the functionality of the meeting report feature. Frontend components have been updated to support this new functionality, improving the overall user experience.
2025-11-12 15:29:04 +01:00
Torsten Schulz (local)
3c64e2e92d Remove Playwright dependency and related proxy service files from the backend. This commit deletes the node-fetch package and its associated files, as well as the nuscoreProxyRoutes and nuscoreProxyService, streamlining the backend by eliminating unused components. 2025-11-12 13:41:09 +01:00
Torsten Schulz (local)
45381707ea Erweitert den MatchReportApiDialog um neue Funktionen zur Verwaltung von Spielberichten. Implementiert eine verbesserte Logik zur Berechnung der Gesamtpunkte und Sätze sowie zur Validierung von Eingaben. Fügt visuelle Hinweise für den Abschlussstatus und Warnungen bei fehlerhaften Eingaben hinzu. Optimiert die Benutzeroberfläche mit neuen CSS-Stilen für eine bessere Benutzererfahrung. 2025-11-12 13:40:55 +01:00
Torsten Schulz (local)
b166f7c7d5 Refactor member gallery dialog in DiaryView for improved functionality and user experience
This commit replaces the existing BaseDialog for the member gallery with a new MemberGalleryDialog component, streamlining the dialog's functionality. The new component integrates props for current club, date, and participant status, enhancing interactivity. Additionally, redundant gallery loading logic and state management have been removed, simplifying the codebase and improving maintainability.
2025-11-12 12:53:29 +01:00
Torsten Schulz (local)
1f20737721 Enhance member gallery functionality with latest image retrieval and JSON format support
This commit improves the member gallery feature by allowing users to request the latest member images and receive member information in JSON format. The backend has been updated to handle "latest" as a valid imageId, ensuring the most recent image is fetched. Additionally, the frontend has been modified to support displaying member details in an interactive gallery format, enhancing user experience and providing more flexibility in how member information is presented.
2025-11-12 11:58:37 +01:00
Torsten Schulz (local)
8ef4e1dc9d Enhance member gallery generation with customizable image size selection
This commit updates the member gallery generation feature by allowing users to specify the image size through the frontend. The DiaryView component now includes a dropdown for selecting image dimensions (100x100, 150x150, 200x200), which is passed to the backend for processing. The memberService has been modified to validate the size parameter, ensuring only allowed dimensions are used. These changes improve user experience by providing flexibility in gallery display options.
2025-11-12 10:05:08 +01:00
Torsten Schulz (local)
98c50bc03a Refactor gallery layout in memberService and update DiaryView styles for improved responsiveness
This commit refines the gallery layout logic in memberService by adjusting the calculations for canvas dimensions and positions, ensuring a more accurate representation of images. Additionally, the DiaryView component's styles have been updated to enhance the alignment and responsiveness of the gallery images, contributing to a better user experience across devices.
2025-11-12 08:46:58 +01:00
Torsten Schulz (local)
7b28eb04ac Refactor memberService and update DiaryView styles for improved functionality and layout
This commit includes minor refactoring in the memberService to enhance error logging and improve image handling logic. Additionally, the DiaryView component has been updated to adjust the gallery dialog layout, ensuring better alignment and responsiveness of images. These changes contribute to a more robust backend and a better user experience in the frontend.
2025-11-12 07:57:55 +01:00
Torsten Schulz (local)
ed15137003 Add member gallery generation feature in backend and frontend
This commit introduces a new API endpoint for generating a member gallery, allowing users to retrieve a composite image of active members' latest images. The backend has been updated with a new method in MemberService to handle gallery creation, while the frontend has been enhanced with a dialog for displaying the generated gallery. This feature improves the user experience by providing a visual representation of club members.
2025-11-11 16:22:47 +01:00
Torsten Schulz (local)
2bf5c0137b Adjust image height settings in ImageViewerDialog component for improved responsiveness. Updated max-height for desktop and mobile views to enhance user experience when viewing images. 2025-11-11 15:56:21 +01:00
Torsten Schulz (local)
22e6913005 Implement member image management features in backend and frontend
This commit introduces new functionalities for managing member images, including uploading, deleting, and setting primary images. The memberController and memberService have been updated to handle these operations, while new routes have been added to facilitate image management. The frontend has been enhanced with an ImageViewerDialog component that supports image rotation, deletion, and setting primary images. Additionally, improvements to the member view allow for better image handling and display. These changes enhance the overall user experience and functionality of the member management system.
2025-11-11 15:53:21 +01:00
Torsten Schulz (local)
f7eff0bcb7 Enhance error handling and logging in backend controllers and services
This commit improves error handling in various controllers, including diaryNoteController, memberNoteController, and permissionController, by adding console error logging for better debugging. Additionally, it updates the diaryService and teamDocumentService to enhance functionality and maintainability. The config.js file is also updated to ensure proper configuration for the development environment. These changes contribute to a more robust and user-friendly application.
2025-11-11 11:36:47 +01:00
Torsten Schulz (local)
45c90280f8 Update package dependencies and types for improved compatibility and functionality
This commit updates the package-lock.json and package.json files to reflect the latest versions of dependencies, including @types/node and validator. Additionally, it removes unused type definitions and enhances the overall structure of the project, ensuring better compatibility and performance across the application.
2025-11-11 09:33:24 +01:00
Torsten Schulz (local)
a4890f241b Add new API routes for season, session, team, and tournament management
Integrated additional routes into the Express application for managing seasons, sessions, teams, team documents, and tournaments. This enhancement improves the API's structure and expands its capabilities, allowing for better organization and access to related resources.
2025-11-11 09:30:40 +01:00
Torsten Schulz (local)
684409491f Add permission management tests for role and custom permissions updates
Implemented new tests in permissionRoutes.test.js to validate role changes and custom permissions for users within clubs. Added checks for successful updates and error handling for invalid club IDs. This enhancement improves test coverage for permission management features in the application.
2025-11-11 09:00:18 +01:00
Torsten Schulz (local)
15b88f8177 Add member transfer validation and contact management tests
Implemented a new test for validating transfer configuration via the API in memberRoutes.test.js, ensuring proper error handling for invalid transfer requests. Additionally, enhanced memberService.test.js with tests for creating and updating members along with their contacts, including filtering inactive members. These additions improve the test coverage and reliability of member management features in the application.
2025-11-11 08:35:55 +01:00
Torsten Schulz (local)
20f204e70b Enhance diary note and tag management in backend controllers
Updated the diaryNoteController to require diaryDateId in note creation and improved error handling for missing fields. Enhanced the createTag function in diaryTagController to validate tag names and return appropriate responses. Additionally, refined the deleteTag function to ensure proper error handling when a tag is not found. These changes improve the robustness and usability of the diary management features.
2025-11-11 08:29:18 +01:00
Torsten Schulz (local)
b8191e41ee Add club team and diary routes to the Express application
Integrated new routes for club team management and diary functionality into the backend. This enhancement improves the API's structure and accessibility, allowing for better organization of related resources and expanding the application's capabilities.
2025-11-10 18:29:55 +01:00
Torsten Schulz (local)
b906a218a5 Add API logging and club routes to the Express application
Integrated new routes for API logging and club management into the backend, enhancing the API's functionality and organization. This addition allows for better access to logging and club-related resources, improving overall application structure.
2025-11-10 17:09:57 +01:00
Torsten Schulz (local)
6c3b46c037 Add accident and activity routes to the Express application
Enhanced the backend by integrating new routes for accident and activity management. This addition improves the API's functionality, allowing for better organization and access to related resources.
2025-11-10 17:01:43 +01:00
Torsten Schulz (local)
3f1018ef93 Refactor database configuration and enhance error handling in authentication services
Updated the database configuration to centralize settings and improve maintainability. Enhanced error handling in the authentication service to provide clearer and more specific error messages for various failure scenarios, including registration, activation, and login processes. Additionally, added new dependencies for testing and SQLite support in the package.json file.
2025-11-10 16:54:49 +01:00
Torsten Schulz (local)
620b065ac8 Refactor dialog handling to utilize utility functions for improved consistency
Updated various components to replace direct dialog configurations with utility functions for building dialog configurations. This change enhances the maintainability and readability of the code by centralizing dialog setup logic, ensuring a consistent approach across the application. Additionally, improved error handling and user feedback mechanisms were implemented to provide clearer messages during interactions.
2025-11-10 16:07:24 +01:00
Torsten Schulz (local)
4cfa03834e Refactor alert handling to use dialog components for improved user feedback
Updated various components to replace alert calls with showInfo and showConfirm dialog methods, enhancing user experience by providing more informative and styled feedback. This change standardizes the way messages are displayed across the application, improving consistency and clarity in user interactions.
2025-11-10 14:20:34 +01:00
Torsten Schulz (local)
d94238f6df Refactor logging in cleanup scripts to use report array for improved output management
Updated various cleanup scripts to replace console.log statements with a report array, enhancing the output handling and allowing for better formatting of messages. This change improves the readability of logs and ensures consistent reporting across different cleanup operations, including database connection status, index management, and summary reports.
2025-11-10 13:25:11 +01:00
Torsten Schulz (local)
eb37532de2 Refactor group ID handling in DiaryView and related services for consistency
Updated the handling of group IDs in DiaryView.vue to ensure they are consistently treated as strings. Enhanced the DiaryDateActivityService to filter out duplicate group activities and ensure proper handling of group IDs during updates. This improves data integrity and consistency across the application.
2025-11-08 22:31:38 +01:00
Torsten Schulz (local)
d79e71d6d7 Update PDF parsing and document upload handling in team management
Enhanced the PDFParserService to support layout-based text extraction from PDFs using pdfjs-dist, improving parsing accuracy. Updated the team management view to streamline document uploads and parsing processes, removing unnecessary UI elements and consolidating upload logic. Improved error handling and user feedback during document processing, ensuring better user experience and clarity in case of issues.
2025-11-08 10:15:25 +01:00
Torsten Schulz (local)
f0e3c6a717 Enhance MyTischtennisService and MyTischtennisAccount.vue for improved login handling and account status display
Updated MyTischtennisService to include federation nickname in account data and refined login logic to utilize stored passwords or valid sessions more effectively. Enhanced error handling for login failures. In MyTischtennisAccount.vue, added account status management to display password storage status and improved account loading logic to fetch both account and status data concurrently.
2025-11-07 14:54:15 +01:00
Torsten Schulz (local)
b2d47c7a37 Update error message in MyTischtennisUrlController for clarity
Modified the error handling in MyTischtennisUrlController to specify that only the URL is required, enhancing the clarity of the error response. This change aligns with recent efforts to standardize error handling across the application.
2025-11-07 13:50:18 +01:00
Torsten Schulz (local)
498742e6ae Refactor error handling in controllers and services to standardize HttpError instantiation
Updated multiple controllers and services to ensure consistent error handling by modifying the HttpError instantiation format. This change enhances clarity and maintains uniformity across error responses, improving overall code quality and maintainability.
2025-11-07 13:45:58 +01:00
Torsten Schulz (local)
94aab93f7d Add global error handling middleware for API routes and enhance rating update logic
Implemented a global error handling middleware in the server to standardize error responses for API routes. Additionally, updated the AutoUpdateRatingsService to retrieve the approved user club before updating ratings, ensuring proper access control. Modified the error handling in MyTischtennisAccount.vue to provide more informative login failure messages.
2025-11-07 13:22:22 +01:00
Torsten Schulz (local)
eba8ba30aa Add notes placeholder in member transfer templates for improved data handling
Updated the MemberTransferService to include a new '{{notes}}' placeholder in the member transfer templates. This addition ensures that notes are accounted for during member data transfers, enhancing the completeness of the information being processed.
2025-11-07 10:05:03 +01:00
Torsten Schulz (local)
9d24c6ae7b Filter active members in phone list generation to exclude test memberships for improved accuracy 2025-11-06 17:15:19 +01:00
Torsten Schulz (local)
02b8ba3d0a Enhance PDF generation for member phone lists by improving formatting and fallback handling
Updated the PDFGenerator component to ensure consistent font sizes for phone list entries and added fallback logic for handling cases where no phone contacts are available. Improved the display of parent contact names with appropriate font sizes, enhancing the readability and organization of member information in generated PDFs.
2025-11-06 17:12:58 +01:00
Torsten Schulz (local)
fc7b70b307 Standardize phone number formatting in member service and MembersView
Integrated phone number standardization in the member service to ensure consistent formatting during member updates and contact creation. Added a new utility function in MembersView for phone number standardization, enhancing data integrity and user experience when adding or updating member information.
2025-11-06 16:45:44 +01:00
Torsten Schulz (local)
9cdbd60a23 Enhance member management by adding postal code and contact handling
Introduced a postal code field to the member model and implemented a new MemberContact model to manage multiple phone numbers and email addresses. Updated the member service and controller to handle contact data during member creation and updates. Enhanced the MembersView component to support input for multiple contacts, ensuring better organization and accessibility of member information.
2025-11-06 16:12:34 +01:00
Torsten Schulz (local)
5a4553a8a0 Add member_contact table and postal_code column to member table
Created a new SQL migration to add a postal_code column to the member table and introduced a member_contact table to store multiple phone numbers and email addresses. Implemented data migration for existing phone numbers and email addresses from the member table to the new member_contact table, ensuring data integrity and improved contact management.
2025-11-06 16:03:42 +01:00
Torsten Schulz (local)
98637eec00 Update MembersView styling for action icons to improve layout consistency
Modified the action-icons-row CSS to prevent wrapping and ensure icons remain in a single line, enhancing the visual layout and usability of the MembersView component.
2025-11-06 15:01:40 +01:00
Torsten Schulz (local)
106c63890e Enhance training participation loading in MembersView with improved ID handling and logging
Updated the loadTrainingParticipations method to store participation totals using both string and number keys in the participation map, addressing potential type issues. Enhanced logging to provide detailed insights into the loading process, including responses and member counts, improving debugging capabilities and user experience.
2025-11-06 14:58:19 +01:00
Torsten Schulz (local)
f6b8388819 Add quick deactivate member functionality and update routes and UI
Implemented a new quickDeactivateMember function in MemberService to handle member deactivation. Updated member routes to include a new endpoint for quick deactivation. Enhanced the MembersView component to support quick deactivation actions with updated UI elements, improving user experience for managing member statuses.
2025-11-06 14:46:16 +01:00
Torsten Schulz (local)
f1a29e4111 Add last scheduler executions endpoint and member quick update functions
Implemented a new endpoint in ApiLogController to retrieve the last execution information for scheduler jobs. Added quick update functions in MemberService and corresponding routes for updating test membership status and marking member forms as handed over. Enhanced the MembersView to support quick actions for managing test memberships and form statuses, improving user experience and operational efficiency.
2025-11-06 14:25:15 +01:00
Torsten Schulz (local)
c9d82827ff Enhance member validation and error reporting in MemberTransferService and MemberTransferDialog
Updated the MemberTransferService to include validation for required member fields, categorizing members into valid and invalid lists. Improved error handling to provide detailed feedback on invalid members during transfers. Enhanced the MemberTransferDialog to display information about excluded members and their errors, ensuring users are informed of any issues during the transfer process.
2025-11-06 08:28:27 +01:00
Torsten Schulz (local)
75242f63fc Enhance error handling and logging in MemberTransferService and MemberTransferDialog
Updated the MemberTransferService to provide detailed error messages during bulk transfers, including identification of problematic members. Improved logging for error responses to facilitate debugging. In the MemberTransferDialog, implemented $nextTick to ensure login credentials are cleared properly upon loading and closing, enhancing user experience and preventing autofill issues.
2025-11-06 08:24:39 +01:00
Torsten Schulz (local)
2f161d1eb5 Refactor logging in MemberTransferService to use info and error logs
Updated the MemberTransferService to replace debug logging with info and error logging for better visibility in production. This change enhances the logging of login credentials, transfer details, and error handling, ensuring critical information is captured appropriately during member transfers.
2025-11-06 08:16:14 +01:00
Torsten Schulz (local)
cad76edaad Enhance MemberTransferService and MemberTransferDialog with detailed debug logging
Added extensive debug logging to the MemberTransferService to track member transfer details, including logging the first few members and bulk data structure. Updated the MemberTransferDialog to clear login credentials upon loading and closing, improving user experience and ensuring a clean state for configuration loading. These enhancements aim to facilitate troubleshooting and provide better visibility into the member transfer process.
2025-11-06 08:13:43 +01:00
Torsten Schulz (local)
d0a8ef5ff2 Enhance MemberTransferService with detailed debug logging for login credentials
Added comprehensive debug logging to the MemberTransferService to track login credentials and endpoint details during member transfers. Implemented checks for the presence of login credentials, including mapping username to email when necessary, and improved error handling for missing credentials. This enhancement aims to facilitate troubleshooting and improve the overall robustness of the login process.
2025-11-06 07:29:49 +01:00
Torsten Schulz (local)
a0d12a895e Enhance permission validation and error handling in permissionController
Updated the getUserPermissions function to include validation for clubId, ensuring it is a valid positive integer. Added error handling to return a 400 status with a descriptive message for invalid club IDs, improving the robustness of the API response.
2025-11-05 16:33:52 +01:00
Torsten Schulz (local)
ad99787f75 Enhance member transfer service and dialog for improved credential handling
Updated the MemberTransferService to better manage login credentials, ensuring that saved credentials are utilized when none are provided. Improved error handling during login attempts by extracting detailed error messages and adjusting status codes to prevent user logout. Refined the MemberTransferDialog to clear login credentials when no club is selected and to only include non-empty values in the login credentials object, enhancing user experience and validation logic.
2025-11-05 16:22:40 +01:00
Torsten Schulz (local)
c05cfbbe38 Refactor MemberTransferDialog to improve configuration handling and UI
Updated the MemberTransferDialog component to enhance user experience by displaying configuration summaries and handling missing configurations more effectively. Removed redundant form sections and added conditional rendering for login credentials. Improved validation logic for transfer settings and introduced new styles for better visual clarity.
2025-11-05 15:39:34 +01:00
Torsten Schulz (local)
1f47a11091 Add member transfer configuration and UI enhancements
Introduced MemberTransferConfig model and integrated it into the backend, allowing for the storage and retrieval of member transfer settings. Updated server routes to include member transfer configuration endpoints. Enhanced the frontend with a new MemberTransferDialog component for user interaction, added a dedicated route for member transfer settings, and updated the App.vue to include a link for accessing these settings. Improved the loading state and configuration handling in the dialog for better user experience.
2025-11-05 15:30:12 +01:00
Torsten Schulz (local)
5bba9522b3 Add member transfer functionality to memberController and update routes and UI
Implemented a new transferMembers function in memberController to handle member transfers, including validation for transfer endpoint and template. Updated memberRoutes to include a new route for member transfers. Enhanced MembersView with a button to open a transfer dialog and integrated a MemberTransferDialog component for user interaction during the transfer process.
2025-11-05 14:33:09 +01:00
Torsten Schulz (local)
5bdcd946cf Update diagonal cell styling in TournamentsView for improved visibility
Modified the CSS for diagonal cells in TournamentsView to change the background and text color from black to a lighter gray. This enhancement improves the visual clarity of the tournament display, making it easier for users to identify active matches.
2025-11-05 08:55:29 +01:00
Torsten Schulz (local)
6500493314 Enhance TournamentsView with diagonal cell styling for match rankings
Updated the TournamentsView to include a new CSS class for diagonal cells, improving the visual representation of match rankings. The diagonal cells now have distinct styling to indicate active matches, enhancing user interaction and clarity in tournament displays.
2025-11-05 08:49:27 +01:00
Torsten Schulz (local)
d0e3ae3610 Implement ranking logic for players with identical scores in TournamentsView
Updated the ranking assignment logic in TournamentsView to ensure players with identical points, set differences, and sets won receive the same position. This change enhances the accuracy of player rankings and improves the user experience by reflecting true standings in tournament scenarios. Additionally, adjusted the position retrieval for players in live stats to accommodate the new ranking logic.
2025-11-05 08:45:23 +01:00
Torsten Schulz (local)
8db827adeb Refactor participant list in DiaryView for improved layout and interaction
Updated the participant list in DiaryView to enhance the layout and user interaction. Introduced new CSS classes for better styling of participant rows, including flexbox for alignment and spacing. Improved the checkbox label structure and added a dedicated actions section for each participant, streamlining the user experience when managing participants. Additionally, modified the selectMember method to automatically open the notes modal for the selected participant.
2025-11-04 16:37:09 +01:00
Torsten Schulz (local)
d40eea5e46 Enhance DiaryView with member form handover functionality and styling improvements
Added a new feature to mark the member form as handed over, updating the member's status accordingly. Enhanced the participant list with conditional styling based on member activity and membership status. Introduced new CSS classes for better visual representation of inactive members and those with test memberships. Improved user interaction by adding an icon for marking the form handover.
2025-11-04 16:22:21 +01:00
Torsten Schulz (local)
a3ed130211 Add animation controls and enhance arrow drawing in CourtDrawingRender component
Implemented animation controls for arrow rendering in the CourtDrawingRender component, allowing users to start and stop animations. Enhanced the drawArrow method to support progressive rendering based on animation state. Added computed properties to determine the presence of arrows and updated the component's lifecycle methods for proper animation management. Improved styling for animation buttons to enhance user experience.
2025-11-04 15:54:18 +01:00
Torsten Schulz (local)
e8766b919a --- 2025-11-03 14:15:00 +01:00
Torsten Schulz (local)
76ee9ee742 Refactor memberController and memberService to include memberFormHandedOver field and improve parameter handling
Updated memberController to handle the showAll parameter more effectively and added memberFormHandedOver to the setClubMember method in memberService. Enhanced Member model to include memberFormHandedOver field with appropriate defaults. Updated MembersView to reflect changes in the UI, allowing for better member data management and visibility.
2025-11-03 12:36:13 +01:00
Torsten Schulz (local)
84ff4e126e Enhance MyTischtennisUrlController with ratings update and improve apiLogService truncation limits
Added functionality in MyTischtennisUrlController to update (Q)TTR ratings for clubs based on user authentication. Enhanced error handling for ratings updates to provide clearer feedback. Updated apiLogService to increase truncation limits for request and response bodies, accommodating larger API JSON payloads, ensuring better logging accuracy.
2025-11-03 12:03:34 +01:00
Torsten Schulz (local)
23708b99b5 Refactor error handling in MyTischtennisUrlController and improve memberService indexing
Refactored error handling in MyTischtennisUrlController to standardize error messages and ensure consistent status codes. Enhanced memberService by implementing a more efficient indexing system for member data retrieval, improving performance and accuracy in TTR and QTTR updates. Updated TeamManagementView to handle timeout errors and provide detailed user feedback, enhancing overall user experience.
2025-11-03 10:45:04 +01:00
Torsten Schulz (local)
acf2cf00bd Refactor MyTischtennisUrlController and enhance error handling in TeamManagementView
Refactored MyTischtennisUrlController to define variables outside of try/catch for better error handling. Improved error messaging in TeamManagementView by providing more detailed debug information and ensuring fallback messages are available. This enhances the robustness of data retrieval and user feedback.
2025-11-03 10:07:24 +01:00
Torsten Schulz (local)
bb3f0f3a03 Enhance error handling in MyTischtennisUrlController and update TrainingStatsView with TTR and QTTR columns
Improved error handling in MyTischtennisUrlController by adding detailed debug information in the response, including status codes and relevant data. Updated TrainingStatsView to display TTR and QTTR values for members, enhancing data visibility and user experience.
2025-11-03 09:46:26 +01:00
Torsten Schulz (local)
f4411a4ee5 Enhance club rankings retrieval and member TTR updates
Updated the getClubRankings method in myTischtennisClient to include an optional parameter for current rankings. Modified memberService to fetch both current and quarterly TTR values, improving member data accuracy. Enhanced the TeamManagementView to display (Q)TTR values for better user visibility. Added error handling for QTTR retrieval, ensuring robustness in member updates.
2025-11-03 09:35:04 +01:00
Torsten Schulz (local)
e32871a005 Implement deleteGroupActivity endpoint and enhance addGroupActivity functionality
Added a new endpoint to delete group activities in the diaryDateActivityController and corresponding route in diaryDateActivityRoutes. Enhanced the addGroupActivity function to accept an optional timeblockId parameter, allowing for more precise activity management. Updated the DiaryView component to support the removal of group activities, improving user experience and functionality in activity management.
2025-11-03 08:43:24 +01:00
Torsten Schulz (local)
a8318c74cf Implement last participations endpoint and enhance member activity retrieval
Added a new endpoint to fetch the last participations of a member, including group assignment checks for activities. Updated the member activity controller to include logic for filtering activities based on participant group IDs. Enhanced the DiaryView component to display activity statistics and last participations in a modal, improving user experience and data accessibility.
2025-10-31 16:33:20 +01:00
Torsten Schulz (local)
7e85926aa1 Refactor CourtDrawing components by removing overlay text and debug logs
Removed overlay text rendering and debug logging from CourtDrawingRender.vue and CourtDrawingTool.vue to streamline the drawing process. Updated DiaryView.vue to suppress debug messages and handle parsing errors more gracefully, enhancing overall code clarity and user experience.
2025-10-31 15:14:11 +01:00
Torsten Schulz (local)
91fc3e9d13 Implement Court Drawing Dialog and enhance activity management in DiaryView and PredefinedActivities
Added a Court Drawing Dialog component to facilitate the creation and editing of drawing data for activities. Updated DiaryView.vue to include a button for opening the drawing dialog and handling the resulting data. Enhanced PredefinedActivities.vue to allow users to create or edit drawing data directly from the activity form, improving the overall user experience. Refactored related styles and logic for better integration and usability.
2025-10-31 15:05:40 +01:00
Torsten Schulz (local)
6a333f198d Enhance CourtDrawing components with new rendering logic and overlay features
Updated CourtDrawingRender.vue to include an overlay displaying the rendered code string and raw values for debugging. Refactored position resolution logic to streamline the determination of start positions. Added methods for building and applying render codes, improving the overall functionality of the drawing tool. Enhanced CourtDrawingTool.vue with updated button styles and layout adjustments for better user experience. Updated DiaryView.vue to ensure proper handling of drawing data and start positions from render codes.
2025-10-30 15:24:57 +01:00
Torsten Schulz (local)
7ea719c178 Enhance CourtDrawing components with additional target selection and arrow drawing features
Added new target selection buttons for main and additional strokes in CourtDrawingTool.vue, allowing users to explicitly choose target positions. Updated CourtDrawingRender.vue to support drawing up to three additional arrows, alternating between left and right sides, with distinct colors for each stroke. Improved the logic for determining the next stroke side and updated related methods for better clarity and functionality.
2025-10-30 11:09:55 +01:00
Torsten Schulz (local)
e23d9fbc44 Update testMembership flag in DiaryView to enable testing mode 2025-10-30 08:32:04 +01:00
Torsten Schulz (local)
3f2b92d886 Refactor backend configuration and enhance logging in services
Updated config.js to ensure .env is loaded correctly from the backend directory. Enhanced MyTischtennisUrlController by removing unnecessary console logs and improving error handling. Updated autoFetchMatchResultsService and autoUpdateRatingsService to return detailed summaries, including counts of fetched or updated items. Improved logging in schedulerService to capture execution details, enhancing monitoring capabilities across scheduled tasks.
2025-10-30 08:14:17 +01:00
Torsten Schulz (local)
89329607dc Enhance myTischtennis URL controller with improved error handling and logging
Updated MyTischtennisUrlController to include detailed error messages for missing user IDs and team configurations. Added logging for session details and automatic login attempts, improving debugging capabilities. Enhanced request logging for API calls to myTischtennis, ensuring all requests are logged, even in case of errors. Updated requestLoggingMiddleware to only log myTischtennis-related requests, streamlining log management. Improved validation checks in autoFetchMatchResultsService for team and league data integrity.
2025-10-29 18:07:43 +01:00
Torsten Schulz (local)
c2b8656783 Refactor request logging middleware to simplify endpoint exclusion logic
Updated the requestLoggingMiddleware to streamline the logic for excluding specific endpoints from logging. The new implementation checks for 'status' in the path and handles root paths more efficiently, improving code readability and maintainability.
2025-10-29 13:41:31 +01:00
Torsten Schulz (local)
0b1e745f03 Add API logging functionality and enhance scheduler service
Introduced ApiLog model and integrated logging for scheduled tasks in the SchedulerService. Updated server.js to include request logging middleware and new API log routes. Enhanced frontend navigation by adding a link to system logs for admin users. Adjusted session check interval in App.vue for improved performance. This update improves monitoring and debugging capabilities across the application.
2025-10-29 13:35:25 +01:00
Torsten Schulz (local)
7a35a0a1d3 Update server port and enhance participant management features
Changed the server port from 3000 to 3005 for local development. Enhanced the participant management functionality by adding a new endpoint to update participant group assignments, including error handling for non-existent participants. Updated the participant model to include a groupId reference, and modified the participant retrieval logic to include group information. Additionally, improved the frontend API client to accommodate the new backend structure and added filtering options in the MembersView for better user experience.
2025-10-29 11:48:24 +01:00
Torsten Schulz (local)
bb2164f666 Add league configuration endpoint and frontend integration for myTischtennis
Implemented a new POST endpoint in MyTischtennisUrlController to configure leagues from table URLs, including season creation logic. Updated myTischtennisRoutes to include the new route for league configuration. Enhanced the myTischtennisUrlParserService to support parsing of table URLs and added a method for decoding group names. Updated TeamManagementView.vue to prompt users for league configuration when a table URL is detected, providing feedback upon successful configuration and reloading relevant data.
2025-10-24 17:06:10 +02:00
Torsten Schulz (local)
d16f250f80 Refactor match and predefined activity services for improved functionality and user experience
Removed unnecessary logging from the MatchService to streamline performance. Enhanced the PredefinedActivityService by implementing a more intelligent search feature that splits queries into individual terms, allowing for more precise filtering of activities. Updated the frontend PredefinedActivities.vue to include a search input with real-time results and a clear search button, improving user interaction and accessibility.
2025-10-24 16:35:03 +02:00
Torsten Schulz (local)
c18b70c6f6 Update styles and enhance clipboard functionality in ScheduleView
Modified main.scss to adjust table header styles, changing font weight to 600 and text transformation to uppercase for improved readability. In ScheduleView.vue, updated clipboard copy functionality to provide user feedback with a visual confirmation when copying codes and PINs, enhancing user experience. The feedback mechanism now includes style changes for better visibility and a reset after a brief display period.
2025-10-24 15:56:59 +02:00
Torsten Schulz (local)
67f4f728fe Refactor PDF table generation to support manual column exclusion
Updated the addTable method in PDFGenerator.js to enhance the functionality for excluding specified columns when generating PDF tables. This change allows for more flexible table rendering by manually parsing the HTML table if columns are to be excluded, improving the overall usability of the PDF generation feature.
2025-10-20 23:43:14 +02:00
Torsten Schulz (local)
b69684ad03 Enhance PDF generation by adding column exclusion functionality in PDFGenerator
Updated PDFGenerator.js to allow exclusion of specified columns when generating tables in PDFs. Modified ScheduleView.vue to determine which columns to exclude based on the selected league, improving the flexibility and usability of the PDF generation feature.
2025-10-20 23:40:05 +02:00
Torsten Schulz (local)
4ff021a85c Refactor schedule view to improve PDF generation and enhance element referencing
Updated ScheduleView.vue to replace direct DOM querying with Vue's ref system for better maintainability. The 'flex-item' div now uses a reference for PDF generation, improving code clarity and performance. This change aligns with best practices in Vue component design.
2025-10-20 23:36:31 +02:00
Torsten Schulz (local)
f1b37d131f Refactor predefined activity routes to simplify permission checks and enhance manual navigation in the frontend
Updated backend predefined activity routes to remove explicit permission checks, allowing for streamlined authentication. Modified frontend App.vue to eliminate automatic redirection to the training-stats page, enabling users to navigate manually. This change improves user experience by providing more control over navigation.
2025-10-17 12:48:58 +02:00
Torsten Schulz (local)
48bbc8015b Enhance permission management by adding caching control and improving permission parsing
Implement middleware to disable caching for permission routes, ensuring up-to-date responses. Update permission parsing logic in the backend to handle JSON strings more robustly, preventing errors during permission retrieval. Enhance the frontend PermissionsView with improved UI elements for managing permissions, including reset functionality and better state representation for actions. Ensure that only explicitly set permissions are saved, optimizing data handling.
2025-10-17 11:55:43 +02:00
Torsten Schulz (local)
56f0ce2f27 Implement permission management and enhance user interface for permissions in the application
Add new permission routes and integrate permission checks across various existing routes to ensure proper access control. Update the UserClub model to include role and permissions fields, allowing for more granular user access management. Enhance the frontend by introducing a user dropdown menu for managing permissions and displaying relevant options based on user roles. Improve the overall user experience by implementing permission-based visibility for navigation links and actions throughout the application.
2025-10-17 09:44:10 +02:00
Torsten Schulz (local)
2dd5e28cbc Add manual trigger endpoints for scheduler service in sessionRoutes
Introduce new POST endpoints for triggering rating updates and fetching match results, along with a GET endpoint for retrieving scheduler status. Enhance error handling and response formatting for better API usability.
2025-10-17 08:10:26 +02:00
Torsten Schulz (local)
c74217f6d8 Add member activity routes and UI enhancements in MembersView
Integrate member activity management by adding new routes in the backend for member activities. Update MembersView.vue to include a button for opening the activities modal and implement the MemberActivitiesDialog component for displaying member activities. Enhance the UI with new button styles for better user interaction.
2025-10-16 22:36:49 +02:00
Torsten Schulz (local)
01bbb85485 Enhance diary member activity management by adding validation and logging in addMembersToActivity function. Implement checks for participantIds to ensure they are an array, and log relevant information for better debugging. Update DiaryDateActivityService to improve error handling and logging for group activity associations. Modify frontend DiaryView to support group activity member assignment, including new methods for toggling and assigning members to group activities, enhancing user experience and functionality. 2025-10-16 22:20:51 +02:00
Torsten Schulz (local)
24aaa9c150 Update favicon links and Open Graph/Twitter image metadata in index.html for improved branding and social sharing. Replace existing favicon with multiple formats and update image references for better compatibility across platforms. 2025-10-16 21:36:59 +02:00
Torsten Schulz (local)
ea3cca563b Enhance match management functionality by adding player selection capabilities. Introduce new endpoints for updating match players and retrieving player match statistics in matchController and matchService. Update Match model to include fields for players ready, planned, and played. Modify frontend components to support player selection dialog, allowing users to manage player statuses effectively. Improve UI for better user experience and data visibility. 2025-10-16 21:09:13 +02:00
Torsten Schulz (local)
e0d56ddadd Enhance MyTischtennis fetch logging in AutoFetchMatchResultsService and AutoUpdateRatingsService. Integrate logging for match results and league table fetch attempts, including success status and execution details. Update updateRatings method to utilize memberService for fetching ratings, improving error handling and logging consistency. Update .gitignore to exclude backend log files. 2025-10-16 18:53:28 +02:00
Torsten Schulz (local)
32f06d7399 Refactor MyTischtennis URL controller to streamline match results and league table fetching. Remove redundant logging and execution time tracking for match results, while ensuring successful fetch counts are accurately reported in the response. Simplify error handling for league table updates without failing the entire request. 2025-10-14 23:31:12 +02:00
Torsten Schulz (local)
36bf99c013 Add MyTischtennis fetch log functionality and new endpoints
Enhance MyTischtennis integration by introducing fetch log capabilities. Implement new controller methods to retrieve fetch logs and latest successful fetches for users. Update routes to include these new endpoints. Modify the MyTischtennis model to support fetch logs and ensure proper logging of fetch operations in various services. Update frontend components to display fetch statistics, improving user experience and data visibility.
2025-10-14 23:07:57 +02:00
Torsten Schulz (local)
7549fb5730 Implement league table functionality and MyTischtennis integration. Add new endpoints for retrieving and updating league tables in matchController and matchRoutes. Enhance Team model with additional fields for match statistics. Update frontend components to display league tables and allow fetching data from MyTischtennis, improving user experience and data accuracy. 2025-10-14 22:55:39 +02:00
Torsten Schulz (local)
1517d83f6c Refactor backend to enhance MyTischtennis integration. Update package.json to change main entry point to server.js. Modify server.js to improve scheduler service logging. Add new fields to ClubTeam, League, Match, and Member models for MyTischtennis data. Update routes to include new MyTischtennis URL parsing and configuration endpoints. Enhance services for fetching team data and scheduling match results. Improve frontend components for MyTischtennis URL configuration and display match results with scores. 2025-10-14 21:58:21 +02:00
Torsten Schulz (local)
993e12d4a5 Update MyTischtennis functionality to support automatic rating updates. Introduce new autoUpdateRatings field in MyTischtennis model and enhance MyTischtennisController to handle update history retrieval. Integrate node-cron for scheduling daily updates at 6:00 AM. Update frontend components to allow users to enable/disable automatic updates and display last update timestamps. 2025-10-09 00:18:41 +02:00
Torsten Schulz (local)
806cb527d4 Refactor group creation and assignment logic in DiaryView. Update group creation to allow specifying the number of groups, enhance participant assignment with group selection, and improve UI elements for better user experience. 2025-10-08 19:21:15 +02:00
Torsten Schulz (local)
7e9d2d2c4f Update member count calculations in MembersView to exclude inactive test memberships. Adjust activeMembersCount and testMembersCount computed properties for improved accuracy in member statistics. 2025-10-08 18:15:18 +02:00
Torsten Schulz (local)
ec9b92000e Update Member model to allow optional birthDate and enhance QuickAddMemberDialog for better input handling. Refactor DiaryView to remove default birthDate logic and improve member creation process. Adjust MembersView to handle empty birthDate gracefully in formatting. 2025-10-08 18:06:22 +02:00
Torsten Schulz (local)
d110900e85 Implement member statistics dropdown in MembersView. Add computed properties for active, test, and inactive member counts, and introduce a toggle for displaying member info. Enhance styling for the dropdown and member stats for improved user experience. 2025-10-08 17:47:43 +02:00
Torsten Schulz (local)
cd3c3502f6 Add tournament name column to OfficialTournaments view. Update data structure to include tournament names for better clarity in participation details. 2025-10-08 15:44:37 +02:00
Torsten Schulz (local)
ccce9bffac Enhance MembersView by adding a new row style for test memberships. Update the member row class to include 'row-test' for better visual distinction of test members, improving user experience in member management. 2025-10-08 15:00:38 +02:00
Torsten Schulz (local)
f1ba25f9f5 Implement logic to determine the quarter with the highest training data in TrainingStatsView. Update average participation calculation to reflect the best quarter's total participants, enhancing accuracy in participation metrics. 2025-10-08 14:54:57 +02:00
Torsten Schulz (local)
548f51ac54 Refactor participation metrics in TrainingStatsView to use total participant counts instead of member participation estimates. Update methods for calculating average participation across different time periods to enhance accuracy and maintainability. 2025-10-08 14:47:30 +02:00
Torsten Schulz (local)
946e4fce1e Enhance SeasonSelector and TeamManagementView with dialog components for improved user interaction. Introduce new dialog states and helper methods for consistent handling of information and confirmations. Update styles in TrainingStatsView to reflect new participation metrics and improve layout. Refactor document display in TeamManagementView to a table format for better readability. 2025-10-08 14:43:53 +02:00
Torsten Schulz (local)
40dcd0e54c Refactor modals in DiaryView, MembersView, OfficialTournaments, ScheduleView, and TrainingStatsView to use dedicated dialog components for improved maintainability and user experience. Update styles and structure for consistency across the application. 2025-10-08 12:49:42 +02:00
Torsten Schulz (local)
bd338b86df Implementiert InfoDialog und ConfirmDialog in mehreren Komponenten, um die Benutzerinteraktion zu verbessern. Fügt Dialogzustände und Hilfsmethoden hinzu, die eine konsistente Handhabung von Informationen und Bestätigungen ermöglichen. Diese Änderungen erhöhen die Benutzerfreundlichkeit und verbessern die visuelle Rückmeldung in der Anwendung. 2025-10-08 11:46:07 +02:00
Torsten Schulz (local)
1d4aa43b02 Aktualisiert das Styling in App.vue durch Hinzufügen von Padding am unteren Rand für die Statusleiste. Ändert den Hintergrund und die Polsterung in DialogManager.vue, um das visuelle Design zu verbessern und die Benutzeroberfläche zu optimieren. 2025-10-08 11:22:45 +02:00
Torsten Schulz (local)
cc08f4ba43 Verbessert die Lesbarkeit und Struktur des Codes in DiaryView.vue durch Anpassungen der Einrückungen und Formatierungen. Optimiert die Anzeige von Aktivitätsvisualisierungen und aktualisiert die Logik für die Eingabefelder, um die Benutzerfreundlichkeit zu erhöhen. Diese Änderungen tragen zur allgemeinen Verbesserung der Benutzeroberfläche und der Codequalität bei. 2025-10-08 11:14:20 +02:00
Torsten Schulz (local)
d0ccaa9e54 Fügt eine neue Methode hasActivityVisual in DiaryView.vue hinzu, um die Sichtbarkeit von Aktivitätsvisualisierungen zu überprüfen. Aktualisiert die Bedingungen für die Anzeige von Icons, die Bilder oder Zeichnungen darstellen, um die Benutzeroberfläche zu verbessern und die Logik zu optimieren. 2025-10-08 11:00:20 +02:00
Torsten Schulz (local)
dc0eff4e4c Entfernt die PDF-Datei 9_code_list_1759357969975.pdf und implementiert eine Sidebar-Toggle-Funktionalität in App.vue. Die Sidebar kann nun auf mobilen Geräten ein- und ausgeklappt werden, um die Benutzeroberfläche zu optimieren. Zudem wurden Titelattribute zu Navigationslinks hinzugefügt, um die Benutzerfreundlichkeit zu verbessern. Der Vuex-Store wurde aktualisiert, um den Zustand der Sidebar zu speichern und zu verwalten. 2025-10-08 10:52:07 +02:00
Torsten Schulz (local)
db9e404372 Aktualisiert die Logik zum Löschen eines Datums in DiaryView.vue. Die Schaltfläche zum Löschen wird nun nur angezeigt, wenn keine Inhalte (Trainingplan, Teilnehmer, Aktivitäten, Unfälle oder Notizen) vorhanden sind. Fügt eine neue Methode canDeleteCurrentDate hinzu, die diese Überprüfung durchführt, um die Benutzerfreundlichkeit zu verbessern und versehentliche Löschungen zu verhindern. 2025-10-04 02:30:23 +02:00
Torsten Schulz (local)
60ac89636e Ändert das Eingabefeld für den Nachnamen in der Mitgliederregistrierung auf optional und aktualisiert die Validierungslogik entsprechend. Setzt ein Standard-Geburtsdatum für neue Mitglieder auf den 01.01. des aktuellen Jahres minus 10 Jahre, wenn kein Geburtsdatum eingegeben wird. Diese Änderungen verbessern die Benutzerfreundlichkeit und Flexibilität bei der Registrierung neuer Mitglieder. 2025-10-04 02:27:49 +02:00
Torsten Schulz (local)
2b1365339e Fügt die Funktion zum Drehen von Mitgliedsbildern hinzu. Implementiert die Logik zur Bildrotation in MemberService und aktualisiert die entsprechenden Routen und Frontend-Komponenten, um die Benutzeroberfläche für die Bildbearbeitung zu verbessern. Ermöglicht das Drehen von Bildern über die Mitgliederansicht und aktualisiert die Anzeige nach der Bearbeitung. 2025-10-04 01:59:21 +02:00
Torsten Schulz (local)
0cf2351c79 Erweitert die Funktionalität von updateRatingsFromMyTischtennis, um eine automatische Anmeldung bei abgelaufener Session zu ermöglichen. Fügt Fehlerbehandlung für den Login-Prozess hinzu und entfernt die Bestätigungsabfrage vor der Aktualisierung der TTR/QTTR-Werte, um den Benutzerfluss zu verbessern. 2025-10-04 01:49:30 +02:00
Torsten Schulz (local)
5c32fad34e Fügt eine bedingte Navigation in App.vue hinzu, die eine leere Navigationsleiste anzeigt, wenn keine Authentifizierung vorliegt. Diese Änderung verbessert die Benutzeroberfläche und sorgt für eine klarere Struktur der Navigation. 2025-10-04 01:43:37 +02:00
Torsten Schulz (local)
7f0b681e88 Ermöglicht die Bearbeitung von Spielergebnissen in TournamentsView.vue durch klickbare Labels und editierbare Eingabefelder. Fügt Logik zum Speichern und Abbrechen von Änderungen hinzu. Aktualisiert das Styling für Eingabefelder und klickbare Texte, um die Benutzererfahrung zu verbessern. 2025-10-04 01:38:27 +02:00
Torsten Schulz (local)
e823af064e Erweitert den MatchReportApiDialog um neue Funktionen zur Anzeige und Verwaltung von Start- und Endzeiten. Fügt Buttons zum Setzen der aktuellen Zeit für beide Zeitpunkte hinzu und implementiert eine Formatierungsfunktion für die Zeitdarstellung. Optimiert die Benutzeroberfläche mit neuen CSS-Stilen für die Zeitanzeigen und die Auto-Fill-Funktion. 2025-10-04 01:30:10 +02:00
Torsten Schulz (local)
3bc6a465a2 Erweitert den MatchReportApiDialog um eine neue Funktion zur Verwaltung des Abschlussstatus der Begrüßung. Fügt einen Toggle-Button hinzu, um den Status als abgeschlossen oder nicht abgeschlossen zu kennzeichnen, und aktualisiert die Benutzeroberfläche mit entsprechenden visuellen Hinweisen. Implementiert Logik zum automatischen Wechsel zur Ergebniserfassung nach Abschluss der Begrüßung. 2025-10-04 01:06:10 +02:00
Torsten Schulz (local)
e8b6578bd4 Erweitert den MatchReportApiDialog um eine neue Validierungslogik zur Überprüfung, ob beide Mannschaften genügend Spieler für den ALLGAMES-Modus haben. Implementiert die Methode canBothTeamsPlayAllGames, die die Spieleranzahl basierend auf verschiedenen Spielsystemen überprüft. Aktualisiert die Benutzeroberfläche mit neuen Warnungen und Rückmeldungen für unvollständige Mannschaften und ermöglicht das Absenden des Berichts trotz unvollständiger Spieleranzahl. 2025-10-04 00:54:41 +02:00
Torsten Schulz (local)
280c1432b7 Fügt eine Auto-Fill-Funktion für leere Matches im MatchReportApiDialog hinzu. Implementiert einen neuen Header mit einem Button zum automatischen Ausfüllen, der die Spielergebnisse für unvollständige Matches basierend auf vorhandenen Spielern automatisch ausfüllt. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für den Header und den Button. 2025-10-04 00:51:25 +02:00
Torsten Schulz (local)
fd82efdcee Optimiert die Spieleranzahl-Berechnung im MatchReportApiDialog, um die Braunschweiger Regel zu berücksichtigen. Implementiert die Methode getEffectivePlayerCount, die die Spieleranzahl für 2er-Mannschaften als 3 zählt. Aktualisiert die Logik zur Überprüfung der verfügbaren Spieler für Positionen und verbessert die Benutzeroberfläche durch präzisere Rückmeldungen. 2025-10-04 00:45:45 +02:00
Torsten Schulz (local)
e354d82969 Erweitert den MatchReportApiDialog um umfassende Validierungslogik für die Eingabe von Spielberichten. Implementiert neue Warnungen für unvollständige oder fehlerhafte Match-Ergebnisse sowie für ungültige Start- und Endzeiten. Aktualisiert die Logik zur Aktivierung und Deaktivierung des Absende-Buttons basierend auf den Validierungsbedingungen. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für Validierungsbenachrichtigungen und optimiert die Benutzererfahrung durch dynamische Rückmeldungen. 2025-10-03 23:42:52 +02:00
Torsten Schulz (local)
049ee56571 Erweitert den MatchReportApiDialog um neue Validierungsfunktionen zur Überprüfung von Satz-Inputs und zur Handhabung von Lücken in der Eingabe. Implementiert Methoden zur Berechnung gewonnener Sätze und zur Deaktivierung von Eingabefeldern, wenn ein Spieler bereits 3 Sätze gewonnen hat. Fügt visuelle Warnungen für problematische Eingaben hinzu und verbessert die Benutzeroberfläche mit neuen CSS-Stilen für Eingabefelder. 2025-10-03 23:11:18 +02:00
Torsten Schulz (local)
c6bb534a0d Erweitert den MatchReportApiDialog um Validierungslogik für Mindestspielerzahlen und Doppel. Fügt Methoden zur Überprüfung der Spieleranzahl und Doppelformationen basierend auf dem Spielmodus hinzu. Implementiert Fehlermeldungen zur Anzeige von Validierungsfehlern für Heim- und Gastteams. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für Fehlermeldungen. 2025-10-03 23:01:13 +02:00
Torsten Schulz (local)
a0fdf256e7 Fügt neue Funktionen zur Bearbeitung von Spielberichten im MatchReportApiDialog hinzu. Implementiert eine Anzeige für den Abschlussstatus des Spiels, einschließlich Warnungen und Deaktivierungen von Eingabefeldern, wenn das Match bereits abgeschlossen ist. Aktualisiert die Logik zur Berechnung der Gesamtpunkte und Sätze sowie zur Validierung von PINs. Verbessert die Benutzeroberfläche mit neuen CSS-Stilen für abgeschlossene Matches und Dialoge zur Anzeige von Match-Daten. 2025-10-03 22:49:05 +02:00
Torsten Schulz (local)
d23a9f086c Erweitert den MatchReportApiDialog mit neuen Funktionen zur Abschlussbearbeitung. Fügt die Anzeige von Aufstellungen, Endergebnis, Zeitangaben, Protest-Eingabe und PIN-Eingaben hinzu. Implementiert Methoden zur Extraktion von Einzel- und Doppelspielern sowie zur Berechnung des Gesamtergebnisses. Optimiert die Benutzeroberfläche mit neuen CSS-Stilen für eine verbesserte Darstellung. 2025-10-03 20:13:49 +02:00
Torsten Schulz (local)
ac727c6c5b Fügt die Funktionalität zur Aktualisierung der Vereinseinstellungen hinzu. Implementiert die Methode updateClubSettings im clubsController, um Begrüßungstexte und Mitgliedsnummern zu aktualisieren. Aktualisiert das Club-Modell, um neue Felder für greetingText und associationMemberNumber zu unterstützen. Ergänzt die Routen in clubRoutes, um die neuen Einstellungen zu verarbeiten. Fügt eine neue Ansicht für die Vereins-Einstellungen im Frontend hinzu und aktualisiert die Navigation entsprechend. 2025-10-03 19:49:19 +02:00
Torsten Schulz (local)
4b1a046149 Fügt Unterstützung für die neue nuscore API hinzu. Aktualisiert die Backend-Routen zur Verarbeitung von Anfragen an die nuscore API und integriert die neuen Dialogkomponenten im Frontend. Ermöglicht das Erstellen lokaler Kopien von nuscore-Daten und verbessert die Benutzeroberfläche durch neue Schaltflächen und Dialoge. Entfernt veraltete Konsolenausgaben und optimiert die Logik zur PIN-Verwaltung. 2025-10-03 15:57:57 +02:00
Torsten Schulz (local)
cc964da9cf Fügt einen Dialog-Manager hinzu, um die Verwaltung von Dialogen zu ermöglichen. Aktualisiert den Vuex-Store mit neuen Mutationen und Aktionen zur Handhabung von Dialogen. Integriert den MatchReportDialog in ScheduleView.vue und ermöglicht das Öffnen von Spielberichten über die Benutzeroberfläche. Verbessert die Benutzererfahrung durch neue Schaltflächen und CSS-Stile für die Dialoge. 2025-10-02 11:44:27 +02:00
Torsten Schulz (local)
dbede48d4f Entfernt Konsolenausgaben aus der MyTischtennisClient-Klasse, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Methoden getUserProfile und getClubRankings und tragen zur Optimierung der Protokollierung und Performance bei. 2025-10-02 10:40:24 +02:00
Torsten Schulz (local)
6cd3c3a020 Entfernt Konsolenausgaben aus verschiedenen Controllern und Services, um die Codequalität zu verbessern und die Lesbarkeit zu erhöhen. Diese Änderungen betreffen die Controller für Clubs, Club-Teams, Mitglieder, Tagebuch-Tags, Saisons und Teams sowie die zugehörigen Services. Ziel ist es, die Protokollierung zu optimieren und die Performance zu steigern. 2025-10-02 10:34:56 +02:00
Torsten Schulz (local)
7ecbef806d Entfernt Konsolenausgaben aus mehreren Komponenten, um den Code zu bereinigen und die Lesbarkeit zu verbessern. Betroffene Dateien sind CourtDrawingRender.vue, CourtDrawingTool.vue, SeasonSelector.vue, PredefinedActivities.vue, ScheduleView.vue, TeamManagementView.vue und TournamentsView.vue. Diese Änderungen tragen zur Optimierung der Performance und zur Reduzierung von unnötigen Protokollierungen bei. 2025-10-02 10:13:03 +02:00
Torsten Schulz (local)
1c70ca97bb Fügt Unterstützung für Team-Dokumente hinzu. Aktualisiert die Backend-Modelle und -Routen, um Team-Dokumente zu verwalten, einschließlich Upload- und Parsing-Funktionen für Code- und Pin-Listen. Ergänzt die Benutzeroberfläche in TeamManagementView.vue zur Anzeige und Verwaltung von Team-Dokumenten sowie zur Integration von PDF-Parsing. Aktualisiert die Match-Modelle, um zusätzliche Felder für Spiel-Codes und PINs zu berücksichtigen. 2025-10-02 09:04:19 +02:00
Torsten Schulz (local)
a6493990d3 Erweitert die Backend- und Frontend-Funktionalität zur Unterstützung von Teams und Saisons. Fügt neue Routen für Team- und Club-Team-Management hinzu, aktualisiert die Match- und Team-Modelle zur Berücksichtigung von Saisons, und implementiert die Saison-Auswahl in der Benutzeroberfläche. Optimiert die Logik zur Abfrage von Ligen und Spielen basierend auf der ausgewählten Saison. 2025-10-01 22:47:13 +02:00
Torsten Schulz (local)
f8f4d23c4e Aktualisiert die Schaltflächen im MyTischtennisAccount.vue, um die Benutzeroberfläche zu verbessern. Ändert den Text der Schaltfläche "Verbindung testen" in "Erneut einloggen" und entfernt die Testausgabe für Login-Tests. Optimiert die Erfolgsmeldung nach erfolgreichem Login und aktualisiert die Account-Daten. Entfernt die nicht mehr benötigte Funktionalität für den Login-Flow-Test. 2025-10-01 13:52:14 +02:00
Torsten Schulz (local)
1ef1711eea Merge branch 'main' into httv 2025-10-01 13:49:26 +02:00
Torsten Schulz (local)
85981a880d Ändert die Navigationsstruktur in App.vue, indem die Klasse der Fußzeile von "nav-menu" zu "sidebar-footer" geändert wird. Fügt eine neue CSS-Klasse "nav-menu-no-flex" hinzu, um das Layout der Navigation zu optimieren und die Flexbox-Eigenschaften anzupassen. 2025-10-01 13:49:13 +02:00
Torsten Schulz (local)
84503b6404 Merge branch 'httv' 2025-10-01 13:31:41 +02:00
Torsten Schulz (local)
bcc3ce036d Ersetzt Konsolenausgaben durch eine bedingte Entwicklungsprotokollierungsfunktion in mehreren Controllern und Services. Dies verbessert die Protokollierung und Fehlerverfolgung im gesamten Code. Aktualisiert die Benutzer-Utils, um die neue Protokollierungsfunktion zu verwenden. 2025-10-01 13:29:49 +02:00
Torsten Schulz (local)
0fe0514660 Verbessert die Protokollierung in den Club-Controller- und Benutzer-Utils-Dateien, indem die Konsolenausgaben durch eine bedingte Entwicklungsprotokollierungsfunktion ersetzt werden. Aktualisiert die Fehlerbehandlung, um detailliertere Fehlermeldungen auszugeben. 2025-10-01 13:24:16 +02:00
Torsten Schulz (local)
431ec861ba Erweitert die Trainingsstatistik-Funktionalität im TrainingStatsController um die Abfrage und Formatierung von Trainingstagen der letzten 12 Monate. Aktualisiert die Benutzeroberfläche in TrainingStatsView.vue zur Anzeige dieser Trainingstage in einer aufklappbaren Tabelle. Fügt Funktionen zum Umschalten der Sichtbarkeit von Trainingstagen und Mitgliedern hinzu. 2025-10-01 13:20:36 +02:00
Torsten Schulz (local)
648b608036 Erweitert die Trainingsstatistik-Funktionalität im TrainingStatsController, um die Anzahl der Trainings in den letzten 12 und 3 Monaten zu berechnen und zurückzugeben. Aktualisiert die Benutzeroberfläche in TrainingStatsView.vue zur Anzeige dieser neuen Daten. Ändert die Navigation in App.vue, um direkt zu den Trainingsstatistiken zu führen. 2025-10-01 13:01:54 +02:00
Torsten Schulz (local)
4ac71d967f Fügt Unterstützung für myTischtennis-Integration hinzu. Aktualisiert die Mitglieder-Controller und -Routen, um die Aktualisierung von TTR/QTTR-Werten zu ermöglichen. Ergänzt die Benutzeroberfläche in MembersView.vue zur Aktualisierung der Bewertungen und fügt neue Routen für die myTischtennis-Daten hinzu. Aktualisiert die Datenmodelle, um die neuen Felder für TTR und QTTR zu integrieren. 2025-10-01 12:09:55 +02:00
Torsten Schulz (local)
75d304ec6d Merge branch 'activitypainter' 2025-10-01 09:42:33 +02:00
Torsten Schulz (local)
afd96f5df1 Optimiert die Berechnung der Startposition im CourtDrawingRender.vue, indem die Offset-Logik für vertikale und horizontale Zeichnungen präzisiert wird. Kommentiert den Code zur besseren Verständlichkeit der Offset-Berechnungen. 2025-10-01 09:42:15 +02:00
Torsten Schulz (local)
4bfa6a5889 Erweitert die Funktionalität in CourtDrawingRender.vue und CourtDrawingTool.vue zur Verbesserung der Zeichnungslogik. Fügt neue Offset-Parameter für Zielkreise hinzu und optimiert die Berechnung der Zielpositionen. Entfernt die Schaltflächen für das manuelle Speichern in CourtDrawingTool.vue zugunsten einer automatischen Speicherung. Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue zur Unterstützung der neuen Zeichnungsdaten-Logik. 2025-10-01 09:41:07 +02:00
Torsten Schulz (local)
144034a305 Verbessert die Benutzeroberfläche in ScheduleView.vue durch Hinzufügen von Funktionen zum Laden von Gesamt- und Erwachsenenspielplänen. Implementiert eine Hover-Info für Spiele mit Fallback-Werten und optimiert die Anzeige von Spielinformationen. Fügt neue CSS-Klassen für die Hervorhebung von Spielen heute und in der nächsten Woche hinzu. 2025-09-25 19:45:20 +02:00
Torsten Schulz (local)
f4187512ba Erweitert die Funktionalität zur Erstellung und Aktualisierung von vordefinierten Aktivitäten, indem das Feld für Zeichnungsdaten in den entsprechenden Controllern, Modellen und Services hinzugefügt wird. Aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue und PredefinedActivities.vue, um die Handhabung von Zeichnungsdaten zu verbessern und die Logik für das Laden und Speichern von Zeichnungen zu optimieren. 2025-09-25 19:35:13 +02:00
Torsten Schulz (local)
b557297bf0 Verbessert die Logik zur Erstellung von Aktivitäten im DiaryDateActivityService, um PredefinedActivities robuster zu finden. Fügt Unterstützung für die Suche nach Aktivitäten per ID, Name oder Code hinzu. Aktualisiert die Benutzeroberfläche in DiaryView.vue zur Anzeige von Zeichnungsdaten und integriert ein neues Rendering-Modal für Zeichnungen. Optimiert die Bildanzeige in CourtDrawingTool.vue und implementiert eine verbesserte Fehlerbehandlung beim Laden von Bildern. 2025-09-23 14:40:41 +02:00
Torsten Schulz (local)
eb2273e28c Aktualisiert die Token-Lebensdauer im Authentifizierungsdienst auf 3 Stunden und verbessert die Logik zur Auswahl der Startposition im CourtDrawingTool.vue, um eine Standard-Startposition festzulegen, wenn keine ausgewählt ist. 2025-09-23 09:13:51 +02:00
Torsten Schulz (local)
091599b745 Erweitert die Funktionalität in PredefinedActivityImageController.js, um Zeichnungsdaten aus dem Request zu extrahieren und in der Datenbank zu speichern. Aktualisiert das Datenmodell in PredefinedActivityImage.js, um ein neues Feld für Zeichnungsdaten hinzuzufügen. Passt die Routen in predefinedActivityRoutes.js an, um die neue PUT-Methode für das Hochladen von Bildern zu unterstützen. Integriert die Zeichnungsdaten in die Aktivitätenlogik in diaryDateActivityService.js und aktualisiert die Benutzeroberfläche in CourtDrawingTool.vue zur Unterstützung von Zeichnungsdaten. Verbessert die Handhabung von Bild-Uploads in PredefinedActivities.vue und implementiert die Logik zum Laden von Zeichnungsdaten beim Bearbeiten von Aktivitäten. 2025-09-23 08:39:13 +02:00
Torsten Schulz (local)
d70a5ca63e Erweitert die Funktionalität in PredefinedActivities.vue um die Möglichkeit, eine Übungszeichnung zu erstellen. Fügt ein Zeichen-Tool hinzu, das die Zeichnungsdaten speichert und automatisch als Bild-Link verwendet, wenn kein Bild-Link vorhanden ist. Aktualisiert die Benutzeroberfläche zur Bild- und Zeichnungshinzufügung. 2025-09-22 12:23:39 +02:00
Torsten Schulz (local)
09ffd1db3d Fügt die Funktionalität zum schnellen Hinzufügen von Mitgliedern in DiaryView.vue hinzu. Implementiert einen Dialog zur Eingabe von Mitgliedsdaten, einschließlich Vorname, Nachname, Geburtsdatum und Geschlecht. Aktualisiert die Logik zur Validierung neuer Mitglieder und zur Integration in die Mitgliederliste. 2025-09-21 19:25:30 +02:00
Torsten Schulz (local)
d90acf43e1 Verbessert die Benutzeroberfläche in OfficialTournaments.vue durch Anpassung der PDF-Generierungsfunktion. Die Schaltfläche zum Erzeugen von PDFs schließt nun das Dialogfeld automatisch. Aktualisiert die Logik zur Auswahl von Mitgliedern, um die ausgewählte ID beim Ändern des Status zu aktualisieren. 2025-09-21 18:49:10 +02:00
Torsten Schulz (local)
adb93af906 Fügt die Unterstützung für Teilnahmegebühren in officialTournamentController.js hinzu, einschließlich der Extraktion von Gebühren aus dem Turniertext. Aktualisiert das Datenmodell in OfficialTournament.js, um die Teilnahmegebühren zu speichern. Passt die Benutzeroberfläche in OfficialTournaments.vue an, um die Teilnahmegebühren anzuzeigen, und aktualisiert PDFGenerator.js, um die Gebühren im PDF-Dokument darzustellen. 2025-09-21 18:39:25 +02:00
Torsten Schulz (local)
a36f0ea446 Aktualisiert die Benutzeroberfläche in OfficialTournaments.vue zur Anzeige des Teilnehmerstatus mit neuen Status-Badges und Aktionsbuttons. Implementiert die Logik zur Aktualisierung des Status eines Teilnehmers, einschließlich der Optionen Anmelden, Teilnehmen und Zurücksetzen. Verbessert die Darstellung der Platzierungseingabe und optimiert das Styling für eine bessere Benutzererfahrung. 2025-09-21 18:11:16 +02:00
Torsten Schulz (local)
e4fcf2eca2 Fügt die Funktionalität zur Aktualisierung des Teilnehmerstatus in officialTournamentController.js hinzu. Implementiert die Route zum Aktualisieren des Status eines Teilnehmers in officialTournamentRoutes.js und passt die Benutzeroberfläche in OfficialTournaments.vue an, um den neuen Status anzuzeigen und Aktionen wie Anmelden, Teilnehmen und Zurücksetzen zu ermöglichen. 2025-09-21 18:05:50 +02:00
Torsten Schulz (local)
0ee16c7766 Fügt detaillierte Konsolenausgaben in TournamentService.js hinzu, um den Prozess der Match-Erstellung für Gruppen zu verfolgen. Aktualisiert das Styling in main.scss, um die Schriftartgewichtung auf 700 zu erhöhen und die Texttransformation zu entfernen. 2025-09-21 17:49:41 +02:00
Torsten Schulz (local)
21c19298da Fügt die Möglichkeit hinzu, Teilnehmer aus dem heutigen Trainingstag zu laden, einschließlich der Logik zur Überprüfung, ob heute ein Trainingstag stattfindet. Implementiert die Methode zum Laden der Teilnehmer und optimiert die Benutzeroberfläche mit einem neuen Button für diese Funktion. 2025-09-21 17:44:39 +02:00
Torsten Schulz (local)
3c65fed994 Fügt die Funktion zum Laden von Turnieren beim Start hinzu und optimiert die Turniererstellung, um die Turnierliste nach der Erstellung eines neuen Turniers automatisch zu aktualisieren. Verbessert die Anzeige von Turnierdaten, indem der Turniername priorisiert wird, und behandelt Fehler beim Laden und Erstellen von Turnieren. 2025-09-21 17:31:12 +02:00
Torsten Schulz (local)
66046ddccd Aktualisiert die Punktevergabe in TournamentService.js und TournamentsView.vue, sodass der Sieger +1 Punkt erhält und der Verlierer -1 Punkt. Fügt eine neue Methode getLivePosition hinzu, um die Live-Punkte und -Sätze der Spieler in der Gruppe zu berechnen und anzuzeigen. Optimiert die Darstellung der Platzierung in der Tabelle. 2025-09-21 17:25:23 +02:00
Torsten Schulz (local)
561d8186d3 Verbessert die Logik zur Zuordnung von Teilnehmern in TournamentService.js, indem manuelle Zuordnungen berücksichtigt werden. Implementiert eine zufällige Verteilung der Teilnehmer nur, wenn keine manuelle Zuordnung vorhanden ist. Aktualisiert die Erstellung von Matches, um sicherzustellen, dass nur Spieler aus derselben Gruppe gegeneinander antreten. In TournamentsView.vue wird die Teilnehmerliste jetzt kollabierbar, und es werden neue Funktionen zur Anzeige von Spielergebnissen und zur Hervorhebung von Matches hinzugefügt. 2025-09-21 17:16:47 +02:00
Torsten Schulz (local)
312f8f24ab Optimiert das Styling in DiaryView.vue, indem die Bildgrößenanpassung auf viewport-basierte Einheiten umgestellt wird. Entfernt Margen und Polsterungen für eine bessere Darstellung der Bilder im Overlay. 2025-09-16 00:16:54 +02:00
Torsten Schulz (local)
ba4b56360d Optimiert die Darstellung von vordefinierten Aktivitäten in DiaryView.vue, indem die Logik zur Anzeige von Aktivitätsnamen und -codes verbessert wird. Aktualisiert das Styling der Bildsymbole und passt die Positionierung der Bilder an, um eine bessere Benutzererfahrung zu gewährleisten. 2025-09-16 00:14:30 +02:00
Torsten Schulz (local)
02732a01da Fügt die Möglichkeit hinzu, Bilder vordefinierter Aktivitäten in DiaryView.vue anzuzeigen. Implementiert die Methode showActivityImage zur Anzeige des Bildes und aktualisiert das Styling für die Bildsymbole. 2025-09-15 23:58:48 +02:00
Torsten Schulz (local)
4307fa7d82 Entfernt die Authentifizierung von der Route zum Abrufen von vordefinierten Aktivitätsbildern in predefinedActivityRoutes.js. Aktualisiert den Alt-Text für Bilder in PredefinedActivities.vue von "Activity Image" auf "Predefined Activity Image". 2025-09-15 23:56:18 +02:00
Torsten Schulz (local)
a1dc6afb2c Ändert die Zugriffskontrolle in predefinedActivityImageController.js von checkAccess zu checkGlobalAccess, um die globale Authentifizierung für vordefinierte Aktivitäten zu ermöglichen. Fügt die Funktion checkGlobalAccess in userUtils.js hinzu, die die Benutzerinformationen basierend auf dem Token zurückgibt. 2025-09-15 23:53:49 +02:00
Torsten Schulz (local)
92ce64b807 Fügt die Funktion zum Löschen von vordefinierten Aktivitätsbildern hinzu. Implementiert die Logik in der Datei predefinedActivityImageController.js und aktualisiert die Routen in predefinedActivityRoutes.js. Ergänzt die Benutzeroberfläche in PredefinedActivities.vue um die Möglichkeit, hochgeladene Bilder anzuzeigen und zu löschen. 2025-09-15 23:46:59 +02:00
Torsten Schulz (local)
296939d1a0 Entfernt die Deaktivierung des "Teilnehmer-PDF"-Buttons in OfficialTournaments.vue, um die Benutzerfreundlichkeit zu verbessern und die PDF-Generierung jederzeit zu ermöglichen. 2025-09-12 14:30:53 +02:00
Torsten Schulz (local)
dc8a5778d6 Implementiert die Funktion zur Generierung eines Teilnehmer-PDFs in OfficialTournaments.vue. Fügt die Methode addParticipantsSummary in PDFGenerator.js hinzu, um eine Zusammenfassung der Teilnehmerdaten in einem PDF-Dokument darzustellen. Integriert die Logik zur Gruppierung und Formatierung der Teilnehmerinformationen basierend auf ihrem Anmeldestatus und der Teilnahme. 2025-09-12 14:23:47 +02:00
Torsten Schulz (local)
cf04e5bfe8 Erweitert die Benutzeroberfläche in OfficialTournaments.vue um einen neuen Tab für Teilnehmer, einschließlich Filteroptionen zur Anzeige von Anmeldestatus und Teilnahme. Implementiert die Logik zur Gruppierung und Anzeige der Teilnehmerdaten in einer Tabelle. 2025-09-12 13:58:04 +02:00
Torsten Schulz (local)
ace15ae1d3 Aktualisiert die index.html zur Unterstützung der deutschen Sprache und verbessert die SEO durch Hinzufügen von Meta-Tags. Modifiziert App.vue, um das Logo in der Kopfzeile anzuzeigen und fügt einen Footer mit Links zu Impressum und Datenschutzerklärung hinzu. Überarbeitet Home.vue mit neuen Marketing- und Funktionsabschnitten sowie einer FAQ-Sektion zur Benutzerinformation. Ergänzt Router-Konfiguration um Impressum- und Datenschutzseiten. 2025-09-11 15:32:49 +02:00
Torsten Schulz (local)
d4b82a3a6f Erweitert die Methode eligibleMembers in OfficialTournaments.vue, um nur aktive Mitglieder zu filtern, die für Wettbewerbe berechtigt sind. Dies verbessert die Genauigkeit der angezeigten Teilnehmerliste. 2025-09-11 14:58:33 +02:00
Torsten Schulz (local)
48cd0921df Fügt die Methode listClubParticipations im OfficialTournamentController hinzu, um die Teilnahme von Mitgliedern an offiziellen Turnieren zu listen. Aktualisiert die Routen, um diese neue Funktionalität zu integrieren. Verbessert die Benutzeroberfläche in OfficialTournaments.vue mit Tabs zur Anzeige von Veranstaltungen und Turnierbeteiligungen sowie einer Filteroption für den Zeitraum der Beteiligungen. 2025-09-11 14:11:19 +02:00
Torsten Schulz (local)
df02e48cfd Fügt das Modell OfficialCompetitionMember hinzu und implementiert die Logik zur Verwaltung der Teilnahme von Mitgliedern an offiziellen Wettbewerben. Aktualisiert die Routen und Controller, um die Teilnahmeinformationen zu speichern und abzurufen. Ergänzt die Benutzeroberfläche in OfficialTournaments.vue zur Anzeige und Bearbeitung der Teilnahmeoptionen für Mitglieder. 2025-09-11 12:58:56 +02:00
Torsten Schulz (local)
4a6d868820 Ändert die Schriftgröße der Navigationslinks in App.vue von 0.75rem auf 1rem, um die Lesbarkeit zu verbessern. 2025-09-01 11:38:44 +02:00
Torsten Schulz (local)
52556a4292 Fügt ein neues Skript zur Bereinigung aller Indizes in package.json hinzu und entfernt überflüssige Leerzeichen in diaryDateActivityService.js. 2025-09-01 11:27:09 +02:00
Torsten Schulz (server)
3a02ffb3e3 Merge branch 'main' of ssh://tsschulz.de:/home/git/trainingstagebuch 2025-09-01 09:23:26 +00:00
Torsten Schulz (local)
c4b9a7d782 Verbessert die Benutzeroberfläche in DiaryView.vue, indem die Struktur des Unfallformulars optimiert und die Audioinitialisierung an die Benutzerinteraktion angepasst wird. Fügt Logik zur Überprüfung von Aktivitätszeiten hinzu und stellt sicher, dass Audio nur bei aktivierter Funktion abgespielt wird. 2025-09-01 11:23:02 +02:00
Torsten Schulz (server)
5e8b221541 Merge branch 'main' of ssh://tsschulz.de:/home/git/trainingstagebuch 2025-09-01 07:38:48 +00:00
Torsten Schulz (server)
26720c8df3 updated package.json 2025-09-01 07:38:43 +00:00
Torsten Schulz (local)
a1ab742126 Optimiert das Styling in DiaryView.vue, indem die Überlauf-Eigenschaften des Trainingsplan-Div-Elements angepasst werden. Entfernt die horizontale Überlauf-Einstellung, um die Benutzeroberfläche zu verbessern. 2025-09-01 09:37:43 +02:00
Torsten Schulz (local)
f21ad3d8a3 Fügt eine neue Skriptfunktion zum Bereinigen von Benutzertoken hinzu und aktualisiert die Logik zum Synchronisieren des UserToken-Modells. Implementiert eine neue Controller-Methode zum Löschen von Datumsangaben für Clubs und passt die Routen entsprechend an. Ergänzt die Benutzeroberfläche in DiaryView.vue um die Möglichkeit, ein Datum zu löschen, und aktualisiert die Logik zur Überprüfung der Datumsaktualität. 2025-09-01 09:33:54 +02:00
Torsten Schulz (local)
51d3087006 Fügt die Anzeige des letzten Trainingsdatums und einen Sortiermechanismus in der Mitgliederstatistik hinzu. Aktualisiert die Backend-Logik zur Berechnung des letzten Trainings und passt die Benutzeroberfläche in TrainingStatsView.vue entsprechend an. 2025-08-31 21:32:03 +02:00
Torsten Schulz (local)
a08588a075 Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue, um die Auswahlmöglichkeiten für das Zusammenführen von Aktivitäten zu verbessern. Sortiert die Aktivitäten in den Dropdown-Listen nach Namen, um die Benutzerfreundlichkeit zu erhöhen. 2025-08-31 21:19:51 +02:00
Torsten Schulz (local)
5d67a52b45 Verbessert das Styling in PredefinedActivities.vue, um die Benutzeroberfläche zu optimieren. Fügt eine Höhe für das Hauptlayout hinzu und ermöglicht das Scrollen in der Liste sowie eine sticky Positionierung für die Toolbar, um die Benutzerfreundlichkeit zu erhöhen. 2025-08-31 21:14:30 +02:00
Torsten Schulz (local)
f29425c987 Fügt Funktionen zum Zusammenführen und Entfernen von Duplikaten vordefinierter Aktivitäten hinzu. Implementiert die entsprechenden Controller-Methoden und Routen. Aktualisiert die Benutzeroberfläche in PredefinedActivities.vue, um die neuen Funktionen zur Verfügung zu stellen und die Aktivitäten nach Namen und Code zu sortieren. 2025-08-31 21:09:48 +02:00
Torsten Schulz (local)
e3b8488d2b Erweitert die PDF-Generierung in PDFGenerator.js, um empfohlene und andere Wettbewerbe für Mitglieder zu unterscheiden. Fügt eine neue Struktur für die Anzeige von Empfehlungen und Hinweisen hinzu. Aktualisiert OfficialTournaments.vue, um die Auswahl von Mitgliedern und deren Wettbewerben zu verbessern, einschließlich einer neuen Dialogstruktur und der Verwaltung von Empfehlungen. 2025-08-31 15:55:49 +02:00
Torsten Schulz (local)
f49e1896b9 Fügt eine Funktion zur PDF-Generierung für ausgewählte Mitglieder in OfficialTournaments.vue hinzu. Implementiert ein Dialogfeld zur Auswahl von Mitgliedern und ermöglicht die Erstellung eines PDFs mit Wettbewerbsinformationen. Aktualisiert das Styling für die Benutzeroberfläche und die Modal-Komponenten. 2025-08-31 15:28:46 +02:00
Torsten Schulz (local)
2092473cf3 Verbessert die Mitgliederansicht in MembersView.vue, indem inaktive Mitglieder visuell hervorgehoben werden. Fügt CSS-Klassen hinzu, um die Darstellung inaktiver Mitglieder zu optimieren, einschließlich einer inaktiven Auszeichnung und einer durchgestrichenen Schriftart für inaktive Geschlechtssymbole und -bezeichnungen. 2025-08-31 15:11:01 +02:00
Torsten Schulz (local)
c00849a154 Verbessert die Mitgliederansicht in ClubView.vue, indem aktive Mitglieder nach Nachnamen und Vornamen sortiert angezeigt werden. Fügt Geschlechtssymbole und -bezeichnungen hinzu, um die Darstellung zu optimieren. Implementiert neue Methoden zur Geschlechtslabelierung und -symbolisierung sowie entsprechende CSS-Klassen für eine ansprechendere Benutzeroberfläche. 2025-08-31 15:02:57 +02:00
Torsten Schulz (local)
8069946154 Aktualisiert die Mitgliederansicht in MembersView.vue, um Geschlechtssymbole und -bezeichnungen anzuzeigen. Entfernt die Geschlechtsspalte und implementiert neue Methoden zur Darstellung von Geschlecht. Fügt CSS-Klassen für die Geschlechtsdarstellung hinzu, um die Benutzeroberfläche zu verbessern. 2025-08-30 23:39:28 +02:00
Torsten Schulz (local)
975800c1ab Fügt Unterstützung für offizielle Turniere und Wettbewerbe hinzu. Aktualisiert die Datenbankmodelle, um Geschlecht für Mitglieder zu erfassen, und implementiert neue Routen sowie Frontend-Komponenten zur Anzeige und Verwaltung dieser Daten. Verbessert die Benutzeroberfläche zur Eingabe von Mitgliederdaten und aktualisiert die Abhängigkeiten im Projekt. 2025-08-30 23:16:39 +02:00
Torsten Schulz (local)
b82a80a11d Fügt Unterstützung für Aktivitätenmitglieder in DiaryView.vue hinzu. Ermöglicht das Zuordnen von Teilnehmern zu Aktivitäten, einschließlich der Verwaltung von Teilnehmern über das Backend. Aktualisiert die Datenbankmodelle und -routen, um die neuen Funktionen zu unterstützen. 2025-08-28 14:43:04 +02:00
Torsten Schulz (local)
244b61c901 Fügt Unterstützung für vordefinierte Aktivitäten hinzu, einschließlich der Möglichkeit, Bilder hochzuladen und zu suchen. Aktualisiert die Datenbankmodelle und -routen entsprechend. Verbessert die Benutzeroberfläche zur Anzeige und Bearbeitung von Aktivitäten in DiaryView.vue. 2025-08-28 14:11:29 +02:00
Torsten Schulz (local)
c7325ac982 Erweitert die updateActivity-Methode in DiaryDateActivityService, um benutzerdefinierte Aktivitäten zu verarbeiten und neue vordefinierte Aktivitäten zu erstellen. Fügt eine Methode loadTrainingPlan in DiaryView.vue hinzu, um die Anzeige nach Änderungen zu aktualisieren. 2025-08-28 13:14:06 +02:00
Torsten Schulz (local)
8fbdc68016 Implementiert die Bearbeitung von Aktivitäten in DiaryView.vue. Fügt Eingabefelder und Schaltflächen zum Speichern oder Abbrechen von Änderungen hinzu. Aktualisiert das Styling für klickbare Elemente. 2025-08-28 12:04:33 +02:00
Torsten Schulz (local)
455b2c94cd Aktualisiert die Anzeige der Teilnehmeranzahl in DiaryView.vue, indem die Variable von 'members' auf 'participants' geändert wurde. 2025-08-28 09:31:24 +02:00
Torsten Schulz (local)
c9a1026b50 Aktualisiert die Anzeige der Teilnehmeranzahl in DiaryView.vue, indem die Anzahl der Mitglieder in der Überschrift angezeigt wird. 2025-08-28 09:28:00 +02:00
Torsten Schulz (local)
f6f1ea0403 Erhöht das Padding am unteren Rand der Spalte in DiaryView.vue von 3em auf 4em, um das Layout weiter zu optimieren. 2025-08-28 09:25:54 +02:00
Torsten Schulz (local)
a636b32510 Fügt Padding zum unteren Rand der Spalte in DiaryView.vue hinzu, um das Layout zu verbessern. 2025-08-28 09:22:26 +02:00
Torsten Schulz (local)
8ee1203ec6 Implementiert Benutzer-Authentifizierung und Datenladung bei Login. Fügt Links für Registrierung und Login in den entsprechenden Komponenten hinzu. Aktualisiert das Styling für Login- und Registrierungslinks. 2025-08-27 09:10:53 +02:00
Torsten Schulz (local)
bce5150757 Aktualisiert die Datenbankkonfiguration und ändert den Import von Komponenten in SCSS auf die neue Syntax. 2025-08-23 22:02:20 +02:00
Torsten Schulz (local)
117f6b4c93 Fügt Sortierfunktion für Ligen hinzu und entfernt die automatische Neuladung bei Logout 2025-08-23 21:57:55 +02:00
Torsten Schulz (local)
6a8b0e35d7 Fügt Sortierfunktionalität für die Mitgliederstatistik hinzu. Die Tabellenüberschriften sind jetzt klickbar und ermöglichen eine Sortierung nach Name und Teilnahmezahlen. Zudem wurde die Sortierreihenfolge implementiert und visuell durch Icons angezeigt. 2025-08-22 16:06:56 +02:00
Torsten Schulz (local)
ed96fc5f27 Aktualisiert den TrainingStatsController, um nur aktive Mitglieder eines spezifischen Vereins zu laden, indem die Abfrage um die clubId erweitert wurde. 2025-08-22 15:53:50 +02:00
624 changed files with 77322 additions and 22940 deletions

View File

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ frontend/.env
backend/.env
backend/images/*
backend/backend-debug.log
backend/*.log

210
PERMISSIONS_GUIDE.md Normal file
View File

@@ -0,0 +1,210 @@
# Berechtigungssystem - Dokumentation
## Übersicht
Das Trainingstagebuch verfügt nun über ein vollständiges rollenbasiertes Berechtigungssystem (RBAC - Role-Based Access Control). Der Club-Ersteller hat automatisch Admin-Rechte und kann anderen Mitgliedern Rollen und spezifische Berechtigungen zuweisen.
## Rollen
### 1. Administrator (admin)
- **Vollzugriff** auf alle Funktionen
- Kann Berechtigungen anderer Benutzer verwalten
- Der Club-Ersteller ist automatisch Administrator und kann nicht degradiert werden
### 2. Trainer (trainer)
- Kann Trainingseinheiten planen und verwalten
- Kann Mitglieder anlegen und bearbeiten
- Kann Spielpläne einsehen und bearbeiten
- Kann Turniere organisieren
- **Kann nicht**: Einstellungen ändern, Berechtigungen verwalten
### 3. Mannschaftsführer (team_manager)
- Kann Teams und Spielpläne verwalten
- Kann Spieler für Matches einteilen
- Kann Spielergebnisse eintragen
- **Kann nicht**: Trainingseinheiten planen, Mitglieder verwalten
### 4. Mitglied (member)
- Nur Lesezugriff auf alle Bereiche
- Kann eigene Daten einsehen
- **Kann nicht**: Daten ändern oder löschen
## Berechtigungsbereiche
- **diary**: Trainingstagebuch
- **members**: Mitgliederverwaltung
- **teams**: Team-Management
- **schedule**: Spielpläne
- **tournaments**: Turniere
- **statistics**: Statistiken
- **settings**: Einstellungen
- **permissions**: Berechtigungsverwaltung
- **mytischtennis**: MyTischtennis-Integration (für alle zugänglich)
## Backend-Integration
### Migration ausführen
```sql
mysql -u username -p database_name < backend/migrations/add_permissions_to_user_club.sql
```
### Authorization Middleware verwenden
```javascript
import { authorize, requireAdmin, requireOwner } from '../middleware/authorizationMiddleware.js';
// Beispiel: Nur Lesezugriff erforderlich
router.get('/diary/:clubId', authenticate, authorize('diary', 'read'), getDiary);
// Beispiel: Schreibzugriff erforderlich
router.post('/diary/:clubId', authenticate, authorize('diary', 'write'), createDiary);
// Beispiel: Admin-Rechte erforderlich
router.put('/settings/:clubId', authenticate, requireAdmin(), updateSettings);
// Beispiel: Nur Owner
router.delete('/club/:clubId', authenticate, requireOwner(), deleteClub);
```
### Permission Service verwenden
```javascript
import permissionService from '../services/permissionService.js';
// Berechtigungen prüfen
const hasPermission = await permissionService.hasPermission(userId, clubId, 'diary', 'write');
// Rolle setzen
await permissionService.setUserRole(userId, clubId, 'trainer', adminUserId);
// Custom Permissions setzen
await permissionService.setCustomPermissions(
userId,
clubId,
{ diary: { write: false }, members: { write: true } },
adminUserId
);
```
## Frontend-Integration
### Composable verwenden
```vue
<script setup>
import { usePermissions } from '@/composables/usePermissions.js';
const { can, canWrite, canDelete, isAdmin, isOwner, userRole } = usePermissions();
// Beispiel
if (can('diary', 'write')) {
// Zeige Bearbeitungsbutton
}
</script>
```
### Direktiven verwenden
```vue
<template>
<!-- Nur anzeigen, wenn Schreibrechte für diary vorhanden -->
<button v-can:diary.write>Bearbeiten</button>
<!-- Nur anzeigen, wenn Löschrechte für members vorhanden -->
<button v-can:members.delete>Löschen</button>
<!-- Alternative Syntax -->
<div v-can="'diary.write'">Inhalt nur für Berechtigte</div>
<!-- Nur für Admins -->
<div v-admin>Admin-Bereich</div>
<!-- Nur für Owner -->
<div v-owner>Owner-Bereich</div>
</template>
```
### Store verwenden
```javascript
import { useStore } from 'vuex';
const store = useStore();
// Berechtigungen abrufen
const permissions = store.getters.currentPermissions;
const hasPermission = store.getters.hasPermission('diary', 'write');
const isOwner = store.getters.isClubOwner;
const userRole = store.getters.userRole;
// Berechtigungen laden (wird automatisch beim Club-Wechsel gemacht)
await store.dispatch('loadPermissions', clubId);
```
## Admin-UI
Die Berechtigungsverwaltung ist unter `/permissions` verfügbar und nur für Administratoren sichtbar.
**Funktionen:**
- Übersicht aller Clubmitglieder mit ihren Rollen
- Rollen zuweisen/ändern
- Custom Permissions für einzelne Benutzer definieren
- Erklärung der verfügbaren Rollen
## MyTischtennis-Integration
Die MyTischtennis-Einstellungen und -Funktionen sind für **alle Club-Mitglieder** zugänglich, unabhängig von ihrer Rolle. Dies ermöglicht es jedem, die Anbindung einzurichten und Daten abzurufen.
## Sicherheitshinweise
1. **Der Club-Ersteller** (Owner) kann nicht degradiert oder gelöscht werden
2. **Owner-Rechte** können nicht übertragen werden
3. **Backend-Validierung** wird immer durchgeführt, auch wenn das Frontend Elemente ausblendet
4. **Alle API-Routen** sind durch Middleware geschützt
5. **Permissions werden gecacht** im localStorage für bessere Performance
## Beispiel-Szenarien
### Szenario 1: Trainer hinzufügen
1. Admin öffnet `/permissions`
2. Wählt Benutzer aus
3. Ändert Rolle zu "Trainer"
4. Benutzer kann jetzt Trainingseinheiten planen
### Szenario 2: Custom Permissions
1. Admin öffnet `/permissions`
2. Wählt Benutzer aus
3. Klickt auf "Anpassen"
4. Setzt individuelle Berechtigungen (z.B. nur Diary-Schreibrecht)
5. Speichert
### Szenario 3: Neues Mitglied
1. Mitglied registriert sich und fordert Zugang an
2. Admin genehmigt Anfrage (Standardrolle: "member")
3. Mitglied hat Lesezugriff
4. Bei Bedarf kann Admin die Rolle später ändern
## Troubleshooting
**Problem**: Berechtigungen werden nicht aktualisiert
- **Lösung**: Seite neu laden oder Club neu auswählen
**Problem**: "Keine Berechtigung" trotz korrekter Rolle
- **Lösung**: Prüfen, ob Custom Permissions die Rolle überschreiben
**Problem**: Owner kann keine Änderungen vornehmen
- **Lösung**: Owner sollte automatisch alle Rechte haben. Prüfen Sie die `isOwner`-Flag in der Datenbank
## API-Endpunkte
```
GET /api/permissions/:clubId - Eigene Berechtigungen abrufen
GET /api/permissions/:clubId/members - Alle Mitglieder mit Berechtigungen (Admin)
PUT /api/permissions/:clubId/user/:userId/role - Rolle ändern (Admin)
PUT /api/permissions/:clubId/user/:userId/permissions - Custom Permissions setzen (Admin)
GET /api/permissions/roles/available - Verfügbare Rollen abrufen
GET /api/permissions/structure/all - Berechtigungsstruktur abrufen
```

235
PERMISSIONS_MIGRATION.md Normal file
View File

@@ -0,0 +1,235 @@
# Berechtigungssystem - Migrations-Anleitung
## Übersicht
Diese Anleitung hilft Ihnen, das neue Berechtigungssystem für bestehende Clubs einzurichten.
## Schritt 1: Datenbank-Schema erweitern
Führen Sie zuerst die SQL-Migration aus, um die neuen Spalten hinzuzufügen:
```bash
mysql -u username -p database_name < backend/migrations/add_permissions_to_user_club.sql
```
Dies fügt folgende Spalten zur `user_club` Tabelle hinzu:
- `role` (VARCHAR) - Benutzerrolle (admin, trainer, team_manager, member)
- `permissions` (JSON) - Custom Permissions
- `is_owner` (BOOLEAN) - Markiert den Club-Ersteller
## Schritt 2: Bestehende Daten migrieren
Sie haben zwei Optionen:
### Option A: Node.js Script (Empfohlen)
Das Script identifiziert automatisch den ersten Benutzer jedes Clubs (nach `createdAt`) und setzt ihn als Owner.
```bash
cd /home/torsten/Programs/trainingstagebuch/backend
node scripts/migratePermissions.js
```
**Ausgabe:**
```
Starting permissions migration...
Found 3 club(s)
--- Club: TTC Beispiel (ID: 1) ---
Members found: 5
First member (will be owner): admin@example.com
✓ Updated admin@example.com: role=admin, isOwner=true
✓ Updated user1@example.com: role=member, isOwner=false
✓ Updated user2@example.com: role=member, isOwner=false
...
✅ Migration completed successfully!
Summary:
Club Owners (3):
- TTC Beispiel: admin@example.com
- SV Teststadt: owner@test.de
- TSC Demo: demo@example.com
Role Distribution:
- Admins: 3
- Members: 12
```
### Option B: SQL Script
Wenn Sie lieber SQL verwenden möchten:
```bash
mysql -u username -p database_name < backend/migrations/update_existing_user_club_permissions.sql
```
Dieses Script:
1. Setzt `role = 'member'` für alle genehmigten Benutzer ohne Rolle
2. Markiert den Benutzer mit der niedrigsten `user_id` pro Club als Owner
## Schritt 3: Manuelle Anpassungen (Optional)
### Falscher Owner?
Falls das Script den falschen Benutzer als Owner markiert hat, können Sie dies manuell korrigieren:
```sql
-- Alten Owner zurücksetzen
UPDATE user_club
SET is_owner = 0, role = 'member'
WHERE club_id = 1 AND user_id = 123;
-- Neuen Owner setzen
UPDATE user_club
SET is_owner = 1, role = 'admin'
WHERE club_id = 1 AND user_id = 456;
```
### Weitere Admins ernennen
```sql
UPDATE user_club
SET role = 'admin'
WHERE club_id = 1 AND user_id = 789;
```
### Trainer ernennen
```sql
UPDATE user_club
SET role = 'trainer'
WHERE club_id = 1 AND user_id = 101;
```
## Schritt 4: Verifizierung
### Backend neu starten
```bash
# Server neu starten (wenn er läuft)
sudo systemctl restart tt-tagebuch
```
### Im Browser testen
1. Loggen Sie sich ein
2. Wählen Sie einen Club aus
3. Navigieren Sie zu "Berechtigungen" (nur für Admins sichtbar)
4. Überprüfen Sie, dass alle Mitglieder korrekt angezeigt werden
### SQL Verifizierung
```sql
-- Alle Club-Mitglieder mit ihren Berechtigungen anzeigen
SELECT
c.name as club_name,
u.email as user_email,
uc.role,
uc.is_owner,
uc.approved
FROM user_club uc
JOIN club c ON c.id = uc.club_id
JOIN user u ON u.id = uc.user_id
WHERE uc.approved = 1
ORDER BY c.name, uc.is_owner DESC, uc.role, u.email;
```
## Troubleshooting
### Problem: "Keine Berechtigung" trotz Owner-Status
**Lösung:** Überprüfen Sie in der Datenbank:
```sql
SELECT role, is_owner, approved
FROM user_club
WHERE user_id = YOUR_USER_ID AND club_id = YOUR_CLUB_ID;
```
Sollte sein: `role='admin'`, `is_owner=1`, `approved=1`
### Problem: Owner kann nicht geändert werden
Das ist korrekt! Der Owner (Club-Ersteller) kann seine eigenen Rechte nicht verlieren. Dies ist eine Sicherheitsmaßnahme.
### Problem: Berechtigungen werden nicht geladen
**Lösung:**
1. Browser-Cache leeren
2. LocalStorage leeren: `localStorage.clear()` in der Browser-Console
3. Neu einloggen
### Problem: "Lade Mitglieder..." bleibt hängen
**Mögliche Ursachen:**
1. Migration noch nicht ausgeführt
2. Backend nicht neu gestartet
3. Frontend nicht neu gebaut
**Lösung:**
```bash
# Backend
cd /home/torsten/Programs/trainingstagebuch/backend
node scripts/migratePermissions.js
# Frontend
cd /home/torsten/Programs/trainingstagebuch/frontend
npm run build
# Server neu starten
sudo systemctl restart tt-tagebuch
```
## Nach der Migration
### Neue Clubs
Bei neuen Clubs wird der Ersteller automatisch als Owner mit Admin-Rechten eingerichtet. Keine manuelle Aktion erforderlich.
### Neue Mitglieder
Neue Mitglieder erhalten automatisch die Rolle "member" (Lesezugriff). Admins können die Rolle später ändern.
### Berechtigungen verwalten
Admins können über die Web-UI unter `/permissions` Berechtigungen verwalten:
1. Rollen zuweisen (Admin, Trainer, Mannschaftsführer, Mitglied)
2. Custom Permissions definieren (für spezielle Anwendungsfälle)
## Wichtige Hinweise
⚠️ **Sicherung erstellen:**
```bash
mysqldump -u username -p database_name > backup_before_permissions_$(date +%Y%m%d).sql
```
⚠️ **Owner-Rechte:**
- Der Owner (is_owner=1) kann nicht degradiert oder gelöscht werden
- Jeder Club hat genau einen Owner
- Owner-Rechte können nicht übertragen werden (nur durch direkte DB-Änderung)
⚠️ **MyTischtennis:**
- MyTischtennis-Funktionen sind für ALLE Mitglieder zugänglich
- Keine Berechtigungsprüfung für MyTischtennis-Endpunkte
## Rollback (falls nötig)
Falls Sie das Berechtigungssystem zurücknehmen müssen:
```sql
-- Spalten entfernen (Achtung: Datenverlust!)
ALTER TABLE user_club
DROP COLUMN role,
DROP COLUMN permissions,
DROP COLUMN is_owner;
-- Indizes entfernen
DROP INDEX idx_user_club_role ON user_club;
DROP INDEX idx_user_club_owner ON user_club;
```
Dann Backend-Code auf vorherige Version zurücksetzen.

View File

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

View File

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

View File

@@ -9,15 +9,18 @@ const dbConfig = {
database: process.env.DB_NAME || 'trainingsdiary'
};
const report = [];
async function cleanupKeys() {
let connection;
try {
console.log('🔌 Verbinde mit der Datenbank...');
report.push('🔌 Verbinde mit der Datenbank...');
connection = await mysql.createConnection(dbConfig);
// 1. Status vor dem Cleanup
console.log('\n📊 STATUS VOR DEM CLEANUP:');
report.push('');
report.push('📊 STATUS VOR DEM CLEANUP:');
const [tablesBefore] = await connection.execute(`
SELECT
TABLE_NAME,
@@ -29,57 +32,60 @@ async function cleanupKeys() {
`, [dbConfig.database]);
tablesBefore.forEach(table => {
console.log(` ${table.TABLE_NAME}: ${table.key_count} Keys`);
report.push(` ${table.TABLE_NAME}: ${table.key_count} Keys`);
});
// 2. Alle INDEX der Problem-Tabellen anzeigen
const problemTables = ['member', 'diary_tags', 'season'];
for (const tableName of problemTables) {
console.log(`\n🔍 INDEX für Tabelle '${tableName}':`);
report.push('');
report.push(`🔍 INDEX für Tabelle '${tableName}':`);
try {
const [indexes] = await connection.execute(`SHOW INDEX FROM \`${tableName}\``);
if (indexes.length === 0) {
console.log(` Keine INDEX gefunden für Tabelle '${tableName}'`);
report.push(` Keine INDEX gefunden für Tabelle '${tableName}'`);
continue;
}
indexes.forEach(index => {
console.log(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`);
report.push(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`);
});
// 3. Überflüssige INDEX entfernen (alle außer PRIMARY und UNIQUE)
console.log(`\n🗑️ Entferne überflüssige INDEX aus '${tableName}':`);
report.push('');
report.push(`🗑️ Entferne überflüssige INDEX aus '${tableName}':`);
for (const index of indexes) {
// Behalte PRIMARY KEY und UNIQUE constraints
if (index.Key_name === 'PRIMARY' || index.Non_unique === 0) {
console.log(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`);
report.push(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`);
continue;
}
// Entferne alle anderen INDEX
try {
await connection.execute(`DROP INDEX \`${index.Key_name}\` ON \`${tableName}\``);
console.log(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`);
report.push(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`);
} catch (error) {
if (error.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
console.log(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`);
report.push(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`);
} else {
console.log(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`);
report.push(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`);
}
}
}
} catch (error) {
console.log(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`);
report.push(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`);
}
}
// 4. Status nach dem Cleanup
console.log('\n📊 STATUS NACH DEM CLEANUP:');
report.push('');
report.push('📊 STATUS NACH DEM CLEANUP:');
const [tablesAfter] = await connection.execute(`
SELECT
TABLE_NAME,
@@ -96,7 +102,7 @@ async function cleanupKeys() {
const diff = beforeCount - table.key_count;
const status = table.key_count <= 5 ? '✅' : table.key_count <= 10 ? '⚠️' : '❌';
console.log(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`);
report.push(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`);
});
// 5. Gesamtanzahl der Keys
@@ -106,18 +112,20 @@ async function cleanupKeys() {
WHERE TABLE_SCHEMA = ?
`, [dbConfig.database]);
console.log(`\n📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`);
report.push('');
report.push(`📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`);
// 6. Zusammenfassung
console.log('\n🎯 ZUSAMMENFASSUNG:');
report.push('');
report.push('🎯 ZUSAMMENFASSUNG:');
const problemTablesAfter = tablesAfter.filter(t => t.key_count > 10);
if (problemTablesAfter.length === 0) {
console.log(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!');
report.push(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!');
} else {
console.log(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:');
report.push(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:');
problemTablesAfter.forEach(table => {
console.log(` - ${table.TABLE_NAME}: ${table.key_count} Keys`);
report.push(` - ${table.TABLE_NAME}: ${table.key_count} Keys`);
});
}
@@ -126,15 +134,18 @@ async function cleanupKeys() {
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 Datenbankverbindung geschlossen.');
report.push('');
report.push('🔌 Datenbankverbindung geschlossen.');
}
}
}
// Script ausführen
console.log('🚀 Starte intelligentes INDEX-Cleanup...\n');
report.push('🚀 Starte intelligentes INDEX-Cleanup...');
cleanupKeys().then(() => {
console.log('\n✨ Cleanup abgeschlossen!');
report.push('');
report.push('✨ Cleanup abgeschlossen!');
process.stdout.write(`${report.join('\n')}\n`);
process.exit(0);
}).catch(error => {
console.error('\n💥 Fehler beim Cleanup:', error);

View File

@@ -7,41 +7,23 @@ import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const report = [];
// Umgebungsvariablen aus dem Root-Verzeichnis laden
//const envPath = path.join(__dirname, '..', '.env');
//console.log('🔍 Lade .env-Datei von:', envPath);
const envPath = path.join(__dirname, '..', '.env');
dotenv.config();
// Debug: Zeige geladene Umgebungsvariablen
console.log('🔍 Geladene Umgebungsvariablen:');
console.log(' DB_HOST:', process.env.DB_HOST);
console.log(' DB_USER:', process.env.DB_USER);
console.log(' DB_NAME:', process.env.DB_NAME);
console.log(' DB_PASSWORD:', process.env.DB_PASSWORD ? '***gesetzt***' : 'nicht gesetzt');
// Datenbankverbindung
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'trainingsdiary'
};
console.log('🔍 Datenbankverbindung:');
console.log(' Host:', dbConfig.host);
console.log(' User:', dbConfig.user);
console.log(' Database:', dbConfig.database);
console.log(' Password:', dbConfig.password ? '***gesetzt***' : 'nicht gesetzt');
report.push('Environment variables loaded');
async function cleanupKeys() {
let connection;
try {
console.log('🔌 Verbinde mit der Datenbank...');
report.push('🔌 Verbinde mit der Datenbank...');
connection = await mysql.createConnection(dbConfig);
// 1. Status vor dem Cleanup
console.log('\n📊 STATUS VOR DEM CLEANUP:');
report.push('');
report.push('📊 STATUS VOR DEM CLEANUP:');
const [tablesBefore] = await connection.execute(`
SELECT
TABLE_NAME,
@@ -53,57 +35,60 @@ async function cleanupKeys() {
`, [dbConfig.database]);
tablesBefore.forEach(table => {
console.log(` ${table.TABLE_NAME}: ${table.key_count} Keys`);
report.push(` ${table.TABLE_NAME}: ${table.key_count} Keys`);
});
// 2. Alle INDEX der Problem-Tabellen anzeigen
const problemTables = ['member', 'diary_tags', 'season'];
for (const tableName of problemTables) {
console.log(`\n🔍 INDEX für Tabelle '${tableName}':`);
report.push('');
report.push(`🔍 INDEX für Tabelle '${tableName}':`);
try {
const [indexes] = await connection.execute(`SHOW INDEX FROM \`${tableName}\``);
if (indexes.length === 0) {
console.log(` Keine INDEX gefunden für Tabelle '${tableName}'`);
report.push(` Keine INDEX gefunden für Tabelle '${tableName}'`);
continue;
}
indexes.forEach(index => {
console.log(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`);
report.push(` - ${index.Key_name} (${index.Column_name}) - ${index.Non_unique === 0 ? 'UNIQUE' : 'NON-UNIQUE'}`);
});
// 3. Überflüssige INDEX entfernen (alle außer PRIMARY und UNIQUE)
console.log(`\n🗑️ Entferne überflüssige INDEX aus '${tableName}':`);
report.push('');
report.push(`🗑️ Entferne überflüssige INDEX aus '${tableName}':`);
for (const index of indexes) {
// Behalte PRIMARY KEY und UNIQUE constraints
if (index.Key_name === 'PRIMARY' || index.Non_unique === 0) {
console.log(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`);
report.push(` ✅ Behalte: ${index.Key_name} (${index.Column_name})`);
continue;
}
// Entferne alle anderen INDEX
try {
await connection.execute(`DROP INDEX \`${index.Key_name}\` ON \`${tableName}\``);
console.log(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`);
report.push(` ❌ Entfernt: ${index.Key_name} (${index.Column_name})`);
} catch (error) {
if (error.code === 'ER_CANT_DROP_FIELD_OR_KEY') {
console.log(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`);
report.push(` ⚠️ Kann nicht entfernen: ${index.Key_name} (${index.Column_name}) - ${error.message}`);
} else {
console.log(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`);
report.push(` ❌ Fehler beim Entfernen von ${index.Key_name}: ${error.message}`);
}
}
}
} catch (error) {
console.log(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`);
report.push(` ⚠️ Fehler beim Zugriff auf Tabelle '${tableName}': ${error.message}`);
}
}
// 4. Status nach dem Cleanup
console.log('\n📊 STATUS NACH DEM CLEANUP:');
report.push('');
report.push('📊 STATUS NACH DEM CLEANUP:');
const [tablesAfter] = await connection.execute(`
SELECT
TABLE_NAME,
@@ -120,7 +105,7 @@ async function cleanupKeys() {
const diff = beforeCount - table.key_count;
const status = table.key_count <= 5 ? '✅' : table.key_count <= 10 ? '⚠️' : '❌';
console.log(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`);
report.push(` ${status} ${table.TABLE_NAME}: ${table.key_count} Keys (${diff > 0 ? `-${diff}` : `+${Math.abs(diff)}`})`);
});
// 5. Gesamtanzahl der Keys
@@ -130,18 +115,20 @@ async function cleanupKeys() {
WHERE TABLE_SCHEMA = ?
`, [dbConfig.database]);
console.log(`\n📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`);
report.push('');
report.push(`📈 GESAMTANZAHL KEYS: ${totalKeys[0].total_keys}`);
// 6. Zusammenfassung
console.log('\n🎯 ZUSAMMENFASSUNG:');
report.push('');
report.push('🎯 ZUSAMMENFASSUNG:');
const problemTablesAfter = tablesAfter.filter(t => t.key_count > 10);
if (problemTablesAfter.length === 0) {
console.log(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!');
report.push(' ✅ Alle Tabellen haben jetzt weniger als 10 Keys!');
} else {
console.log(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:');
report.push(' ⚠️ Folgende Tabellen haben immer noch zu viele Keys:');
problemTablesAfter.forEach(table => {
console.log(` - ${table.TABLE_NAME}: ${table.key_count} Keys`);
report.push(` - ${table.TABLE_NAME}: ${table.key_count} Keys`);
});
}
@@ -150,15 +137,18 @@ async function cleanupKeys() {
} finally {
if (connection) {
await connection.end();
console.log('\n🔌 Datenbankverbindung geschlossen.');
report.push('');
report.push('🔌 Datenbankverbindung geschlossen.');
}
}
}
// Script ausführen
console.log('🚀 Starte intelligentes INDEX-Cleanup...\n');
report.push('🚀 Starte intelligentes INDEX-Cleanup...');
cleanupKeys().then(() => {
console.log('\n✨ Cleanup abgeschlossen!');
report.push('');
report.push('✨ Cleanup abgeschlossen!');
process.stdout.write(`${report.join('\n')}\n`);
process.exit(0);
}).catch(error => {
console.error('\n💥 Fehler beim Cleanup:', error);

View File

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

View File

@@ -1,17 +1,41 @@
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
dotenv.config();
// Ensure .env is loaded from the backend folder (not dependent on process.cwd())
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
dotenv.config({ path: path.resolve(__dirname, '.env') });
export const development = {
const isTestEnv = process.env.NODE_ENV === 'test';
const baseConfig = {
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'hitomisan',
database: process.env.DB_NAME || 'trainingdiary',
host: process.env.DB_HOST,
dialect: process.env.DB_DIALECT,
host: process.env.DB_HOST || 'localhost',
dialect: process.env.DB_DIALECT || 'mysql',
define: {
freezeTableName: true,
underscored: true,
underscoredAll: true,
},
logging: false,
storage: process.env.DB_STORAGE,
};
if (isTestEnv) {
baseConfig.username = 'sqlite';
baseConfig.password = '';
baseConfig.database = 'sqlite';
baseConfig.host = 'localhost';
baseConfig.dialect = 'sqlite';
baseConfig.storage = process.env.DB_STORAGE || ':memory:';
}
if (baseConfig.dialect === 'sqlite' && !baseConfig.storage) {
baseConfig.storage = ':memory:';
}
export const development = baseConfig;

View File

@@ -0,0 +1,85 @@
import apiLogService from '../services/apiLogService.js';
import HttpError from '../exceptions/HttpError.js';
class ApiLogController {
/**
* GET /api/logs
* Get API logs with optional filters
*/
async getLogs(req, res, next) {
try {
const {
userId,
logType,
method,
path,
statusCode,
startDate,
endDate,
limit = 100,
offset = 0
} = req.query;
const result = await apiLogService.getLogs({
userId: userId ? parseInt(userId) : null,
logType,
method,
path,
statusCode: statusCode ? parseInt(statusCode) : null,
startDate,
endDate,
limit: parseInt(limit),
offset: parseInt(offset)
});
res.json({
success: true,
data: result
});
} catch (error) {
next(error);
}
}
/**
* GET /api/logs/:id
* Get a single log entry by ID
*/
async getLogById(req, res, next) {
try {
const { id } = req.params;
const log = await apiLogService.getLogById(parseInt(id));
if (!log) {
throw new HttpError('Log entry not found', 404);
}
res.json({
success: true,
data: log
});
} catch (error) {
next(error);
}
}
/**
* GET /api/logs/scheduler/last-executions
* Get last execution info for scheduler jobs
*/
async getLastSchedulerExecutions(req, res, next) {
try {
const { clubId } = req.query;
const results = await apiLogService.getLastSchedulerExecutions(clubId ? parseInt(clubId) : null);
res.json({
success: true,
data: results
});
} catch (error) {
next(error);
}
}
}
export default new ApiLogController();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,17 +18,28 @@ export const getNotes = async (req, res) => {
export const createNote = async (req, res) => {
try {
const { memberId, content, tags } = req.body;
const newNote = await DiaryNote.create({ memberId, content });
if (tags && tags.length > 0) {
const { memberId, diaryDateId, content, tags } = req.body;
if (!memberId || !diaryDateId || !content) {
return res.status(400).json({ error: 'memberId, diaryDateId und content sind erforderlich.' });
}
const newNote = await DiaryNote.create({ memberId, diaryDateId, content });
if (Array.isArray(tags) && tags.length > 0 && typeof newNote.addTags === 'function') {
const tagInstances = await DiaryTag.findAll({ where: { id: tags } });
await newNote.addTags(tagInstances);
const noteWithTags = await DiaryNote.findByPk(newNote.id, {
include: [{ model: DiaryTag, as: 'tags', required: false }],
});
return res.status(201).json(noteWithTags ?? newNote);
}
const noteWithTags = await DiaryNote.findByPk(newNote.id, {
include: [{ model: DiaryTag, as: 'tags' }],
});
res.status(201).json(noteWithTags);
res.status(201).json(newNote);
} catch (error) {
console.error('[createNote] - Error:', error);
res.status(500).json({ error: 'Error creating note' });
}
};

View File

@@ -12,11 +12,13 @@ export const getTags = async (req, res) => {
export const createTag = async (req, res) => {
try {
const { name } = req.body;
console.log(name);
const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
res.status(201).json(newTag);
if (!name) {
return res.status(400).json({ error: 'Der Name des Tags ist erforderlich.' });
}
const [tag, created] = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
res.status(created ? 201 : 200).json(tag);
} catch (error) {
console.log('[createTag] - Error:', error);
res.status(500).json({ error: 'Error creating tag' });
}
};
@@ -24,9 +26,14 @@ export const createTag = async (req, res) => {
export const deleteTag = async (req, res) => {
try {
const { tagId } = req.params;
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId);
await DiaryDateTag.destroy({ where: { tagId } });
const deleted = await DiaryTag.destroy({ where: { id: tagId } });
if (!deleted) {
return res.status(404).json({ error: 'Tag nicht gefunden' });
}
res.status(200).json({ message: 'Tag deleted' });
} catch (error) {
console.error('[deleteTag] - Error:', error);

View File

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

View File

@@ -1,6 +1,7 @@
import MatchService from '../services/matchService.js';
import fs from 'fs';
import { devLog } from '../utils/logger.js';
export const uploadCSV = async (req, res) => {
try {
const { clubId } = req.body;
@@ -21,10 +22,11 @@ export const uploadCSV = async (req, res) => {
export const getLeaguesForCurrentSeason = async (req, res) => {
try {
console.log(req.headers, req.params);
devLog(req.headers, req.params);
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId);
const { seasonid: seasonId } = req.query;
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId, seasonId);
return res.status(200).json(leagues);
} catch (error) {
console.error('Error retrieving leagues:', error);
@@ -36,7 +38,8 @@ export const getMatchesForLeagues = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId } = req.params;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId);
const { seasonid: seasonId } = req.query;
const matches = await MatchService.getMatchesForLeagues(userToken, clubId, seasonId);
return res.status(200).json(matches);
} catch (error) {
console.error('Error retrieving matches:', error);
@@ -55,3 +58,90 @@ export const getMatchesForLeague = async (req, res) => {
return res.status(500).json({ error: 'Failed to retrieve matches' });
}
};
export const getLeagueTable = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
return res.status(200).json(table);
} catch (error) {
console.error('Error retrieving league table:', error);
return res.status(500).json({ error: 'Failed to retrieve league table' });
}
};
export const fetchLeagueTableFromMyTischtennis = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const autoFetchService = (await import('../services/autoFetchMatchResultsService.js')).default;
await autoFetchService.fetchAndUpdateLeagueTable(userId, leagueId);
// Return updated table data
const table = await MatchService.getLeagueTable(userToken, clubId, leagueId);
return res.status(200).json({
message: 'League table updated from MyTischtennis',
data: table
});
} catch (error) {
console.error('Error fetching league table from MyTischtennis:', error);
return res.status(500).json({ error: 'Failed to fetch league table from MyTischtennis' });
}
};
export const updateMatchPlayers = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { matchId } = req.params;
const { playersReady, playersPlanned, playersPlayed } = req.body;
const result = await MatchService.updateMatchPlayers(
userToken,
matchId,
playersReady,
playersPlanned,
playersPlayed
);
return res.status(200).json({
message: 'Match players updated successfully',
data: result
});
} catch (error) {
console.error('Error updating match players:', error);
return res.status(error.statusCode || 500).json({
error: error.message || 'Failed to update match players'
});
}
};
export const getPlayerMatchStats = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, leagueId } = req.params;
const { seasonid: seasonId } = req.query;
const stats = await MatchService.getPlayerMatchStats(userToken, clubId, leagueId, seasonId);
return res.status(200).json(stats);
} catch (error) {
console.error('Error retrieving player match stats:', error);
return res.status(error.statusCode || 500).json({
error: error.message || 'Failed to retrieve player match stats'
});
}
};

View File

@@ -0,0 +1,232 @@
import { checkAccess } from '../utils/userUtils.js';
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 PredefinedActivity from '../models/PredefinedActivity.js';
import GroupActivity from '../models/GroupActivity.js';
import { Op } from 'sequelize';
export const getMemberActivities = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
const { period } = req.query; // 'month', '3months', '6months', 'year', 'all'
await checkAccess(userToken, clubId);
// Calculate date range based on period
const now = new Date();
let startDate = null;
switch (period) {
case 'month':
startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
break;
case '3months':
startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate());
break;
case '6months':
startDate = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate());
break;
case 'year':
startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
break;
case 'all':
default:
startDate = null;
break;
}
// Get participant ID for this member
const participants = await Participant.findAll({
where: { memberId: memberId }
});
if (participants.length === 0) {
return res.status(200).json([]);
}
const participantIds = participants.map(p => p.id);
// Get all diary member activities for this member
const whereClause = {
participantId: participantIds
};
const memberActivities = await DiaryMemberActivity.findAll({
where: whereClause,
include: [
{
model: Participant,
as: 'participant',
attributes: ['id', 'groupId', 'diaryDateId']
},
{
model: DiaryDateActivity,
as: 'activity',
include: [
{
model: DiaryDates,
as: 'diaryDate',
where: startDate ? {
date: {
[Op.gte]: startDate
}
} : {}
},
{
model: PredefinedActivity,
as: 'predefinedActivity'
},
{
model: GroupActivity,
as: 'groupActivities',
required: false
}
]
}
],
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']]
});
// Group activities by name and count occurrences, considering group assignment
const activityMap = new Map();
for (const ma of memberActivities) {
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 date = ma.activity.diaryDate?.date;
if (!activityMap.has(activityName)) {
activityMap.set(activityName, {
name: activityName,
count: 0,
dates: []
});
}
const activityData = activityMap.get(activityName);
activityData.count++;
if (date) {
activityData.dates.push(date);
}
}
// Convert map to array and sort by count
const activities = Array.from(activityMap.values())
.sort((a, b) => b.count - a.count);
return res.status(200).json(activities);
} catch (error) {
console.error('Error fetching member activities:', error);
return res.status(500).json({ error: 'Failed to fetch member activities' });
}
};
export const getMemberLastParticipations = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId } = req.params;
const { limit = 3 } = req.query;
await checkAccess(userToken, clubId);
// Get participant ID for this member
const participants = await Participant.findAll({
where: { memberId: memberId }
});
if (participants.length === 0) {
return res.status(200).json([]);
}
const participantIds = participants.map(p => p.id);
// Get last participations for this member
const memberActivities = await DiaryMemberActivity.findAll({
where: {
participantId: participantIds
},
include: [
{
model: Participant,
as: 'participant',
attributes: ['id', 'groupId', 'diaryDateId']
},
{
model: DiaryDateActivity,
as: 'activity',
include: [
{
model: DiaryDates,
as: 'diaryDate'
},
{
model: PredefinedActivity,
as: 'predefinedActivity'
},
{
model: GroupActivity,
as: 'groupActivities',
required: false
}
]
}
],
order: [[{ model: DiaryDateActivity, as: 'activity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
limit: parseInt(limit) * 10 // Get more to filter by group
});
// Format the results, considering group assignment
const participations = memberActivities
.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));
})
.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
}));
return res.status(200).json(participations);
} catch (error) {
console.error('Error fetching member last participations:', error);
return res.status(500).json({ error: 'Failed to fetch member last participations' });
}
};

View File

@@ -1,48 +1,41 @@
import MemberService from "../services/memberService.js";
import MemberTransferService from "../services/memberTransferService.js";
import { devLog } from '../utils/logger.js';
const getClubMembers = async(req, res) => {
try {
const { authcode: userToken } = req.headers;
const { id: clubId, showAll } = req.params;
if (showAll === null) {
showAll = false;
}
const { id: clubId } = req.params;
const showAll = req.params.showAll ?? 'false';
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
} catch(error) {
console.log('[getClubMembers] - Error: ', error);
res.status(500).json({ error: 'systemerror' });
}
}
const getWaitingApprovals = async(req, res) => {
try {
console.log('[getWaitingApprovals] - Start');
const { id: clubId } = req.params;
console.log('[getWaitingApprovals] - get token');
const { authcode: userToken } = req.headers;
console.log('[getWaitingApprovals] - load for waiting approvals');
const waitingApprovals = await MemberService.getApprovalRequests(userToken, clubId);
console.log('[getWaitingApprovals] - set response');
res.status(200).json(waitingApprovals);
console.log('[getWaitingApprovals] - done');
} catch(error) {
console.log('[getWaitingApprovals] - Error: ', error);
res.status(403).json({ error: error });
}
}
const setClubMembers = async (req, res) => {
try {
const { id: memberId, firstname: firstName, lastname: lastName, street, city, birthdate, phone, email, active,
testMembership, picsInInternetAllowed } = req.body;
const { id: memberId, firstname: firstName, lastname: lastName, street, city, postalCode, birthdate, phone, email, active,
testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts } = req.body;
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate,
phone, email, active, testMembership, picsInInternetAllowed);
const addResult = await MemberService.setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, postalCode, birthdate,
phone, email, active, testMembership, picsInInternetAllowed, gender, ttr, qttr, memberFormHandedOver, contacts);
res.status(addResult.status || 500).json(addResult.response);
} catch (error) {
console.error('[setClubMembers] - Error:', error);
res.status(500).json({ error: 'Failed to upload image' });
res.status(500).json({ error: 'Failed to save member' });
}
}
@@ -50,8 +43,12 @@ const uploadMemberImage = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.uploadMemberImage(userToken, clubId, memberId, req.file.buffer);
res.status(result.status).json(result.message ? { message: result.message } : { error: result.error });
const makePrimary =
req.body?.makePrimary === true ||
req.body?.makePrimary === 'true' ||
req.query?.makePrimary === 'true';
const result = await MemberService.uploadMemberImage(userToken, clubId, memberId, req.file.buffer, { makePrimary });
res.status(result.status).json(result.response ?? { success: false, error: 'Unknown upload result' });
} catch (error) {
console.error('[uploadMemberImage] - Error:', error);
res.status(500).json({ error: 'Failed to upload image' });
@@ -59,11 +56,12 @@ const uploadMemberImage = async (req, res) => {
};
const getMemberImage = async (req, res) => {
console.log('[getMemberImage]');
try {
const { clubId, memberId } = req.params;
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.getMemberImage(userToken, clubId, memberId);
// Support "latest" as imageId to get the latest image
const actualImageId = imageId === 'latest' ? null : (imageId || null);
const result = await MemberService.getMemberImage(userToken, clubId, memberId, actualImageId);
if (result.status === 200) {
res.sendFile(result.imagePath);
} else {
@@ -75,4 +73,174 @@ const getMemberImage = async (req, res) => {
}
};
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage };
const updateRatingsFromMyTischtennis = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.updateRatingsFromMyTischtennis(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
res.status(500).json({ error: 'Failed to update ratings' });
}
};
const rotateMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { direction } = req.body;
const { authcode: userToken } = req.headers;
if (!direction || !['left', 'right'].includes(direction)) {
return res.status(400).json({
success: false,
error: 'Ungültige Drehrichtung. Verwenden Sie "left" oder "right".'
});
}
const result = await MemberService.rotateMemberImage(userToken, clubId, memberId, imageId, direction);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[rotateMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to rotate image' });
}
};
const deleteMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.deleteMemberImage(userToken, clubId, memberId, imageId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[deleteMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to delete image' });
}
};
const generateMemberGallery = async (req, res) => {
try {
const { clubId } = req.params;
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);
if (result.status === 200) {
if (format === 'json') {
// Return member information for interactive gallery
return res.status(200).json({
members: result.galleryEntries.map(entry => ({
memberId: entry.memberId,
firstName: entry.firstName,
lastName: entry.lastName,
fullName: entry.fullName
}))
});
}
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'no-store');
return res.status(200).send(result.buffer);
}
return res.status(result.status).json({ error: result.error || 'Galerie konnte nicht erstellt werden' });
} catch (error) {
console.error('[generateMemberGallery] - Error:', error);
res.status(500).json({ error: 'Failed to generate member gallery' });
}
};
const setPrimaryMemberImage = async (req, res) => {
try {
const { clubId, memberId, imageId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.setPrimaryMemberImage(userToken, clubId, memberId, imageId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[setPrimaryMemberImage] - Error:', error);
res.status(500).json({ success: false, error: 'Failed to update primary image' });
}
};
const quickUpdateTestMembership = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickUpdateTestMembership(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickUpdateTestMembership] - Error:', error);
res.status(500).json({ error: 'Failed to update test membership' });
}
};
const quickUpdateMemberFormHandedOver = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickUpdateMemberFormHandedOver(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickUpdateMemberFormHandedOver] - Error:', error);
res.status(500).json({ error: 'Failed to update member form status' });
}
};
const quickDeactivateMember = async (req, res) => {
try {
const { clubId, memberId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberService.quickDeactivateMember(userToken, clubId, memberId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[quickDeactivateMember] - Error:', error);
res.status(500).json({ error: 'Failed to deactivate member' });
}
};
const transferMembers = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const config = req.body;
// Validierung
if (!config.transferEndpoint) {
return res.status(400).json({
success: false,
error: 'Übertragungs-Endpoint ist erforderlich'
});
}
if (!config.transferTemplate) {
return res.status(400).json({
success: false,
error: 'Übertragungs-Template ist erforderlich'
});
}
const result = await MemberTransferService.transferMembers(userToken, clubId, config);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[transferMembers] - Error:', error);
res.status(500).json({
success: false,
error: 'Fehler bei der Übertragung: ' + error.message
});
}
};
export {
getClubMembers,
getWaitingApprovals,
setClubMembers,
uploadMemberImage,
getMemberImage,
updateRatingsFromMyTischtennis,
rotateMemberImage,
transferMembers,
quickUpdateTestMembership,
quickUpdateMemberFormHandedOver,
quickDeactivateMember,
deleteMemberImage,
setPrimaryMemberImage,
generateMemberGallery
};

View File

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

View File

@@ -0,0 +1,51 @@
import MemberTransferConfigService from '../services/memberTransferConfigService.js';
export const getConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberTransferConfigService.getConfig(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[getConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Laden der Konfiguration'
});
}
};
export const saveConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const configData = req.body;
const result = await MemberTransferConfigService.saveConfig(userToken, clubId, configData);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[saveConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Speichern der Konfiguration'
});
}
};
export const deleteConfig = async (req, res) => {
try {
const { id: clubId } = req.params;
const { authcode: userToken } = req.headers;
const result = await MemberTransferConfigService.deleteConfig(userToken, clubId);
res.status(result.status).json(result.response);
} catch (error) {
console.error('[deleteConfig] Error:', error);
res.status(500).json({
success: false,
error: 'Fehler beim Löschen der Konfiguration'
});
}
};

View File

@@ -0,0 +1,205 @@
import myTischtennisService from '../services/myTischtennisService.js';
import HttpError from '../exceptions/HttpError.js';
class MyTischtennisController {
/**
* GET /api/mytischtennis/account
* Get current user's myTischtennis account
*/
async getAccount(req, res, next) {
try {
const userId = req.user.id;
const account = await myTischtennisService.getAccount(userId);
if (!account) {
return res.status(200).json({ account: null });
}
res.status(200).json({ account });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/status
* Check account configuration status
*/
async getStatus(req, res, next) {
try {
const userId = req.user.id;
const status = await myTischtennisService.checkAccountStatus(userId);
res.status(200).json(status);
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/account
* Create or update myTischtennis account
*/
async upsertAccount(req, res, next) {
try {
const userId = req.user.id;
const { email, password, savePassword, autoUpdateRatings, userPassword } = req.body;
if (!email) {
throw new HttpError('E-Mail-Adresse erforderlich', 400);
}
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
if (password && !userPassword) {
throw new HttpError('App-Passwort erforderlich zum Setzen des myTischtennis-Passworts', 400);
}
const account = await myTischtennisService.upsertAccount(
userId,
email,
password,
savePassword || false,
autoUpdateRatings || false,
userPassword
);
res.status(200).json({
message: 'myTischtennis-Account erfolgreich gespeichert',
account
});
} catch (error) {
next(error);
}
}
/**
* DELETE /api/mytischtennis/account
* Delete myTischtennis account
*/
async deleteAccount(req, res, next) {
try {
const userId = req.user.id;
const deleted = await myTischtennisService.deleteAccount(userId);
if (!deleted) {
throw new HttpError('Kein myTischtennis-Account gefunden', 404);
}
res.status(200).json({ message: 'myTischtennis-Account gelöscht' });
} catch (error) {
next(error);
}
}
/**
* POST /api/mytischtennis/verify
* Verify login credentials
*/
async verifyLogin(req, res, next) {
try {
const userId = req.user.id;
const { password } = req.body;
const result = await myTischtennisService.verifyLogin(userId, password);
res.status(200).json({
message: 'Login erfolgreich',
success: true,
accessToken: result.accessToken,
expiresAt: result.expiresAt,
clubId: result.clubId,
clubName: result.clubName
});
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/session
* Get stored session data for authenticated requests
*/
async getSession(req, res, next) {
try {
const userId = req.user.id;
const session = await myTischtennisService.getSession(userId);
res.status(200).json({ session });
} catch (error) {
next(error);
}
}
/**
* GET /api/mytischtennis/update-history
* Get update ratings history
*/
async getUpdateHistory(req, res, next) {
try {
const userId = req.user.id;
const history = await myTischtennisService.getUpdateHistory(userId);
res.status(200).json({ history });
} catch (error) {
next(error);
}
}
/**
* Get fetch logs for current user
*/
async getFetchLogs(req, res, next) {
try {
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
const logs = await fetchLogService.getFetchLogs(userId, {
limit: req.query.limit ? parseInt(req.query.limit) : 50,
fetchType: req.query.type
});
res.status(200).json({ logs });
} catch (error) {
next(error);
}
}
/**
* Get latest successful fetches for each type
*/
async getLatestFetches(req, res, next) {
try {
const { userid: userIdOrEmail } = req.headers;
// Convert email to userId if needed
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const User = (await import('../models/User.js')).default;
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
userId = user.id;
}
const fetchLogService = (await import('../services/myTischtennisFetchLogService.js')).default;
const latestFetches = await fetchLogService.getLatestSuccessfulFetches(userId);
res.status(200).json({ latestFetches });
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisController();

View File

@@ -0,0 +1,601 @@
import myTischtennisUrlParserService from '../services/myTischtennisUrlParserService.js';
import myTischtennisService from '../services/myTischtennisService.js';
import MemberService from '../services/memberService.js';
import autoFetchMatchResultsService from '../services/autoFetchMatchResultsService.js';
import apiLogService from '../services/apiLogService.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import User from '../models/User.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
class MyTischtennisUrlController {
/**
* Parse myTischtennis URL and return configuration data
* POST /api/mytischtennis/parse-url
* Body: { url: string }
*/
async parseUrl(req, res, next) {
try {
const { url } = req.body;
if (!url) {
throw new HttpError('URL is required', 400);
}
// Validate URL
if (!myTischtennisUrlParserService.isValidTeamUrl(url)) {
throw new HttpError('Invalid myTischtennis URL format', 400);
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
// Try to fetch additional data if user is authenticated
const userIdOrEmail = req.headers.userid;
let completeData = parsedData;
if (userIdOrEmail) {
// Get actual user ID
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (user) userId = user.id;
}
try {
const account = await myTischtennisService.getAccount(userId);
if (account && account.accessToken) {
completeData = await myTischtennisUrlParserService.fetchTeamData(
parsedData,
account.cookie,
account.accessToken
);
}
} catch (error) {
// Continue with parsed data only
}
}
res.json({
success: true,
data: completeData
});
} catch (error) {
next(error);
}
}
/**
* Configure team from myTischtennis URL
* POST /api/mytischtennis/configure-team
* Body: { url: string, clubTeamId: number, createLeague?: boolean, createSeason?: boolean }
*/
async configureTeam(req, res, next) {
try {
const { url, clubTeamId, createLeague, createSeason } = req.body;
const userIdOrEmail = req.headers.userid;
if (!url || !clubTeamId) {
throw new HttpError('URL and clubTeamId are required', 400);
}
// Get actual user ID
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError('User not found', 404);
}
userId = user.id;
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
// Try to fetch additional data
let completeData = parsedData;
const account = await myTischtennisService.getAccount(userId);
if (account && account.accessToken) {
try {
completeData = await myTischtennisUrlParserService.fetchTeamData(
parsedData,
account.cookie,
account.accessToken
);
} catch (error) {
}
}
// Find or create season
let season = await Season.findOne({
where: { season: completeData.season }
});
if (!season && createSeason) {
season = await Season.create({
season: completeData.season
});
}
if (!season) {
throw new HttpError(`Season ${completeData.season} not found. Set createSeason=true to create it.`, 404);
}
// Find or create league
const team = await ClubTeam.findByPk(clubTeamId);
if (!team) {
throw new HttpError('Club team not found', 404);
}
let league;
// First, try to find existing league by name and season
const leagueName = completeData.leagueName || completeData.groupname;
league = await League.findOne({
where: {
name: leagueName,
seasonId: season.id,
clubId: team.clubId
}
});
if (league) {
devLog(`Found existing league: ${league.name} (ID: ${league.id})`);
// Update myTischtennis fields
await league.update({
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
} else if (team.leagueId) {
// Team has a league assigned, update it
league = await League.findByPk(team.leagueId);
if (league) {
devLog(`Updating team's existing league: ${league.name} (ID: ${league.id})`);
await league.update({
name: leagueName,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
}
} else if (createLeague) {
// Create new league
devLog(`Creating new league: ${leagueName}`);
league = await League.create({
name: leagueName,
seasonId: season.id,
clubId: team.clubId,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
});
} else {
throw new HttpError('League not found and team has no league assigned. Set createLeague=true to create one.', 400);
}
// Update team
await team.update({
myTischtennisTeamId: completeData.teamId,
leagueId: league.id,
seasonId: season.id
});
res.json({
success: true,
message: 'Team configured successfully',
data: {
team: {
id: team.id,
name: team.name,
myTischtennisTeamId: completeData.teamId
},
league: {
id: league.id,
name: league.name,
myTischtennisGroupId: completeData.groupId,
association: completeData.association,
groupname: completeData.groupname
},
season: {
id: season.id,
name: season.season
},
parsedData: completeData
}
});
} catch (error) {
next(error);
}
}
/**
* Manually fetch team data from myTischtennis
* POST /api/mytischtennis/fetch-team-data
* Body: { clubTeamId: number }
*/
async fetchTeamData(req, res, next) {
// Define outside of try/catch so catch has access
let account = null;
let team = null;
let myTischtennisUrl = null;
let requestStartTime = null;
try {
const { clubTeamId } = req.body;
const userIdOrEmail = req.headers.userid;
if (!clubTeamId) {
throw new HttpError('clubTeamId is required', 400);
}
if (!userIdOrEmail) {
throw new HttpError('User-ID fehlt. Bitte melden Sie sich an.', 401);
}
// Get actual user ID (userid header might be email address)
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
// It's an email, find the user
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError('User not found', 404);
}
userId = user.id;
}
// Get myTischtennis session (similar to memberService.updateRatingsFromMyTischtennis)
let session;
try {
session = await myTischtennisService.getSession(userId);
} catch (sessionError) {
// Versuche automatischen Login mit gespeicherten Credentials
try {
// Check if account exists and has password
const accountCheck = await myTischtennisService.getAccount(userId);
if (!accountCheck) {
throw new Error('MyTischtennis-Account nicht gefunden');
}
if (!accountCheck.encryptedPassword) {
throw new Error('Kein Passwort gespeichert. Bitte melden Sie sich in den MyTischtennis-Einstellungen an und speichern Sie Ihr Passwort.');
}
await myTischtennisService.verifyLogin(userId);
session = await myTischtennisService.getSession(userId);
} catch (loginError) {
const errorMessage = loginError.message || 'Automatischer Login fehlgeschlagen';
throw new HttpError(`MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen: ${errorMessage}. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.`, 401);
}
}
// Get account data (for clubId, etc.)
account = await myTischtennisService.getAccount(userId);
if (!account) {
throw new HttpError('MyTischtennis-Account nicht verknüpft. Bitte verknüpfen Sie Ihren Account in den MyTischtennis-Einstellungen.', 404);
}
// Get team with league and season
team = await ClubTeam.findByPk(clubTeamId, {
include: [
{
model: League,
as: 'league',
include: [
{
model: Season,
as: 'season'
}
]
}
]
});
if (!team) {
throw new HttpError(`Team mit ID ${clubTeamId} nicht gefunden`, 404);
}
// Verbesserte Validierung mit detaillierten Fehlermeldungen
if (!team.myTischtennisTeamId) {
throw new HttpError(`Team "${team.name}" (interne ID: ${team.id}) ist nicht für myTischtennis konfiguriert: myTischtennisTeamId fehlt. Bitte konfigurieren Sie das Team zuerst über die MyTischtennis-URL.`, 400);
}
if (!team.league) {
throw new HttpError('Team ist keiner Liga zugeordnet. Bitte ordnen Sie das Team einer Liga zu.', 400);
}
if (!team.league.myTischtennisGroupId) {
throw new HttpError('Liga ist nicht für myTischtennis konfiguriert: myTischtennisGroupId fehlt. Bitte konfigurieren Sie die Liga zuerst über die MyTischtennis-URL.', 400);
}
// Validate season before proceeding
if (!team.league.season || !team.league.season.season) {
throw new HttpError('Liga ist keiner Saison zugeordnet. Bitte ordnen Sie die Liga einer Saison zu.', 400);
}
// Build the URL that will be used - do this early so we can log it even if errors occur
const seasonFull = team.league.season.season;
const seasonParts = seasonFull.split('/');
const seasonShort = seasonParts.length === 2
? `${seasonParts[0].slice(-2)}/${seasonParts[1].slice(-2)}`
: seasonFull;
const seasonStr = seasonShort.replace('/', '--');
const teamnameEncoded = encodeURIComponent(team.name.replace(/\s/g, '_'));
myTischtennisUrl = `https://www.mytischtennis.de/click-tt/${team.league.association}/${seasonStr}/ligen/${team.league.groupname}/gruppe/${team.league.myTischtennisGroupId}/mannschaft/${team.myTischtennisTeamId}/${teamnameEncoded}/spielerbilanzen/gesamt`;
// Log the request to myTischtennis BEFORE making the call
// This ensures we always see what WILL BE sent, even if the call fails
requestStartTime = Date.now();
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: myTischtennisUrl.replace('https://www.mytischtennis.de', ''),
statusCode: null,
requestBody: JSON.stringify({
url: myTischtennisUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
leagueName: team.league.name,
association: team.league.association,
groupId: team.league.myTischtennisGroupId,
groupname: team.league.groupname,
season: seasonFull
}),
responseBody: null,
executionTime: null,
errorMessage: 'Request wird ausgeführt...',
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
// Silent fail - logging errors shouldn't break the request
}
// Fetch data for this specific team
// Note: fetchTeamResults will also log and update with actual response
const result = await autoFetchMatchResultsService.fetchTeamResults(
{
userId: account.userId,
email: account.email,
cookie: session.cookie,
accessToken: session.accessToken,
expiresAt: session.expiresAt,
getPassword: () => null // Not needed for manual fetch
},
team
);
// Also fetch and update league table data
let tableUpdateResult = null;
try {
await autoFetchMatchResultsService.fetchAndUpdateLeagueTable(account.userId, team.league.id);
tableUpdateResult = 'League table updated successfully';
} catch (error) {
tableUpdateResult = 'League table update failed: ' + error.message;
// Don't fail the entire request if table update fails
}
// Additionally update (Q)TTR ratings for the club
let ratingsUpdate = null;
try {
// Use already resolved userId instead of authcode to avoid header dependency
const ratingsResult = await MemberService.updateRatingsFromMyTischtennisByUserId(userId, team.clubId);
ratingsUpdate = ratingsResult?.response?.message || `Ratings update status: ${ratingsResult?.status}`;
} catch (ratingsErr) {
ratingsUpdate = 'Ratings update failed: ' + (ratingsErr.message || String(ratingsErr));
}
res.json({
success: true,
message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`,
data: {
fetchedCount: result.fetchedCount,
teamName: team.name,
tableUpdate: tableUpdateResult,
ratingsUpdate
}
});
} catch (error) {
// Update log with error information if we got far enough to build the URL
if (myTischtennisUrl && account && team) {
const requestExecutionTime = requestStartTime ? (Date.now() - requestStartTime) : null;
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: myTischtennisUrl.replace('https://www.mytischtennis.de', ''),
statusCode: 0,
requestBody: JSON.stringify({
url: myTischtennisUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
leagueName: team.league?.name,
association: team.league?.association,
groupname: team.league?.groupname,
groupId: team.league?.myTischtennisGroupId
}),
responseBody: null,
executionTime: requestExecutionTime,
errorMessage: error.message || String(error),
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
// Silent fail - logging errors shouldn't break the request
}
}
// Normalize HTTP status code (guard against strings)
const rawCode = error && (error.statusCode != null ? error.statusCode : error.status);
const parsed = Number(rawCode);
const status = Number.isInteger(parsed) && parsed >= 100 && parsed <= 599 ? parsed : 500;
const debug = {
message: error.message || String(error),
name: error.name,
stack: (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') ? (error.stack || null) : undefined,
team: team ? { id: team.id, name: team.name } : null,
league: team && team.league ? { id: team.league.id, name: team.league.name, association: team.league.association, groupId: team.league.myTischtennisGroupId, groupname: team.league.groupname } : null,
url: typeof myTischtennisUrl !== 'undefined' ? myTischtennisUrl : null
};
try {
if (!res.headersSent) {
// Spezieller Fall: myTischtennis-Reauth nötig → nicht 401 an FE senden, um App-Logout zu vermeiden
const isMyTischtennisAuthIssue = status === 401 && /MyTischtennis-Session abgelaufen|Automatischer Login fehlgeschlagen|Passwort gespeichert/i.test(debug.message || '');
if (isMyTischtennisAuthIssue) {
return res.status(200).json({ success: false, error: debug.message, debug, needsMyTischtennisReauth: true });
}
res.status(status).json({ success: false, error: debug.message, debug });
}
} catch (writeErr) {
// Fallback, falls Headers schon gesendet wurden
// eslint-disable-next-line no-console
console.error('[fetchTeamData] Response write failed:', writeErr);
}
}
}
/**
* Get myTischtennis URL for a team
* GET /api/mytischtennis/team-url/:teamId
*/
async getTeamUrl(req, res, next) {
try {
const { teamId } = req.params;
const team = await ClubTeam.findByPk(teamId, {
include: [
{
model: League,
as: 'league',
include: [
{
model: Season,
as: 'season'
}
]
}
]
});
if (!team) {
throw new HttpError('Team not found', 404);
}
if (!team.myTischtennisTeamId || !team.league || !team.league.myTischtennisGroupId) {
throw new HttpError('Team is not configured for myTischtennis', 400);
}
const url = myTischtennisUrlParserService.buildUrl({
association: team.league.association,
season: team.league.season?.season,
groupname: team.league.groupname,
groupId: team.league.myTischtennisGroupId,
teamId: team.myTischtennisTeamId,
teamname: team.name
});
res.json({
success: true,
url
});
} catch (error) {
next(error);
}
}
/**
* Configure league from myTischtennis table URL
* POST /api/mytischtennis/configure-league
* Body: { url: string, createSeason?: boolean }
*/
async configureLeague(req, res, next) {
try {
const { url, createSeason } = req.body;
const userIdOrEmail = req.headers.userid;
if (!url) {
throw new HttpError('URL is required', 400);
}
// Parse URL
const parsedData = myTischtennisUrlParserService.parseUrl(url);
if (parsedData.urlType !== 'table') {
throw new HttpError('URL must be a table URL (not a team URL)', 400);
}
// Find or create season
let season = await Season.findOne({
where: { season: parsedData.season }
});
if (!season && createSeason) {
season = await Season.create({
season: parsedData.season,
startDate: new Date(),
endDate: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 Jahr später
});
}
// Find or create league
let league = await League.findOne({
where: {
myTischtennisGroupId: parsedData.groupId,
association: parsedData.association
}
});
if (!league) {
league = await League.create({
name: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
myTischtennisGroupId: parsedData.groupId,
association: parsedData.association,
groupname: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
seasonId: season?.id || null
});
} else {
// Update existing league - aber nur wenn es sich wirklich geändert hat
if (league.name !== parsedData.groupnameOriginal) {
league.name = parsedData.groupnameOriginal;
league.groupname = parsedData.groupnameOriginal;
}
league.seasonId = season?.id || league.seasonId;
await league.save();
}
res.json({
success: true,
message: 'League configured successfully',
data: {
league: {
id: league.id,
name: league.name,
myTischtennisGroupId: league.myTischtennisGroupId,
association: league.association,
groupname: league.groupname
},
season: season ? {
id: season.id,
name: season.season
} : null,
parsedData
}
});
} catch (error) {
next(error);
}
}
}
export default new MyTischtennisUrlController();

View File

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

View File

@@ -1,23 +1,53 @@
import Participant from '../models/Participant.js';
import { devLog } from '../utils/logger.js';
export const getParticipants = async (req, res) => {
try {
const { dateId } = req.params;
const participants = await Participant.findAll({ where: { diaryDateId: dateId } });
const participants = await Participant.findAll({
where: { diaryDateId: dateId },
attributes: ['id', 'diaryDateId', 'memberId', 'groupId', 'notes', 'createdAt', 'updatedAt']
});
res.status(200).json(participants);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Abrufen der Teilnehmer' });
}
};
export const updateParticipantGroup = async (req, res) => {
try {
const { dateId, memberId } = req.params;
const { groupId } = req.body;
const participant = await Participant.findOne({
where: {
diaryDateId: dateId,
memberId: memberId
}
});
if (!participant) {
return res.status(404).json({ error: 'Teilnehmer nicht gefunden' });
}
participant.groupId = groupId || null;
await participant.save();
res.status(200).json(participant);
} catch (error) {
devLog(error);
res.status(500).json({ error: 'Fehler beim Aktualisieren der Teilnehmer-Gruppenzuordnung' });
}
};
export const addParticipant = async (req, res) => {
try {
const { diaryDateId, memberId } = req.body;
const participant = await Participant.create({ diaryDateId, memberId });
res.status(201).json(participant);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Hinzufügen des Teilnehmers' });
}
};
@@ -28,7 +58,7 @@ export const removeParticipant = async (req, res) => {
await Participant.destroy({ where: { diaryDateId, memberId } });
res.status(200).json({ message: 'Teilnehmer entfernt' });
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Fehler beim Entfernen des Teilnehmers' });
}
};

View File

@@ -0,0 +1,167 @@
import permissionService from '../services/permissionService.js';
/**
* Get user's permissions for a club
*/
export const getUserPermissions = async (req, res) => {
try {
const { clubId } = req.params;
const userId = req.user.id;
// Validierung: clubId muss eine gültige Zahl sein
const parsedClubId = parseInt(clubId, 10);
if (isNaN(parsedClubId) || parsedClubId <= 0) {
return res.status(400).json({ error: 'Ungültige Club-ID' });
}
const permissions = await permissionService.getUserClubPermissions(userId, parsedClubId);
if (!permissions) {
return res.status(404).json({ error: 'Keine Berechtigungen gefunden' });
}
res.json(permissions);
} catch (error) {
console.error('Error getting user permissions:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Berechtigungen' });
}
};
/**
* Get all club members with their permissions
*/
export const getClubMembersWithPermissions = async (req, res) => {
try {
const { clubId } = req.params;
const userId = req.user.id;
const members = await permissionService.getClubMembersWithPermissions(
parseInt(clubId),
userId
);
res.json(members);
} catch (error) {
console.error('Error getting club members with permissions:', error);
if (error.message === 'Keine Berechtigung zum Anzeigen von Berechtigungen') {
return res.status(403).json({ error: error.message });
}
res.status(500).json({ error: 'Fehler beim Abrufen der Mitglieder' });
}
};
/**
* Update user role
*/
export const updateUserRole = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { role } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setUserRole(
parseInt(targetUserId),
parseInt(clubId),
role,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user role:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
/**
* Update user custom permissions
*/
export const updateUserPermissions = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { permissions } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setCustomPermissions(
parseInt(targetUserId),
parseInt(clubId),
permissions,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user permissions:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
/**
* Get available roles
*/
export const getAvailableRoles = async (req, res) => {
try {
const roles = permissionService.getAvailableRoles();
res.json(roles);
} catch (error) {
console.error('Error getting available roles:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Rollen' });
}
};
/**
* Get permission structure
*/
export const getPermissionStructure = async (req, res) => {
try {
const structure = permissionService.getPermissionStructure();
res.json(structure);
} catch (error) {
console.error('Error getting permission structure:', error);
res.status(500).json({ error: 'Fehler beim Abrufen der Berechtigungsstruktur' });
}
};
/**
* Update user status (activate/deactivate)
*/
export const updateUserStatus = async (req, res) => {
try {
const { clubId, userId: targetUserId } = req.params;
const { approved } = req.body;
const updatingUserId = req.user.id;
const result = await permissionService.setUserStatus(
parseInt(targetUserId),
parseInt(clubId),
approved,
updatingUserId
);
res.json(result);
} catch (error) {
console.error('Error updating user status:', error);
if (error.message && error.message.toLowerCase().includes('keine berechtigung')) {
return res.status(403).json({ error: error.message });
}
res.status(400).json({ error: error.message });
}
};
export default {
getUserPermissions,
getClubMembersWithPermissions,
updateUserRole,
updateUserPermissions,
updateUserStatus,
getAvailableRoles,
getPermissionStructure
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,9 @@ export const authenticate = async (req, res, next) => {
if (!token) {
token = req.headers['authcode'];
}
if (!token) {
token = req.query?.authcode || req.query?.token || null;
}
if (!token) {
return res.status(401).json({ error: 'Unauthorized: Token fehlt' });
}

View File

@@ -0,0 +1,215 @@
import permissionService from '../services/permissionService.js';
/**
* Authorization Middleware
* Checks if user has permission to access a resource
*/
/**
* Check if user has permission for a specific resource and action
* @param {string} resource - Resource name (diary, members, teams, etc.)
* @param {string} action - Action type (read, write, delete)
* @returns {Function} Express middleware function
*/
export const authorize = (resource, action = 'read') => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
// Get clubId from various possible sources
const clubId =
req.params.clubId ??
req.params.clubid ??
req.params.id ??
req.body.clubId ??
req.body.clubid ??
req.query.clubId ??
req.query.clubid;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
// Check permission
const hasPermission = await permissionService.hasPermission(
userId,
parseInt(clubId),
resource,
action
);
if (!hasPermission) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: `Fehlende Berechtigung: ${resource}.${action}`
});
}
// Store permissions in request for later use
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Authorization error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user is club owner
* @returns {Function} Express middleware function
*/
export const requireOwner = () => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId =
req.params.clubId ??
req.params.clubid ??
req.params.id ??
req.body.clubId ??
req.body.clubid ??
req.query.clubId ??
req.query.clubid;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || !userPermissions.isOwner) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: 'Nur der Club-Ersteller hat Zugriff'
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Owner check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user is admin (owner or admin role)
* @returns {Function} Express middleware function
*/
export const requireAdmin = () => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId =
req.params.clubId ??
req.params.clubid ??
req.params.id ??
req.body.clubId ??
req.body.clubid ??
req.query.clubId ??
req.query.clubid;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || (userPermissions.role !== 'admin' && !userPermissions.isOwner)) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: 'Administrator-Rechte erforderlich'
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Admin check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
/**
* Check if user has any of the specified roles
* @param {string[]} roles - Array of allowed roles
* @returns {Function} Express middleware function
*/
export const requireRole = (roles) => {
return async (req, res, next) => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: 'Nicht authentifiziert' });
}
const clubId =
req.params.clubId ??
req.params.clubid ??
req.params.id ??
req.body.clubId ??
req.body.clubid ??
req.query.clubId ??
req.query.clubid;
if (!clubId) {
return res.status(400).json({ error: 'Club-ID fehlt' });
}
const userPermissions = await permissionService.getUserClubPermissions(
userId,
parseInt(clubId)
);
if (!userPermissions || !roles.includes(userPermissions.role)) {
return res.status(403).json({
error: 'Keine Berechtigung',
details: `Erforderliche Rolle: ${roles.join(', ')}`
});
}
req.userPermissions = userPermissions;
next();
} catch (error) {
console.error('Role check error:', error);
res.status(500).json({ error: 'Fehler bei der Berechtigungsprüfung' });
}
};
};
export default {
authorize,
requireOwner,
requireAdmin,
requireRole
};

View File

@@ -0,0 +1,87 @@
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
*/
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);
}
});
next();
};

View File

@@ -0,0 +1,17 @@
-- Create table for storing multiple images per member
CREATE TABLE IF NOT EXISTS `member_image` (
`id` INT NOT NULL AUTO_INCREMENT,
`member_id` INT NOT NULL,
`file_name` VARCHAR(255) NOT NULL,
`sort_order` INT NOT NULL DEFAULT 0,
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_member_image_member_id` (`member_id`),
CONSTRAINT `fk_member_image_member`
FOREIGN KEY (`member_id`)
REFERENCES `member` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
ALTER TABLE clubs
ADD COLUMN IF NOT EXISTS greeting_text TEXT NULL,
ADD COLUMN IF NOT EXISTS association_member_number VARCHAR(255) NULL;

View File

@@ -0,0 +1,9 @@
-- Migration: Add group_id to participants table
-- This allows assigning participants to groups for training organization
ALTER TABLE participants
ADD COLUMN group_id INTEGER NULL REFERENCES "group"(id) ON DELETE SET NULL ON UPDATE CASCADE;
-- Add index for better query performance
CREATE INDEX IF NOT EXISTS idx_participants_group_id ON participants(group_id);

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
-- Add postal_code column to member table
ALTER TABLE `member`
ADD COLUMN `postal_code` TEXT NULL COMMENT 'Postal code (PLZ)' AFTER `city`;
-- Create member_contact table for multiple phone numbers and email addresses
CREATE TABLE IF NOT EXISTS `member_contact` (
`id` INT NOT NULL AUTO_INCREMENT,
`member_id` INT NOT NULL,
`type` ENUM('phone', 'email') NOT NULL COMMENT 'Type of contact: phone or email',
`value` TEXT NOT NULL,
`is_parent` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Whether this contact belongs to a parent',
`parent_name` TEXT NULL COMMENT 'Name of the parent (e.g. "Mutter", "Vater", "Elternteil 1")',
`is_primary` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Whether this is the primary contact of this type',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
INDEX `idx_member_id` (`member_id`),
INDEX `idx_type` (`type`),
CONSTRAINT `fk_member_contact_member`
FOREIGN KEY (`member_id`)
REFERENCES `member` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Migrate existing phone numbers from member.phone to member_contact
INSERT INTO `member_contact` (`member_id`, `type`, `value`, `is_parent`, `parent_name`, `is_primary`, `created_at`, `updated_at`)
SELECT
`id` AS `member_id`,
'phone' AS `type`,
`phone` AS `value`,
FALSE AS `is_parent`,
NULL AS `parent_name`,
TRUE AS `is_primary`,
NOW() AS `created_at`,
NOW() AS `updated_at`
FROM `member`
WHERE `phone` IS NOT NULL
AND `phone` != ''
AND TRIM(`phone`) != '';
-- Migrate existing email addresses from member.email to member_contact
INSERT INTO `member_contact` (`member_id`, `type`, `value`, `is_parent`, `parent_name`, `is_primary`, `created_at`, `updated_at`)
SELECT
`id` AS `member_id`,
'email' AS `type`,
`email` AS `value`,
FALSE AS `is_parent`,
NULL AS `parent_name`,
TRUE AS `is_primary`,
NOW() AS `created_at`,
NOW() AS `updated_at`
FROM `member`
WHERE `email` IS NOT NULL
AND `email` != ''
AND TRIM(`email`) != '';

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
-- Add role and permissions columns to user_club table
ALTER TABLE `user_club`
ADD COLUMN `role` VARCHAR(50) DEFAULT 'member' COMMENT 'User role: admin, trainer, team_manager, member' AFTER `approved`,
ADD COLUMN `permissions` JSON NULL COMMENT 'Specific permissions: {diary: {read: true, write: true}, members: {...}, ...}' AFTER `role`,
ADD COLUMN `is_owner` BOOLEAN DEFAULT FALSE COMMENT 'True if user created the club' AFTER `permissions`;
-- Create index for faster role lookups
CREATE INDEX `idx_user_club_role` ON `user_club` (`role`);
CREATE INDEX `idx_user_club_owner` ON `user_club` (`is_owner`);
-- Set existing approved users as members
UPDATE `user_club` SET `role` = 'member' WHERE `approved` = 1 AND `role` IS NULL;
-- If there's a user who created the club (we need to identify them somehow)
-- For now, we'll need to manually set the owner after migration

View File

@@ -0,0 +1,8 @@
-- Add player tracking fields to match table
-- These fields store arrays of member IDs for different participation states
ALTER TABLE `match`
ADD COLUMN `players_ready` JSON NULL COMMENT 'Array of member IDs who are ready to play' AFTER `pdf_url`,
ADD COLUMN `players_planned` JSON NULL COMMENT 'Array of member IDs who are planned to play' AFTER `players_ready`,
ADD COLUMN `players_played` JSON NULL COMMENT 'Array of member IDs who actually played' AFTER `players_planned`;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
-- Migration: Create api_log table for comprehensive request/response and execution logging
CREATE TABLE IF NOT EXISTS api_log (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NULL,
method VARCHAR(10) NOT NULL COMMENT 'HTTP method (GET, POST, PUT, DELETE, etc.)',
path VARCHAR(500) NOT NULL COMMENT 'Request path',
status_code INT NULL COMMENT 'HTTP status code',
request_body TEXT NULL COMMENT 'Request body (truncated if too long)',
response_body TEXT NULL COMMENT 'Response body (truncated if too long)',
execution_time INT NULL COMMENT 'Execution time in milliseconds',
error_message TEXT NULL COMMENT 'Error message if request failed',
ip_address VARCHAR(45) NULL COMMENT 'Client IP address',
user_agent VARCHAR(500) NULL COMMENT 'User agent string',
log_type ENUM('api_request', 'scheduler', 'cron_job', 'manual') NOT NULL DEFAULT 'api_request' COMMENT 'Type of log entry',
scheduler_job_type VARCHAR(50) NULL COMMENT 'Type of scheduler job (rating_updates, match_results, etc.)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE SET NULL ON UPDATE CASCADE,
INDEX idx_api_log_user_id (user_id, created_at),
INDEX idx_api_log_path (path, created_at),
INDEX idx_api_log_log_type (log_type, created_at),
INDEX idx_api_log_created_at (created_at),
INDEX idx_api_log_status_code (status_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,38 @@
-- Update existing user_club entries with default permissions
-- This migration sets default values for role and is_owner for existing club memberships
-- Set default role to 'member' for all approved users who don't have a role yet
UPDATE `user_club`
SET `role` = 'member'
WHERE `approved` = 1
AND (`role` IS NULL OR `role` = '');
-- Optionally: Set the first approved user of each club as owner
-- This finds the user with the lowest user_id per club (oldest member) and marks them as owner
UPDATE `user_club` AS uc1
INNER JOIN (
SELECT `club_id`, MIN(`user_id`) as `first_user_id`
FROM `user_club`
WHERE `approved` = 1
GROUP BY `club_id`
) AS uc2 ON uc1.`club_id` = uc2.`club_id` AND uc1.`user_id` = uc2.`first_user_id`
SET
uc1.`is_owner` = 1,
uc1.`role` = 'admin';
-- Verify the changes
SELECT
uc.`club_id`,
c.`name` as club_name,
uc.`user_id`,
u.`email` as user_email,
uc.`role`,
uc.`is_owner`,
uc.`approved`
FROM `user_club` uc
LEFT JOIN `club` c ON c.`id` = uc.`club_id`
LEFT JOIN `user` u ON u.`id` = uc.`user_id`
WHERE uc.`approved` = 1
ORDER BY uc.`club_id`, uc.`is_owner` DESC, uc.`user_id`;

102
backend/models/ApiLog.js Normal file
View File

@@ -0,0 +1,102 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import User from './User.js';
const ApiLog = sequelize.define('ApiLog', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false,
},
userId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: User,
key: 'id',
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
},
method: {
type: DataTypes.STRING(10),
allowNull: false,
comment: 'HTTP method (GET, POST, PUT, DELETE, etc.)'
},
path: {
type: DataTypes.STRING(500),
allowNull: false,
comment: 'Request path'
},
statusCode: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'HTTP status code'
},
requestBody: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Request body (truncated if too long)'
},
responseBody: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Response body (truncated if too long)'
},
executionTime: {
type: DataTypes.INTEGER,
allowNull: true,
comment: 'Execution time in milliseconds'
},
errorMessage: {
type: DataTypes.TEXT,
allowNull: true,
comment: 'Error message if completes failed'
},
ipAddress: {
type: DataTypes.STRING(45),
allowNull: true,
comment: 'Client IP address'
},
userAgent: {
type: DataTypes.STRING(500),
allowNull: true,
comment: 'User agent string'
},
logType: {
type: DataTypes.ENUM('api_request', 'scheduler', 'cron_job', 'manual'),
allowNull: false,
defaultValue: 'api_request',
comment: 'Type of log entry'
},
schedulerJobType: {
type: DataTypes.STRING(50),
allowNull: true,
comment: 'Type of scheduler job (rating_updates, match_results, etc.)'
},
}, {
underscored: true,
tableName: 'api_log',
timestamps: true,
indexes: [
{
fields: ['user_id', 'created_at']
},
{
fields: ['path', 'created_at']
},
{
fields: ['log_type', 'created_at']
},
{
fields: ['created_at']
},
{
fields: ['status_code']
}
]
});
export default ApiLog;

View File

@@ -7,6 +7,16 @@ const Club = sequelize.define('Club', {
allowNull: false,
unique: true,
},
greetingText: {
type: DataTypes.TEXT,
allowNull: true,
field: 'greeting_text'
},
associationMemberNumber: {
type: DataTypes.STRING,
allowNull: true,
field: 'association_member_number'
},
}, {
tableName: 'clubs',
underscored: true,

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,13 +45,14 @@ const Member = sequelize.define('Member', {
},
birthDate: {
type: DataTypes.STRING,
allowNull: false,
allowNull: true,
set(value) {
const encryptedValue = encryptData(value);
const encryptedValue = encryptData(value || '');
this.setDataValue('birthDate', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('birthDate');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
}
},
@@ -91,6 +92,21 @@ const Member = sequelize.define('Member', {
return decryptData(encryptedValue);
}
},
postalCode: {
type: DataTypes.STRING,
allowNull: true,
set(value) {
const encryptedValue = encryptData(value || '');
this.setDataValue('postalCode', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('postalCode');
if (!encryptedValue) return null;
return decryptData(encryptedValue);
},
field: 'postal_code',
comment: 'Postal code (PLZ)'
},
email: {
type: DataTypes.STRING,
allowNull: false,
@@ -122,6 +138,35 @@ const Member = sequelize.define('Member', {
allowNull: false,
default: false,
}
,
gender: {
type: DataTypes.ENUM('male','female','diverse','unknown'),
allowNull: true,
defaultValue: 'unknown'
},
ttr: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null
},
qttr: {
type: DataTypes.INTEGER,
allowNull: true,
defaultValue: null
},
memberFormHandedOver: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'member_form_handed_over',
comment: 'Mitgliedsformular ausgehändigt'
},
myTischtennisPlayerId: {
type: DataTypes.STRING,
allowNull: true,
comment: 'Player ID from myTischtennis (e.g. NU2705037)',
field: 'my_tischtennis_player_id'
}
}, {
underscored: true,
sequelize,
@@ -135,13 +180,7 @@ const Member = sequelize.define('Member', {
member.save();
},
}
},
{
underscored: true,
tableName: 'log',
timestamps: true
}
);
});
Member.belongsTo(Club, { as: 'club' });
Club.hasMany(Member, { as: 'members' });

View File

@@ -0,0 +1,89 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Member from './Member.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const MemberContact = sequelize.define('MemberContact', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
memberId: {
type: DataTypes.INTEGER,
allowNull: false,
references: {
model: 'member',
key: 'id'
},
onDelete: 'CASCADE',
field: 'member_id'
},
type: {
type: DataTypes.ENUM('phone', 'email'),
allowNull: false,
comment: 'Type of contact: phone or email'
},
value: {
type: DataTypes.STRING,
allowNull: false,
set(value) {
const encryptedValue = encryptData(value);
this.setDataValue('value', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('value');
if (!encryptedValue) return null;
try {
return decryptData(encryptedValue);
} catch (error) {
console.error('[MemberContact] Error decrypting value:', error);
return encryptedValue; // Fallback: return encrypted value if decryption fails
}
}
},
isParent: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'is_parent',
comment: 'Whether this contact belongs to a parent'
},
parentName: {
type: DataTypes.STRING,
allowNull: true,
set(value) {
if (value) {
const encryptedValue = encryptData(value);
this.setDataValue('parentName', encryptedValue);
} else {
this.setDataValue('parentName', null);
}
},
get() {
const encryptedValue = this.getDataValue('parentName');
return encryptedValue ? decryptData(encryptedValue) : null;
},
field: 'parent_name',
comment: 'Name of the parent (e.g. "Mutter", "Vater", "Elternteil 1")'
},
isPrimary: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'is_primary',
comment: 'Whether this is the primary contact of this type'
}
}, {
underscored: true,
sequelize,
modelName: 'MemberContact',
tableName: 'member_contact',
timestamps: true
});
// Associations are defined in models/index.js to avoid duplicate alias errors
export default MemberContact;

View File

@@ -0,0 +1,40 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
const MemberImage = sequelize.define('MemberImage', {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
allowNull: false
},
memberId: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'member_id',
references: {
model: 'member',
key: 'id'
},
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
},
fileName: {
type: DataTypes.STRING(255),
allowNull: false,
field: 'file_name'
},
sortOrder: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
field: 'sort_order'
}
}, {
tableName: 'member_image',
underscored: true,
timestamps: true
});
export default MemberImage;

View File

@@ -0,0 +1,117 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Club from './Club.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const MemberTransferConfig = sequelize.define('MemberTransferConfig', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
allowNull: false
},
clubId: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
references: {
model: Club,
key: 'id'
},
onDelete: 'CASCADE'
},
server: {
type: DataTypes.STRING,
allowNull: true,
field: 'server',
comment: 'Base URL des Servers (z.B. https://example.com)'
},
loginEndpoint: {
type: DataTypes.STRING,
allowNull: true,
field: 'login_endpoint',
comment: 'Relativer Pfad zum Login-Endpoint (z.B. /api/auth/login)'
},
loginFormat: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: 'json',
field: 'login_format'
},
encryptedLoginCredentials: {
type: DataTypes.TEXT,
allowNull: true,
field: 'encrypted_login_credentials',
comment: 'Verschlüsselte Login-Daten als JSON-String'
},
transferEndpoint: {
type: DataTypes.STRING,
allowNull: false,
field: 'transfer_endpoint',
comment: 'Relativer Pfad zum Übertragungs-Endpoint (z.B. /api/members/bulk)'
},
transferMethod: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'POST',
field: 'transfer_method'
},
transferFormat: {
type: DataTypes.STRING,
allowNull: false,
defaultValue: 'json',
field: 'transfer_format'
},
transferTemplate: {
type: DataTypes.TEXT,
allowNull: false,
field: 'transfer_template'
},
useBulkMode: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
field: 'use_bulk_mode'
},
bulkWrapperTemplate: {
type: DataTypes.TEXT,
allowNull: true,
field: 'bulk_wrapper_template',
comment: 'Optionales Template für die äußere Struktur im Bulk-Modus (z.B. {"data": {"members": "{{members}}"}})'
}
}, {
underscored: true,
tableName: 'member_transfer_config',
timestamps: true
});
// Getter/Setter für verschlüsselte Login-Daten
MemberTransferConfig.prototype.getLoginCredentials = function() {
if (!this.encryptedLoginCredentials) {
return null;
}
try {
const decrypted = decryptData(this.encryptedLoginCredentials);
return JSON.parse(decrypted);
} catch (error) {
console.error('[MemberTransferConfig] Error decrypting login credentials:', error);
return null;
}
};
MemberTransferConfig.prototype.setLoginCredentials = function(credentials) {
if (!credentials || Object.keys(credentials).length === 0) {
this.encryptedLoginCredentials = null;
return;
}
try {
const jsonString = JSON.stringify(credentials);
this.encryptedLoginCredentials = encryptData(jsonString);
} catch (error) {
console.error('[MemberTransferConfig] Error encrypting login credentials:', error);
this.encryptedLoginCredentials = null;
}
};
export default MemberTransferConfig;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import Member from './Member.js';
import DiaryDate from './DiaryDates.js';
import Group from './Group.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const Participant = sequelize.define('Participant', {
@@ -27,6 +28,16 @@ const Participant = sequelize.define('Participant', {
key: 'id'
}
},
groupId: {
type: DataTypes.INTEGER,
allowNull: true,
references: {
model: Group,
key: 'id'
},
onDelete: 'SET NULL',
onUpdate: 'CASCADE'
},
notes: {
type: DataTypes.STRING(4096),
allowNull: true,

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import Club from './Club.js';
const UserClub = sequelize.define('UserClub', {
userId: {
type: DataTypes.INTEGER,
primaryKey: true,
references: {
model: User,
key: 'id',
@@ -13,6 +14,7 @@ const UserClub = sequelize.define('UserClub', {
},
clubId: {
type: DataTypes.INTEGER,
primaryKey: true,
references: {
model: Club,
key: 'id',
@@ -22,6 +24,23 @@ const UserClub = sequelize.define('UserClub', {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
role: {
type: DataTypes.STRING(50),
defaultValue: 'member',
allowNull: false,
comment: 'User role: admin, trainer, team_manager, member'
},
permissions: {
type: DataTypes.JSON,
allowNull: true,
comment: 'Specific permissions: {diary: {read: true, write: true}, members: {...}, ...}'
},
isOwner: {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false,
comment: 'True if user created the club'
}
}, {
underscored: true,
tableName: 'user_club',

View File

@@ -13,10 +13,14 @@ import DiaryDateTag from './DiaryDateTag.js';
import DiaryMemberNote from './DiaryMemberNote.js';
import DiaryMemberTag from './DiaryMemberTag.js';
import PredefinedActivity from './PredefinedActivity.js';
import DiaryMemberActivity from './DiaryMemberActivity.js';
import PredefinedActivityImage from './PredefinedActivityImage.js';
import DiaryDateActivity from './DiaryDateActivity.js';
import Match from './Match.js';
import League from './League.js';
import Team from './Team.js';
import ClubTeam from './ClubTeam.js';
import TeamDocument from './TeamDocument.js';
import Season from './Season.js';
import Location from './Location.js';
import Group from './Group.js';
@@ -28,6 +32,26 @@ import TournamentMatch from './TournamentMatch.js';
import TournamentResult from './TournamentResult.js';
import Accident from './Accident.js';
import UserToken from './UserToken.js';
import OfficialTournament from './OfficialTournament.js';
import OfficialCompetition from './OfficialCompetition.js';
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
import MyTischtennis from './MyTischtennis.js';
import MyTischtennisUpdateHistory from './MyTischtennisUpdateHistory.js';
import MyTischtennisFetchLog from './MyTischtennisFetchLog.js';
import ApiLog from './ApiLog.js';
import MemberTransferConfig from './MemberTransferConfig.js';
import MemberContact from './MemberContact.js';
import MemberImage from './MemberImage.js';
// Official tournaments relations
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
// Official competition participations
OfficialCompetition.hasMany(OfficialCompetitionMember, { foreignKey: 'competitionId', as: 'members' });
OfficialCompetitionMember.belongsTo(OfficialCompetition, { foreignKey: 'competitionId', as: 'competition' });
OfficialTournament.hasMany(OfficialCompetitionMember, { foreignKey: 'tournamentId', as: 'competitionMembers' });
OfficialCompetitionMember.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
Member.hasMany(OfficialCompetitionMember, { foreignKey: 'memberId', as: 'officialCompetitionEntries' });
OfficialCompetitionMember.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
User.hasMany(Log, { foreignKey: 'userId' });
Log.belongsTo(User, { foreignKey: 'userId' });
@@ -76,6 +100,14 @@ DiaryDateActivity.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDa
PredefinedActivity.hasMany(DiaryDateActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivities' });
DiaryDateActivity.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' });
// DiaryMemberActivity links a Participant to a DiaryDateActivity
DiaryMemberActivity.belongsTo(DiaryDateActivity, { foreignKey: 'diaryDateActivityId', as: 'activity' });
DiaryDateActivity.hasMany(DiaryMemberActivity, { foreignKey: 'diaryDateActivityId', as: 'activityMembers' });
DiaryMemberActivity.belongsTo(Participant, { foreignKey: 'participantId', as: 'participant' });
Participant.hasMany(DiaryMemberActivity, { foreignKey: 'participantId', as: 'memberActivities' });
// PredefinedActivity Images
PredefinedActivity.hasMany(PredefinedActivityImage, { foreignKey: 'predefinedActivityId', as: 'images' });
PredefinedActivityImage.belongsTo(PredefinedActivity, { foreignKey: 'predefinedActivityId', as: 'predefinedActivity' });
Club.hasMany(Match, { foreignKey: 'clubId', as: 'matches' });
Match.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
@@ -94,8 +126,28 @@ Team.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Club.hasMany(League, { foreignKey: 'clubId', as: 'leagues' });
League.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Match.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
Season.hasMany(Match, { foreignKey: 'seasonId', as: 'matches' });
Season.hasMany(League, { foreignKey: 'seasonId', as: 'leagues' });
League.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
League.hasMany(Team, { foreignKey: 'leagueId', as: 'teams' });
Team.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(Team, { foreignKey: 'seasonId', as: 'teams' });
Team.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// ClubTeam relationships
Club.hasMany(ClubTeam, { foreignKey: 'clubId', as: 'clubTeams' });
ClubTeam.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
League.hasMany(ClubTeam, { foreignKey: 'leagueId', as: 'clubTeams' });
ClubTeam.belongsTo(League, { foreignKey: 'leagueId', as: 'league' });
Season.hasMany(ClubTeam, { foreignKey: 'seasonId', as: 'clubTeams' });
ClubTeam.belongsTo(Season, { foreignKey: 'seasonId', as: 'season' });
// TeamDocument relationships
ClubTeam.hasMany(TeamDocument, { foreignKey: 'clubTeamId', as: 'documents' });
TeamDocument.belongsTo(ClubTeam, { foreignKey: 'clubTeamId', as: 'clubTeam' });
Match.belongsTo(Location, { foreignKey: 'locationId', as: 'location' });
Location.hasMany(Match, { foreignKey: 'locationId', as: 'matches' });
@@ -112,7 +164,7 @@ Club.hasMany(UserClub, { foreignKey: 'clubId' });
Group.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDateGroup' });
DiaryDate.hasMany(Group, { foreignKey: 'diaryDateId', as: 'groupsDiaryDate' });
GroupActivity.belongsTo(DiaryDateActivity, { foreignKey: 'id', as: 'activityGroupActivity' });
GroupActivity.belongsTo(DiaryDateActivity, { foreignKey: 'diaryDateActivity', as: 'activityGroupActivity' });
DiaryDateActivity.hasMany(GroupActivity, { foreignKey: 'diaryDateActivity', as: 'groupActivities' });
Group.hasOne(GroupActivity, { foreignKey: 'groupId', as: 'groupGroupActivity' });
@@ -181,6 +233,27 @@ Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
Accident.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' });
User.hasOne(MyTischtennis, { foreignKey: 'userId', as: 'myTischtennis' });
MyTischtennis.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(MyTischtennisUpdateHistory, { foreignKey: 'userId', as: 'updateHistory' });
MyTischtennisUpdateHistory.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(MyTischtennisFetchLog, { foreignKey: 'userId', as: 'fetchLogs' });
MyTischtennisFetchLog.belongsTo(User, { foreignKey: 'userId', as: 'user' });
User.hasMany(ApiLog, { foreignKey: 'userId', as: 'apiLogs' });
ApiLog.belongsTo(User, { foreignKey: 'userId', as: 'user' });
Club.hasOne(MemberTransferConfig, { foreignKey: 'clubId', as: 'memberTransferConfig' });
MemberTransferConfig.belongsTo(Club, { foreignKey: 'clubId', as: 'club' });
Member.hasMany(MemberContact, { foreignKey: 'memberId', as: 'contacts' });
MemberContact.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
Member.hasMany(MemberImage, { foreignKey: 'memberId', as: 'images' });
MemberImage.belongsTo(Member, { foreignKey: 'memberId', as: 'member' });
export {
User,
Log,
@@ -198,10 +271,14 @@ export {
DiaryMemberNote,
DiaryMemberTag,
PredefinedActivity,
DiaryMemberActivity,
PredefinedActivityImage,
DiaryDateActivity,
Match,
League,
Team,
ClubTeam,
TeamDocument,
Group,
GroupActivity,
Tournament,
@@ -211,4 +288,14 @@ export {
TournamentResult,
Accident,
UserToken,
OfficialTournament,
OfficialCompetition,
OfficialCompetitionMember,
MyTischtennis,
MyTischtennisUpdateHistory,
MyTischtennisFetchLog,
ApiLog,
MemberTransferConfig,
MemberContact,
MemberImage,
};

3204
backend/node_modules/.package-lock.json generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ This package contains type definitions for node (https://nodejs.org/).
Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node.
### Additional Details
* Last updated: Tue, 23 Jul 2024 18:09:25 GMT
* Last updated: Mon, 03 Nov 2025 01:29:59 GMT
* Dependencies: [undici-types](https://npmjs.com/package/undici-types)
# Credits
These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [Alberto Schiabel](https://github.com/jkomyno), [Alvis HT Tang](https://github.com/alvis), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [Chigozirim C.](https://github.com/smac89), [David Junger](https://github.com/touffy), [Deividas Bakanas](https://github.com/DeividasBakanas), [Eugene Y. Q. Shen](https://github.com/eyqs), [Hannes Magnusson](https://github.com/Hannes-Magnusson-CK), [Huw](https://github.com/hoo29), [Kelvin Jin](https://github.com/kjin), [Klaus Meinhardt](https://github.com/ajafff), [Lishude](https://github.com/islishude), [Mariusz Wiktorczyk](https://github.com/mwiktorczyk), [Mohsen Azimi](https://github.com/mohsen1), [Nikita Galkin](https://github.com/galkin), [Parambir Singh](https://github.com/parambirs), [Sebastian Silbermann](https://github.com/eps1lon), [Thomas den Hollander](https://github.com/ThomasdenH), [Wilco Bakker](https://github.com/WilcoBakker), [wwwy3y3](https://github.com/wwwy3y3), [Samuel Ainsworth](https://github.com/samuela), [Kyle Uehlein](https://github.com/kuehlein), [Thanik Bhongbhibhat](https://github.com/bhongy), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Victor Perin](https://github.com/victorperin), [Yongsheng Zhang](https://github.com/ZYSzys), [NodeJS Contributors](https://github.com/NodeJS), [Linus Unnebäck](https://github.com/LinusU), [wafuwafu13](https://github.com/wafuwafu13), [Matteo Collina](https://github.com/mcollina), and [Dmitry Semigradsky](https://github.com/Semigradsky).
These definitions were written by [Microsoft TypeScript](https://github.com/Microsoft), [Alberto Schiabel](https://github.com/jkomyno), [Andrew Makarov](https://github.com/r3nya), [Benjamin Toueg](https://github.com/btoueg), [David Junger](https://github.com/touffy), [Mohsen Azimi](https://github.com/mohsen1), [Nikita Galkin](https://github.com/galkin), [Sebastian Silbermann](https://github.com/eps1lon), [Wilco Bakker](https://github.com/WilcoBakker), [Marcin Kopacz](https://github.com/chyzwar), [Trivikram Kamat](https://github.com/trivikr), [Junxiao Shi](https://github.com/yoursunny), [Ilia Baryshnikov](https://github.com/qwelias), [ExE Boss](https://github.com/ExE-Boss), [Piotr Błażejewicz](https://github.com/peterblazejewicz), [Anna Henningsen](https://github.com/addaleax), [Victor Perin](https://github.com/victorperin), [NodeJS Contributors](https://github.com/NodeJS), [Linus Unnebäck](https://github.com/LinusU), [wafuwafu13](https://github.com/wafuwafu13), [Matteo Collina](https://github.com/mcollina), [Dmitry Semigradsky](https://github.com/Semigradsky), [René](https://github.com/Renegade334), and [Yagiz Nizipli](https://github.com/anonrig).

View File

@@ -1,20 +1,166 @@
/**
* The `node:assert` module provides a set of assertion functions for verifying
* invariants.
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/assert.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/assert.js)
*/
declare module "assert" {
import strict = require("assert/strict");
/**
* An alias of {@link ok}.
* An alias of {@link assert.ok}.
* @since v0.5.9
* @param value The input that is checked for being truthy.
*/
function assert(value: unknown, message?: string | Error): asserts value;
const kOptions: unique symbol;
namespace assert {
type AssertMethodNames =
| "deepEqual"
| "deepStrictEqual"
| "doesNotMatch"
| "doesNotReject"
| "doesNotThrow"
| "equal"
| "fail"
| "ifError"
| "match"
| "notDeepEqual"
| "notDeepStrictEqual"
| "notEqual"
| "notStrictEqual"
| "ok"
| "partialDeepStrictEqual"
| "rejects"
| "strictEqual"
| "throws";
interface AssertOptions {
/**
* If set to `'full'`, shows the full diff in assertion errors.
* @default 'simple'
*/
diff?: "simple" | "full" | undefined;
/**
* If set to `true`, non-strict methods behave like their
* corresponding strict methods.
* @default true
*/
strict?: boolean | undefined;
/**
* If set to `true`, skips prototype and constructor
* comparison in deep equality checks.
* @since v24.9.0
* @default false
*/
skipPrototype?: boolean | undefined;
}
interface Assert extends Pick<typeof assert, AssertMethodNames> {
readonly [kOptions]: AssertOptions & { strict: false };
}
interface AssertStrict extends Pick<typeof strict, AssertMethodNames> {
readonly [kOptions]: AssertOptions & { strict: true };
}
/**
* The `Assert` class allows creating independent assertion instances with custom options.
* @since v24.6.0
*/
var Assert: {
/**
* Creates a new assertion instance. The `diff` option controls the verbosity of diffs in assertion error messages.
*
* ```js
* const { Assert } = require('node:assert');
* const assertInstance = new Assert({ diff: 'full' });
* assertInstance.deepStrictEqual({ a: 1 }, { a: 2 });
* // Shows a full diff in the error message.
* ```
*
* **Important**: When destructuring assertion methods from an `Assert` instance,
* the methods lose their connection to the instance's configuration options (such
* as `diff`, `strict`, and `skipPrototype` settings).
* The destructured methods will fall back to default behavior instead.
*
* ```js
* const myAssert = new Assert({ diff: 'full' });
*
* // This works as expected - uses 'full' diff
* myAssert.strictEqual({ a: 1 }, { b: { c: 1 } });
*
* // This loses the 'full' diff setting - falls back to default 'simple' diff
* const { strictEqual } = myAssert;
* strictEqual({ a: 1 }, { b: { c: 1 } });
* ```
*
* The `skipPrototype` option affects all deep equality methods:
*
* ```js
* class Foo {
* constructor(a) {
* this.a = a;
* }
* }
*
* class Bar {
* constructor(a) {
* this.a = a;
* }
* }
*
* const foo = new Foo(1);
* const bar = new Bar(1);
*
* // Default behavior - fails due to different constructors
* const assert1 = new Assert();
* assert1.deepStrictEqual(foo, bar); // AssertionError
*
* // Skip prototype comparison - passes if properties are equal
* const assert2 = new Assert({ skipPrototype: true });
* assert2.deepStrictEqual(foo, bar); // OK
* ```
*
* When destructured, methods lose access to the instance's `this` context and revert to default assertion behavior
* (diff: 'simple', non-strict mode).
* To maintain custom options when using destructured methods, avoid
* destructuring and call methods directly on the instance.
* @since v24.6.0
*/
new(
options?: AssertOptions & { strict?: true | undefined },
): AssertStrict;
new(
options: AssertOptions,
): Assert;
};
interface AssertionErrorOptions {
/**
* If provided, the error message is set to this value.
*/
message?: string | undefined;
/**
* The `actual` property on the error instance.
*/
actual?: unknown;
/**
* The `expected` property on the error instance.
*/
expected?: unknown;
/**
* The `operator` property on the error instance.
*/
operator?: string | undefined;
/**
* If provided, the generated stack trace omits frames before this function.
*/
stackStartFn?: Function | undefined;
/**
* If set to `'full'`, shows the full diff in assertion errors.
* @default 'simple'
*/
diff?: "simple" | "full" | undefined;
}
/**
* Indicates the failure of an assertion. All errors thrown by the `node:assert` module will be instances of the `AssertionError` class.
*/
class AssertionError extends Error {
constructor(options: AssertionErrorOptions);
/**
* Set to the `actual` argument for methods such as {@link assert.strictEqual()}.
*/
@@ -23,10 +169,6 @@ declare module "assert" {
* Set to the `expected` argument for methods such as {@link assert.strictEqual()}.
*/
expected: unknown;
/**
* Set to the passed in operator value.
*/
operator: string;
/**
* Indicates if the message was auto-generated (`true`) or not.
*/
@@ -35,19 +177,10 @@ declare module "assert" {
* Value is always `ERR_ASSERTION` to show that the error is an assertion error.
*/
code: "ERR_ASSERTION";
constructor(options?: {
/** If provided, the error message is set to this value. */
message?: string | undefined;
/** The `actual` property on the error instance. */
actual?: unknown | undefined;
/** The `expected` property on the error instance. */
expected?: unknown | undefined;
/** The `operator` property on the error instance. */
operator?: string | undefined;
/** If provided, the generated stack trace omits frames before this function. */
// eslint-disable-next-line @typescript-eslint/ban-types
stackStartFn?: Function | undefined;
});
/**
* Set to the passed in operator value.
*/
operator: string;
}
/**
* This feature is deprecated and will be removed in a future version.
@@ -79,7 +212,9 @@ declare module "assert" {
* @return A function that wraps `fn`.
*/
calls(exact?: number): () => void;
calls<Func extends (...args: any[]) => any>(fn?: Func, exact?: number): Func;
calls(fn: undefined, exact?: number): () => void;
calls<Func extends (...args: any[]) => any>(fn: Func, exact?: number): Func;
calls<Func extends (...args: any[]) => any>(fn?: Func, exact?: number): Func | (() => void);
/**
* Example:
*
@@ -226,7 +361,7 @@ declare module "assert" {
expected: unknown,
message?: string | Error,
operator?: string,
// eslint-disable-next-line @typescript-eslint/ban-types
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
stackStartFn?: Function,
): never;
/**
@@ -796,7 +931,7 @@ declare module "assert" {
* check that the promise is rejected.
*
* If `asyncFn` is a function and it throws an error synchronously, `assert.rejects()` will return a rejected `Promise` with that error. If the
* function does not return a promise, `assert.rejects()` will return a rejected `Promise` with an [ERR_INVALID_RETURN_VALUE](https://nodejs.org/docs/latest-v20.x/api/errors.html#err_invalid_return_value)
* function does not return a promise, `assert.rejects()` will return a rejected `Promise` with an [ERR_INVALID_RETURN_VALUE](https://nodejs.org/docs/latest-v24.x/api/errors.html#err_invalid_return_value)
* error. In both cases the error handler is skipped.
*
* Besides the async nature to await the completion behaves identically to {@link throws}.
@@ -866,7 +1001,7 @@ declare module "assert" {
*
* If `asyncFn` is a function and it throws an error synchronously, `assert.doesNotReject()` will return a rejected `Promise` with that error. If
* the function does not return a promise, `assert.doesNotReject()` will return a
* rejected `Promise` with an [ERR_INVALID_RETURN_VALUE](https://nodejs.org/docs/latest-v20.x/api/errors.html#err_invalid_return_value) error. In both cases
* rejected `Promise` with an [ERR_INVALID_RETURN_VALUE](https://nodejs.org/docs/latest-v24.x/api/errors.html#err_invalid_return_value) error. In both cases
* the error handler is skipped.
*
* Using `assert.doesNotReject()` is actually not useful because there is little
@@ -929,7 +1064,7 @@ declare module "assert" {
* If the values do not match, or if the `string` argument is of another type than `string`, an `{@link AssertionError}` is thrown with a `message` property set equal
* to the value of the `message` parameter. If the `message` parameter is
* undefined, a default error message is assigned. If the `message` parameter is an
* instance of an [Error](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) then it will be thrown instead of the `{@link AssertionError}`.
* instance of an [Error](https://nodejs.org/docs/latest-v24.x/api/errors.html#class-error) then it will be thrown instead of the `{@link AssertionError}`.
* @since v13.6.0, v12.16.0
*/
function match(value: string, regExp: RegExp, message?: string | Error): void;
@@ -952,85 +1087,25 @@ declare module "assert" {
* If the values do match, or if the `string` argument is of another type than `string`, an `{@link AssertionError}` is thrown with a `message` property set equal
* to the value of the `message` parameter. If the `message` parameter is
* undefined, a default error message is assigned. If the `message` parameter is an
* instance of an [Error](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) then it will be thrown instead of the `{@link AssertionError}`.
* instance of an [Error](https://nodejs.org/docs/latest-v24.x/api/errors.html#class-error) then it will be thrown instead of the `{@link AssertionError}`.
* @since v13.6.0, v12.16.0
*/
function doesNotMatch(value: string, regExp: RegExp, message?: string | Error): void;
/**
* In strict assertion mode, non-strict methods behave like their corresponding strict methods. For example,
* {@link deepEqual} will behave like {@link deepStrictEqual}.
* Tests for partial deep equality between the `actual` and `expected` parameters.
* "Deep" equality means that the enumerable "own" properties of child objects
* are recursively evaluated also by the following rules. "Partial" equality means
* that only properties that exist on the `expected` parameter are going to be
* compared.
*
* In strict assertion mode, error messages for objects display a diff. In legacy assertion mode, error
* messages for objects display the objects, often truncated.
*
* To use strict assertion mode:
*
* ```js
* import { strict as assert } from 'node:assert';COPY
* import assert from 'node:assert/strict';
* ```
*
* Example error diff:
*
* ```js
* import { strict as assert } from 'node:assert';
*
* assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);
* // AssertionError: Expected inputs to be strictly deep-equal:
* // + actual - expected ... Lines skipped
* //
* // [
* // [
* // ...
* // 2,
* // + 3
* // - '3'
* // ],
* // ...
* // 5
* // ]
* ```
*
* To deactivate the colors, use the `NO_COLOR` or `NODE_DISABLE_COLORS` environment variables. This will also
* deactivate the colors in the REPL. For more on color support in terminal environments, read the tty
* `getColorDepth()` documentation.
*
* @since v15.0.0, v13.9.0, v12.16.2, v9.9.0
* This method always passes the same test cases as `assert.deepStrictEqual()`,
* behaving as a super set of it.
* @since v22.13.0
*/
namespace strict {
type AssertionError = assert.AssertionError;
type AssertPredicate = assert.AssertPredicate;
type CallTrackerCall = assert.CallTrackerCall;
type CallTrackerReportInformation = assert.CallTrackerReportInformation;
}
const strict:
& Omit<
typeof assert,
| "equal"
| "notEqual"
| "deepEqual"
| "notDeepEqual"
| "ok"
| "strictEqual"
| "deepStrictEqual"
| "ifError"
| "strict"
>
& {
(value: unknown, message?: string | Error): asserts value;
equal: typeof strictEqual;
notEqual: typeof notStrictEqual;
deepEqual: typeof deepStrictEqual;
notDeepEqual: typeof notDeepStrictEqual;
// Mapped types and assertion functions are incompatible?
// TS2775: Assertions require every name in the call target
// to be declared with an explicit type annotation.
ok: typeof ok;
strictEqual: typeof strictEqual;
deepStrictEqual: typeof deepStrictEqual;
ifError: typeof ifError;
strict: typeof strict;
};
function partialDeepStrictEqual(actual: unknown, expected: unknown, message?: string | Error): void;
}
namespace assert {
export { strict };
}
export = assert;
}

View File

@@ -1,8 +1,111 @@
/**
* In strict assertion mode, non-strict methods behave like their corresponding
* strict methods. For example, `assert.deepEqual()` will behave like
* `assert.deepStrictEqual()`.
*
* In strict assertion mode, error messages for objects display a diff. In legacy
* assertion mode, error messages for objects display the objects, often truncated.
*
* To use strict assertion mode:
*
* ```js
* import { strict as assert } from 'node:assert';
* ```
*
* ```js
* import assert from 'node:assert/strict';
* ```
*
* Example error diff:
*
* ```js
* import { strict as assert } from 'node:assert';
*
* assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);
* // AssertionError: Expected inputs to be strictly deep-equal:
* // + actual - expected ... Lines skipped
* //
* // [
* // [
* // ...
* // 2,
* // + 3
* // - '3'
* // ],
* // ...
* // 5
* // ]
* ```
*
* To deactivate the colors, use the `NO_COLOR` or `NODE_DISABLE_COLORS`
* environment variables. This will also deactivate the colors in the REPL. For
* more on color support in terminal environments, read the tty
* [`getColorDepth()`](https://nodejs.org/docs/latest-v24.x/api/tty.html#writestreamgetcolordepthenv) documentation.
* @since v15.0.0
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/assert/strict.js)
*/
declare module "assert/strict" {
import { strict } from "node:assert";
import {
Assert,
AssertionError,
AssertionErrorOptions,
AssertOptions,
AssertPredicate,
AssertStrict,
CallTracker,
CallTrackerCall,
CallTrackerReportInformation,
deepStrictEqual,
doesNotMatch,
doesNotReject,
doesNotThrow,
fail,
ifError,
match,
notDeepStrictEqual,
notStrictEqual,
ok,
partialDeepStrictEqual,
rejects,
strictEqual,
throws,
} from "node:assert";
function strict(value: unknown, message?: string | Error): asserts value;
namespace strict {
export {
Assert,
AssertionError,
AssertionErrorOptions,
AssertOptions,
AssertPredicate,
AssertStrict,
CallTracker,
CallTrackerCall,
CallTrackerReportInformation,
deepStrictEqual,
deepStrictEqual as deepEqual,
doesNotMatch,
doesNotReject,
doesNotThrow,
fail,
ifError,
match,
notDeepStrictEqual,
notDeepStrictEqual as notDeepEqual,
notStrictEqual,
notStrictEqual as notEqual,
ok,
partialDeepStrictEqual,
rejects,
strict,
strictEqual,
strictEqual as equal,
throws,
};
}
export = strict;
}
declare module "node:assert/strict" {
import { strict } from "node:assert";
import strict = require("assert/strict");
export = strict;
}

View File

@@ -2,8 +2,8 @@
* We strongly discourage the use of the `async_hooks` API.
* Other APIs that can cover most of its use cases include:
*
* * [`AsyncLocalStorage`](https://nodejs.org/docs/latest-v20.x/api/async_context.html#class-asynclocalstorage) tracks async context
* * [`process.getActiveResourcesInfo()`](https://nodejs.org/docs/latest-v20.x/api/process.html#processgetactiveresourcesinfo) tracks active resources
* * [`AsyncLocalStorage`](https://nodejs.org/docs/latest-v24.x/api/async_context.html#class-asynclocalstorage) tracks async context
* * [`process.getActiveResourcesInfo()`](https://nodejs.org/docs/latest-v24.x/api/process.html#processgetactiveresourcesinfo) tracks active resources
*
* The `node:async_hooks` module provides an API to track asynchronous resources.
* It can be accessed using:
@@ -12,7 +12,7 @@
* import async_hooks from 'node:async_hooks';
* ```
* @experimental
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/async_hooks.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/async_hooks.js)
*/
declare module "async_hooks" {
/**
@@ -44,7 +44,7 @@ declare module "async_hooks" {
* ```
*
* Promise contexts may not get precise `executionAsyncIds` by default.
* See the section on [promise execution tracking](https://nodejs.org/docs/latest-v20.x/api/async_hooks.html#promise-execution-tracking).
* See the section on [promise execution tracking](https://nodejs.org/docs/latest-v24.x/api/async_hooks.html#promise-execution-tracking).
* @since v8.1.0
* @return The `asyncId` of the current execution context. Useful to track when something calls.
*/
@@ -77,7 +77,7 @@ declare module "async_hooks" {
* executionAsyncId,
* executionAsyncResource,
* createHook,
* } from 'async_hooks';
* } from 'node:async_hooks';
* const sym = Symbol('state'); // Private symbol to avoid pollution
*
* createHook({
@@ -117,7 +117,7 @@ declare module "async_hooks" {
* ```
*
* Promise contexts may not get valid `triggerAsyncId`s by default. See
* the section on [promise execution tracking](https://nodejs.org/docs/latest-v20.x/api/async_hooks.html#promise-execution-tracking).
* the section on [promise execution tracking](https://nodejs.org/docs/latest-v24.x/api/async_hooks.html#promise-execution-tracking).
* @return The ID of the resource responsible for calling the callback that is currently being executed.
*/
function triggerAsyncId(): number;
@@ -320,6 +320,16 @@ declare module "async_hooks" {
*/
triggerAsyncId(): number;
}
interface AsyncLocalStorageOptions {
/**
* The default value to be used when no store is provided.
*/
defaultValue?: any;
/**
* A name for the `AsyncLocalStorage` value.
*/
name?: string | undefined;
}
/**
* This class creates stores that stay coherent through asynchronous operations.
*
@@ -358,8 +368,8 @@ declare module "async_hooks" {
* http.get('http://localhost:8080');
* // Prints:
* // 0: start
* // 1: start
* // 0: finish
* // 1: start
* // 1: finish
* ```
*
@@ -369,10 +379,14 @@ declare module "async_hooks" {
* @since v13.10.0, v12.17.0
*/
class AsyncLocalStorage<T> {
/**
* Creates a new instance of `AsyncLocalStorage`. Store is only provided within a
* `run()` call or after an `enterWith()` call.
*/
constructor(options?: AsyncLocalStorageOptions);
/**
* Binds the given function to the current execution context.
* @since v19.8.0
* @experimental
* @param fn The function to bind to the current execution context.
* @return A new function that calls `fn` within the captured execution context.
*/
@@ -403,7 +417,6 @@ declare module "async_hooks" {
* console.log(asyncLocalStorage.run(321, () => foo.get())); // returns 123
* ```
* @since v19.8.0
* @experimental
* @return A new function with the signature `(fn: (...args) : R, ...args) : R`.
*/
static snapshot(): <R, TArgs extends any[]>(fn: (...args: TArgs) => R, ...args: TArgs) => R;
@@ -432,6 +445,11 @@ declare module "async_hooks" {
* @since v13.10.0, v12.17.0
*/
getStore(): T | undefined;
/**
* The name of the `AsyncLocalStorage` instance if provided.
* @since v24.0.0
*/
readonly name: string;
/**
* Runs a function synchronously within a context and returns its
* return value. The store is not accessible outside of the callback function.
@@ -535,6 +553,70 @@ declare module "async_hooks" {
*/
enterWith(store: T): void;
}
/**
* @since v17.2.0, v16.14.0
* @return A map of provider types to the corresponding numeric id.
* This map contains all the event types that might be emitted by the `async_hooks.init()` event.
*/
namespace asyncWrapProviders {
const NONE: number;
const DIRHANDLE: number;
const DNSCHANNEL: number;
const ELDHISTOGRAM: number;
const FILEHANDLE: number;
const FILEHANDLECLOSEREQ: number;
const FIXEDSIZEBLOBCOPY: number;
const FSEVENTWRAP: number;
const FSREQCALLBACK: number;
const FSREQPROMISE: number;
const GETADDRINFOREQWRAP: number;
const GETNAMEINFOREQWRAP: number;
const HEAPSNAPSHOT: number;
const HTTP2SESSION: number;
const HTTP2STREAM: number;
const HTTP2PING: number;
const HTTP2SETTINGS: number;
const HTTPINCOMINGMESSAGE: number;
const HTTPCLIENTREQUEST: number;
const JSSTREAM: number;
const JSUDPWRAP: number;
const MESSAGEPORT: number;
const PIPECONNECTWRAP: number;
const PIPESERVERWRAP: number;
const PIPEWRAP: number;
const PROCESSWRAP: number;
const PROMISE: number;
const QUERYWRAP: number;
const SHUTDOWNWRAP: number;
const SIGNALWRAP: number;
const STATWATCHER: number;
const STREAMPIPE: number;
const TCPCONNECTWRAP: number;
const TCPSERVERWRAP: number;
const TCPWRAP: number;
const TTYWRAP: number;
const UDPSENDWRAP: number;
const UDPWRAP: number;
const SIGINTWATCHDOG: number;
const WORKER: number;
const WORKERHEAPSNAPSHOT: number;
const WRITEWRAP: number;
const ZLIB: number;
const CHECKPRIMEREQUEST: number;
const PBKDF2REQUEST: number;
const KEYPAIRGENREQUEST: number;
const KEYGENREQUEST: number;
const KEYEXPORTREQUEST: number;
const CIPHERREQUEST: number;
const DERIVEBITSREQUEST: number;
const HASHREQUEST: number;
const RANDOMBYTESREQUEST: number;
const RANDOMPRIMEREQUEST: number;
const SCRYPTREQUEST: number;
const SIGNREQUEST: number;
const TLSWRAP: number;
const VERIFYREQUEST: number;
}
}
declare module "node:async_hooks" {
export * from "async_hooks";

View File

@@ -1,3 +1,8 @@
// If lib.dom.d.ts or lib.webworker.d.ts is loaded, then use the global types.
// Otherwise, use the types from node.
type _Blob = typeof globalThis extends { onmessage: any; Blob: any } ? {} : import("buffer").Blob;
type _File = typeof globalThis extends { onmessage: any; File: any } ? {} : import("buffer").File;
/**
* `Buffer` objects are used to represent a fixed-length sequence of bytes. Many
* Node.js APIs support `Buffer`s.
@@ -41,7 +46,7 @@
* // Creates a Buffer containing the Latin-1 bytes [0x74, 0xe9, 0x73, 0x74].
* const buf7 = Buffer.from('tést', 'latin1');
* ```
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/buffer.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/buffer.js)
*/
declare module "buffer" {
import { BinaryLike } from "node:crypto";
@@ -54,7 +59,7 @@ declare module "buffer" {
* @since v19.4.0, v18.14.0
* @param input The input to validate.
*/
export function isUtf8(input: Buffer | ArrayBuffer | NodeJS.TypedArray): boolean;
export function isUtf8(input: ArrayBuffer | NodeJS.TypedArray): boolean;
/**
* This function returns `true` if `input` contains only valid ASCII-encoded data,
* including the case in which `input` is empty.
@@ -63,8 +68,8 @@ declare module "buffer" {
* @since v19.6.0, v18.15.0
* @param input The input to validate.
*/
export function isAscii(input: Buffer | ArrayBuffer | NodeJS.TypedArray): boolean;
export const INSPECT_MAX_BYTES: number;
export function isAscii(input: ArrayBuffer | NodeJS.TypedArray): boolean;
export let INSPECT_MAX_BYTES: number;
export const kMaxLength: number;
export const kStringMaxLength: number;
export const constants: {
@@ -108,28 +113,26 @@ declare module "buffer" {
* @param fromEnc The current encoding.
* @param toEnc To target encoding.
*/
export function transcode(source: Uint8Array, fromEnc: TranscodeEncoding, toEnc: TranscodeEncoding): Buffer;
export const SlowBuffer: {
/** @deprecated since v6.0.0, use `Buffer.allocUnsafeSlow()` */
new(size: number): Buffer;
prototype: Buffer;
};
export function transcode(
source: Uint8Array,
fromEnc: TranscodeEncoding,
toEnc: TranscodeEncoding,
): NonSharedBuffer;
/**
* Resolves a `'blob:nodedata:...'` an associated `Blob` object registered using
* a prior call to `URL.createObjectURL()`.
* @since v16.7.0
* @experimental
* @param id A `'blob:nodedata:...` URL string returned by a prior call to `URL.createObjectURL()`.
*/
export function resolveObjectURL(id: string): Blob | undefined;
export { Buffer };
export { type AllowSharedBuffer, Buffer, type NonSharedBuffer };
/**
* @experimental
*/
export interface BlobOptions {
/**
* One of either `'transparent'` or `'native'`. When set to `'native'`, line endings in string source parts
* will be converted to the platform native line-ending as specified by `require('node:os').EOL`.
* will be converted to the platform native line-ending as specified by `import { EOL } from 'node:os'`.
*/
endings?: "transparent" | "native";
/**
@@ -140,7 +143,7 @@ declare module "buffer" {
type?: string | undefined;
}
/**
* A [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) encapsulates immutable, raw data that can be safely shared across
* A `Blob` encapsulates immutable, raw data that can be safely shared across
* multiple worker threads.
* @since v15.7.0, v14.18.0
*/
@@ -170,6 +173,17 @@ declare module "buffer" {
* @since v15.7.0, v14.18.0
*/
arrayBuffer(): Promise<ArrayBuffer>;
/**
* The `blob.bytes()` method returns the byte of the `Blob` object as a `Promise<Uint8Array>`.
*
* ```js
* const blob = new Blob(['hello']);
* blob.bytes().then((bytes) => {
* console.log(bytes); // Outputs: Uint8Array(5) [ 104, 101, 108, 108, 111 ]
* });
* ```
*/
bytes(): Promise<Uint8Array>;
/**
* Creates and returns a new `Blob` containing a subset of this `Blob` objects
* data. The original `Blob` is not altered.
@@ -194,7 +208,7 @@ declare module "buffer" {
export interface FileOptions {
/**
* One of either `'transparent'` or `'native'`. When set to `'native'`, line endings in string source parts will be
* converted to the platform native line-ending as specified by `require('node:os').EOL`.
* converted to the platform native line-ending as specified by `import { EOL } from 'node:os'`.
*/
endings?: "native" | "transparent";
/** The File content-type. */
@@ -221,10 +235,10 @@ declare module "buffer" {
}
export import atob = globalThis.atob;
export import btoa = globalThis.btoa;
import { Blob as NodeBlob } from "buffer";
// This conditional type will be the existing global Blob in a browser, or
// the copy below in a Node environment.
type __Blob = typeof globalThis extends { onmessage: any; Blob: any } ? {} : NodeBlob;
export type WithImplicitCoercion<T> =
| T
| { valueOf(): T }
| (T extends string ? { [Symbol.toPrimitive](hint: "string"): T } : never);
global {
namespace NodeJS {
export { BufferEncoding };
@@ -243,111 +257,15 @@ declare module "buffer" {
| "latin1"
| "binary"
| "hex";
type WithImplicitCoercion<T> =
| T
| {
valueOf(): T;
};
/**
* Raw data is stored in instances of the Buffer class.
* A Buffer is similar to an array of integers but corresponds to a raw memory allocation outside the V8 heap. A Buffer cannot be resized.
* Valid string encodings: 'ascii'|'utf8'|'utf16le'|'ucs2'(alias of 'utf16le')|'base64'|'base64url'|'binary'(deprecated)|'hex'
*/
interface BufferConstructor {
/**
* Allocates a new buffer containing the given {str}.
*
* @param str String to store in buffer.
* @param encoding encoding to use, optional. Default is 'utf8'
* @deprecated since v10.0.0 - Use `Buffer.from(string[, encoding])` instead.
*/
new(str: string, encoding?: BufferEncoding): Buffer;
/**
* Allocates a new buffer of {size} octets.
*
* @param size count of octets to allocate.
* @deprecated since v10.0.0 - Use `Buffer.alloc()` instead (also see `Buffer.allocUnsafe()`).
*/
new(size: number): Buffer;
/**
* Allocates a new buffer containing the given {array} of octets.
*
* @param array The octets to store.
* @deprecated since v10.0.0 - Use `Buffer.from(array)` instead.
*/
new(array: Uint8Array): Buffer;
/**
* Produces a Buffer backed by the same allocated memory as
* the given {ArrayBuffer}/{SharedArrayBuffer}.
*
* @param arrayBuffer The ArrayBuffer with which to share memory.
* @deprecated since v10.0.0 - Use `Buffer.from(arrayBuffer[, byteOffset[, length]])` instead.
*/
new(arrayBuffer: ArrayBuffer | SharedArrayBuffer): Buffer;
/**
* Allocates a new buffer containing the given {array} of octets.
*
* @param array The octets to store.
* @deprecated since v10.0.0 - Use `Buffer.from(array)` instead.
*/
new(array: readonly any[]): Buffer;
/**
* Copies the passed {buffer} data onto a new {Buffer} instance.
*
* @param buffer The buffer to copy.
* @deprecated since v10.0.0 - Use `Buffer.from(buffer)` instead.
*/
new(buffer: Buffer): Buffer;
/**
* Allocates a new `Buffer` using an `array` of bytes in the range `0` `255`.
* Array entries outside that range will be truncated to fit into it.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* // Creates a new Buffer containing the UTF-8 bytes of the string 'buffer'.
* const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]);
* ```
*
* If `array` is an `Array`\-like object (that is, one with a `length` property of
* type `number`), it is treated as if it is an array, unless it is a `Buffer` or
* a `Uint8Array`. This means all other `TypedArray` variants get treated as an `Array`. To create a `Buffer` from the bytes backing a `TypedArray`, use `Buffer.copyBytesFrom()`.
*
* A `TypeError` will be thrown if `array` is not an `Array` or another type
* appropriate for `Buffer.from()` variants.
*
* `Buffer.from(array)` and `Buffer.from(string)` may also use the internal `Buffer` pool like `Buffer.allocUnsafe()` does.
* @since v5.10.0
*/
from(
arrayBuffer: WithImplicitCoercion<ArrayBuffer | SharedArrayBuffer>,
byteOffset?: number,
length?: number,
): Buffer;
/**
* Creates a new Buffer using the passed {data}
* @param data data to create a new Buffer
*/
from(data: Uint8Array | readonly number[]): Buffer;
from(data: WithImplicitCoercion<Uint8Array | readonly number[] | string>): Buffer;
/**
* Creates a new Buffer containing the given JavaScript string {str}.
* If provided, the {encoding} parameter identifies the character encoding.
* If not provided, {encoding} defaults to 'utf8'.
*/
from(
str:
| WithImplicitCoercion<string>
| {
[Symbol.toPrimitive](hint: "string"): string;
},
encoding?: BufferEncoding,
): Buffer;
/**
* Creates a new Buffer using the passed {data}
* @param values to create a new Buffer
*/
of(...items: number[]): Buffer;
// see buffer.buffer.d.ts for implementation specific to TypeScript 5.7 and later
// see ts5.6/buffer.buffer.d.ts for implementation specific to TypeScript 5.6 and earlier
/**
* Returns `true` if `obj` is a `Buffer`, `false` otherwise.
*
@@ -416,65 +334,9 @@ declare module "buffer" {
* @return The number of bytes contained within `string`.
*/
byteLength(
string: string | Buffer | NodeJS.ArrayBufferView | ArrayBuffer | SharedArrayBuffer,
string: string | NodeJS.ArrayBufferView | ArrayBufferLike,
encoding?: BufferEncoding,
): number;
/**
* Returns a new `Buffer` which is the result of concatenating all the `Buffer` instances in the `list` together.
*
* If the list has no items, or if the `totalLength` is 0, then a new zero-length `Buffer` is returned.
*
* If `totalLength` is not provided, it is calculated from the `Buffer` instances
* in `list` by adding their lengths.
*
* If `totalLength` is provided, it is coerced to an unsigned integer. If the
* combined length of the `Buffer`s in `list` exceeds `totalLength`, the result is
* truncated to `totalLength`.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* // Create a single `Buffer` from a list of three `Buffer` instances.
*
* const buf1 = Buffer.alloc(10);
* const buf2 = Buffer.alloc(14);
* const buf3 = Buffer.alloc(18);
* const totalLength = buf1.length + buf2.length + buf3.length;
*
* console.log(totalLength);
* // Prints: 42
*
* const bufA = Buffer.concat([buf1, buf2, buf3], totalLength);
*
* console.log(bufA);
* // Prints: <Buffer 00 00 00 00 ...>
* console.log(bufA.length);
* // Prints: 42
* ```
*
* `Buffer.concat()` may also use the internal `Buffer` pool like `Buffer.allocUnsafe()` does.
* @since v0.7.11
* @param list List of `Buffer` or {@link Uint8Array} instances to concatenate.
* @param totalLength Total length of the `Buffer` instances in `list` when concatenated.
*/
concat(list: readonly Uint8Array[], totalLength?: number): Buffer;
/**
* Copies the underlying memory of `view` into a new `Buffer`.
*
* ```js
* const u16 = new Uint16Array([0, 0xffff]);
* const buf = Buffer.copyBytesFrom(u16, 1, 1);
* u16[1] = 0;
* console.log(buf.length); // 2
* console.log(buf[0]); // 255
* console.log(buf[1]); // 255
* ```
* @since v19.8.0
* @param view The {TypedArray} to copy.
* @param [offset=0] The starting offset within `view`.
* @param [length=view.length - offset] The number of elements from `view` to copy.
*/
copyBytesFrom(view: NodeJS.TypedArray, offset?: number, length?: number): Buffer;
/**
* Compares `buf1` to `buf2`, typically for the purpose of sorting arrays of `Buffer` instances. This is equivalent to calling `buf1.compare(buf2)`.
*
@@ -493,135 +355,6 @@ declare module "buffer" {
* @return Either `-1`, `0`, or `1`, depending on the result of the comparison. See `compare` for details.
*/
compare(buf1: Uint8Array, buf2: Uint8Array): -1 | 0 | 1;
/**
* Allocates a new `Buffer` of `size` bytes. If `fill` is `undefined`, the`Buffer` will be zero-filled.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.alloc(5);
*
* console.log(buf);
* // Prints: <Buffer 00 00 00 00 00>
* ```
*
* If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown.
*
* If `fill` is specified, the allocated `Buffer` will be initialized by calling `buf.fill(fill)`.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.alloc(5, 'a');
*
* console.log(buf);
* // Prints: <Buffer 61 61 61 61 61>
* ```
*
* If both `fill` and `encoding` are specified, the allocated `Buffer` will be
* initialized by calling `buf.fill(fill, encoding)`.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64');
*
* console.log(buf);
* // Prints: <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>
* ```
*
* Calling `Buffer.alloc()` can be measurably slower than the alternative `Buffer.allocUnsafe()` but ensures that the newly created `Buffer` instance
* contents will never contain sensitive data from previous allocations, including
* data that might not have been allocated for `Buffer`s.
*
* A `TypeError` will be thrown if `size` is not a number.
* @since v5.10.0
* @param size The desired length of the new `Buffer`.
* @param [fill=0] A value to pre-fill the new `Buffer` with.
* @param [encoding='utf8'] If `fill` is a string, this is its encoding.
*/
alloc(size: number, fill?: string | Uint8Array | number, encoding?: BufferEncoding): Buffer;
/**
* Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown.
*
* The underlying memory for `Buffer` instances created in this way is _not_
* _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `Buffer.alloc()` instead to initialize`Buffer` instances with zeroes.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.allocUnsafe(10);
*
* console.log(buf);
* // Prints (contents may vary): <Buffer a0 8b 28 3f 01 00 00 00 50 32>
*
* buf.fill(0);
*
* console.log(buf);
* // Prints: <Buffer 00 00 00 00 00 00 00 00 00 00>
* ```
*
* A `TypeError` will be thrown if `size` is not a number.
*
* The `Buffer` module pre-allocates an internal `Buffer` instance of
* size `Buffer.poolSize` that is used as a pool for the fast allocation of new `Buffer` instances created using `Buffer.allocUnsafe()`, `Buffer.from(array)`,
* and `Buffer.concat()` only when `size` is less than `Buffer.poolSize >>> 1` (floor of `Buffer.poolSize` divided by two).
*
* Use of this pre-allocated internal memory pool is a key difference between
* calling `Buffer.alloc(size, fill)` vs. `Buffer.allocUnsafe(size).fill(fill)`.
* Specifically, `Buffer.alloc(size, fill)` will _never_ use the internal `Buffer`pool, while `Buffer.allocUnsafe(size).fill(fill)`_will_ use the internal`Buffer` pool if `size` is less
* than or equal to half `Buffer.poolSize`. The
* difference is subtle but can be important when an application requires the
* additional performance that `Buffer.allocUnsafe()` provides.
* @since v5.10.0
* @param size The desired length of the new `Buffer`.
*/
allocUnsafe(size: number): Buffer;
/**
* Allocates a new `Buffer` of `size` bytes. If `size` is larger than {@link constants.MAX_LENGTH} or smaller than 0, `ERR_OUT_OF_RANGE` is thrown. A zero-length `Buffer` is created if
* `size` is 0.
*
* The underlying memory for `Buffer` instances created in this way is _not_
* _initialized_. The contents of the newly created `Buffer` are unknown and _may contain sensitive data_. Use `buf.fill(0)` to initialize
* such `Buffer` instances with zeroes.
*
* When using `Buffer.allocUnsafe()` to allocate new `Buffer` instances,
* allocations under 4 KiB are sliced from a single pre-allocated `Buffer`. This
* allows applications to avoid the garbage collection overhead of creating many
* individually allocated `Buffer` instances. This approach improves both
* performance and memory usage by eliminating the need to track and clean up as
* many individual `ArrayBuffer` objects.
*
* However, in the case where a developer may need to retain a small chunk of
* memory from a pool for an indeterminate amount of time, it may be appropriate
* to create an un-pooled `Buffer` instance using `Buffer.allocUnsafeSlow()` and
* then copying out the relevant bits.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* // Need to keep around a few small chunks of memory.
* const store = [];
*
* socket.on('readable', () => {
* let data;
* while (null !== (data = readable.read())) {
* // Allocate for retained data.
* const sb = Buffer.allocUnsafeSlow(10);
*
* // Copy the data into the new allocation.
* data.copy(sb, 0, 0, 10);
*
* store.push(sb);
* }
* });
* ```
*
* A `TypeError` will be thrown if `size` is not a number.
* @since v5.12.0
* @param size The desired length of the new `Buffer`.
*/
allocUnsafeSlow(size: number): Buffer;
/**
* This is the size (in bytes) of pre-allocated internal `Buffer` instances used
* for pooling. This value may be modified.
@@ -629,7 +362,10 @@ declare module "buffer" {
*/
poolSize: number;
}
interface Buffer extends Uint8Array {
interface Buffer {
// see buffer.buffer.d.ts for implementation specific to TypeScript 5.7 and later
// see ts5.6/buffer.buffer.d.ts for implementation specific to TypeScript 5.6 and earlier
/**
* Writes `string` to `buf` at `offset` according to the character encoding in`encoding`. The `length` parameter is the number of bytes to write. If `buf` did
* not contain enough space to fit the entire string, only part of `string` will be
@@ -866,100 +602,6 @@ declare module "buffer" {
* @return The number of bytes copied.
*/
copy(target: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number): number;
/**
* Returns a new `Buffer` that references the same memory as the original, but
* offset and cropped by the `start` and `end` indices.
*
* This method is not compatible with the `Uint8Array.prototype.slice()`,
* which is a superclass of `Buffer`. To copy the slice, use`Uint8Array.prototype.slice()`.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.from('buffer');
*
* const copiedBuf = Uint8Array.prototype.slice.call(buf);
* copiedBuf[0]++;
* console.log(copiedBuf.toString());
* // Prints: cuffer
*
* console.log(buf.toString());
* // Prints: buffer
*
* // With buf.slice(), the original buffer is modified.
* const notReallyCopiedBuf = buf.slice();
* notReallyCopiedBuf[0]++;
* console.log(notReallyCopiedBuf.toString());
* // Prints: cuffer
* console.log(buf.toString());
* // Also prints: cuffer (!)
* ```
* @since v0.3.0
* @deprecated Use `subarray` instead.
* @param [start=0] Where the new `Buffer` will start.
* @param [end=buf.length] Where the new `Buffer` will end (not inclusive).
*/
slice(start?: number, end?: number): Buffer;
/**
* Returns a new `Buffer` that references the same memory as the original, but
* offset and cropped by the `start` and `end` indices.
*
* Specifying `end` greater than `buf.length` will return the same result as
* that of `end` equal to `buf.length`.
*
* This method is inherited from [`TypedArray.prototype.subarray()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray).
*
* Modifying the new `Buffer` slice will modify the memory in the original `Buffer`because the allocated memory of the two objects overlap.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* // Create a `Buffer` with the ASCII alphabet, take a slice, and modify one byte
* // from the original `Buffer`.
*
* const buf1 = Buffer.allocUnsafe(26);
*
* for (let i = 0; i < 26; i++) {
* // 97 is the decimal ASCII value for 'a'.
* buf1[i] = i + 97;
* }
*
* const buf2 = buf1.subarray(0, 3);
*
* console.log(buf2.toString('ascii', 0, buf2.length));
* // Prints: abc
*
* buf1[0] = 33;
*
* console.log(buf2.toString('ascii', 0, buf2.length));
* // Prints: !bc
* ```
*
* Specifying negative indexes causes the slice to be generated relative to the
* end of `buf` rather than the beginning.
*
* ```js
* import { Buffer } from 'node:buffer';
*
* const buf = Buffer.from('buffer');
*
* console.log(buf.subarray(-6, -1).toString());
* // Prints: buffe
* // (Equivalent to buf.subarray(0, 5).)
*
* console.log(buf.subarray(-6, -2).toString());
* // Prints: buff
* // (Equivalent to buf.subarray(0, 4).)
*
* console.log(buf.subarray(-5, -2).toString());
* // Prints: uff
* // (Equivalent to buf.subarray(1, 4).)
* ```
* @since v3.0.0
* @param [start=0] Where the new `Buffer` will start.
* @param [end=buf.length] Where the new `Buffer` will end (not inclusive).
*/
subarray(start?: number, end?: number): Buffer;
/**
* Writes `value` to `buf` at the specified `offset` as big-endian.
*
@@ -1617,7 +1259,7 @@ declare module "buffer" {
* @since v5.10.0
* @return A reference to `buf`.
*/
swap16(): Buffer;
swap16(): this;
/**
* Interprets `buf` as an array of unsigned 32-bit integers and swaps the
* byte order _in-place_. Throws `ERR_INVALID_BUFFER_SIZE` if `buf.length` is not a multiple of 4.
@@ -1643,7 +1285,7 @@ declare module "buffer" {
* @since v5.10.0
* @return A reference to `buf`.
*/
swap32(): Buffer;
swap32(): this;
/**
* Interprets `buf` as an array of 64-bit numbers and swaps byte order _in-place_.
* Throws `ERR_INVALID_BUFFER_SIZE` if `buf.length` is not a multiple of 8.
@@ -1669,7 +1311,7 @@ declare module "buffer" {
* @since v6.3.0
* @return A reference to `buf`.
*/
swap64(): Buffer;
swap64(): this;
/**
* Writes `value` to `buf` at the specified `offset`. `value` must be a
* valid unsigned 8-bit integer. Behavior is undefined when `value` is anything
@@ -2063,6 +1705,8 @@ declare module "buffer" {
* @return A reference to `buf`.
*/
fill(value: string | Uint8Array | number, offset?: number, end?: number, encoding?: BufferEncoding): this;
fill(value: string | Uint8Array | number, offset: number, encoding: BufferEncoding): this;
fill(value: string | Uint8Array | number, encoding: BufferEncoding): this;
/**
* If `value` is:
*
@@ -2132,6 +1776,7 @@ declare module "buffer" {
* @return The index of the first occurrence of `value` in `buf`, or `-1` if `buf` does not contain `value`.
*/
indexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number;
indexOf(value: string | number | Uint8Array, encoding: BufferEncoding): number;
/**
* Identical to `buf.indexOf()`, except the last occurrence of `value` is found
* rather than the first occurrence.
@@ -2200,6 +1845,7 @@ declare module "buffer" {
* @return The index of the last occurrence of `value` in `buf`, or `-1` if `buf` does not contain `value`.
*/
lastIndexOf(value: string | number | Uint8Array, byteOffset?: number, encoding?: BufferEncoding): number;
lastIndexOf(value: string | number | Uint8Array, encoding: BufferEncoding): number;
/**
* Equivalent to `buf.indexOf() !== -1`.
*
@@ -2230,6 +1876,7 @@ declare module "buffer" {
* @return `true` if `value` was found in `buf`, `false` otherwise.
*/
includes(value: string | number | Buffer, byteOffset?: number, encoding?: BufferEncoding): boolean;
includes(value: string | number | Buffer, encoding: BufferEncoding): boolean;
}
var Buffer: BufferConstructor;
/**
@@ -2264,17 +1911,22 @@ declare module "buffer" {
* @param data An ASCII (Latin1) string.
*/
function btoa(data: string): string;
interface Blob extends __Blob {}
interface Blob extends _Blob {}
/**
* `Blob` class is a global reference for `require('node:buffer').Blob`
* `Blob` class is a global reference for `import { Blob } from 'node:buffer'`
* https://nodejs.org/api/buffer.html#class-blob
* @since v18.0.0
*/
var Blob: typeof globalThis extends {
onmessage: any;
Blob: infer T;
} ? T
: typeof NodeBlob;
var Blob: typeof globalThis extends { onmessage: any; Blob: infer T } ? T
: typeof import("buffer").Blob;
interface File extends _File {}
/**
* `File` class is a global reference for `import { File } from 'node:buffer'`
* https://nodejs.org/api/buffer.html#class-file
* @since v20.0.0
*/
var File: typeof globalThis extends { onmessage: any; File: infer T } ? T
: typeof import("buffer").File;
}
}
declare module "node:buffer" {

View File

@@ -4,7 +4,7 @@
* is primarily provided by the {@link spawn} function:
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
* const ls = spawn('ls', ['-lh', '/usr']);
*
* ls.stdout.on('data', (data) => {
@@ -24,7 +24,7 @@
* the parent Node.js process and the spawned subprocess. These pipes have
* limited (and platform-specific) capacity. If the subprocess writes to
* stdout in excess of that limit without the output being captured, the
* subprocess blocks waiting for the pipe buffer to accept more data. This is
* subprocess blocks, waiting for the pipe buffer to accept more data. This is
* identical to the behavior of pipes in the shell. Use the `{ stdio: 'ignore' }` option if the output will not be consumed.
*
* The command lookup is performed using the `options.env.PATH` environment
@@ -63,14 +63,14 @@
* For certain use cases, such as automating shell scripts, the `synchronous counterparts` may be more convenient. In many cases, however,
* the synchronous methods can have significant impact on performance due to
* stalling the event loop while spawned processes complete.
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/child_process.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/child_process.js)
*/
declare module "child_process" {
import { ObjectEncodingOptions } from "node:fs";
import { NonSharedBuffer } from "node:buffer";
import { Abortable, EventEmitter } from "node:events";
import * as dgram from "node:dgram";
import * as net from "node:net";
import { Pipe, Readable, Stream, Writable } from "node:stream";
import { Readable, Stream, Writable } from "node:stream";
import { URL } from "node:url";
type Serializable = string | object | number | boolean | bigint;
type SendHandle = net.Socket | net.Server | dgram.Socket | undefined;
@@ -109,7 +109,7 @@ declare module "child_process" {
* refer to the same value.
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
*
* const subprocess = spawn('ls');
*
@@ -140,7 +140,7 @@ declare module "child_process" {
* no IPC channel exists, this property is `undefined`.
* @since v7.1.0
*/
readonly channel?: Pipe | null | undefined;
readonly channel?: Control | null;
/**
* A sparse array of pipes to the child process, corresponding with positions in
* the `stdio` option passed to {@link spawn} that have been set
@@ -152,9 +152,9 @@ declare module "child_process" {
* in the array are `null`.
*
* ```js
* const assert = require('node:assert');
* const fs = require('node:fs');
* const child_process = require('node:child_process');
* import assert from 'node:assert';
* import fs from 'node:fs';
* import child_process from 'node:child_process';
*
* const subprocess = child_process.spawn('ls', {
* stdio: [
@@ -202,7 +202,7 @@ declare module "child_process" {
* emitted.
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
* const grep = spawn('grep', ['ssh']);
*
* console.log(`Spawned child pid: ${grep.pid}`);
@@ -249,7 +249,7 @@ declare module "child_process" {
* returns `true` if [`kill(2)`](http://man7.org/linux/man-pages/man2/kill.2.html) succeeds, and `false` otherwise.
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
* const grep = spawn('grep', ['ssh']);
*
* grep.on('close', (code, signal) => {
@@ -282,7 +282,7 @@ declare module "child_process" {
*
* ```js
* 'use strict';
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
*
* const subprocess = spawn(
* 'sh',
@@ -320,7 +320,7 @@ declare module "child_process" {
* For example, in the parent script:
*
* ```js
* const cp = require('node:child_process');
* import cp from 'node:child_process';
* const n = cp.fork(`${__dirname}/sub.js`);
*
* n.on('message', (m) => {
@@ -374,10 +374,12 @@ declare module "child_process" {
* a TCP server object to the child process as illustrated in the example below:
*
* ```js
* const subprocess = require('node:child_process').fork('subprocess.js');
* import { createServer } from 'node:net';
* import { fork } from 'node:child_process';
* const subprocess = fork('subprocess.js');
*
* // Open up the server object and send the handle.
* const server = require('node:net').createServer();
* const server = createServer();
* server.on('connection', (socket) => {
* socket.end('handled by parent');
* });
@@ -412,13 +414,14 @@ declare module "child_process" {
* handle connections with "normal" or "special" priority:
*
* ```js
* const { fork } = require('node:child_process');
* import { createServer } from 'node:net';
* import { fork } from 'node:child_process';
* const normal = fork('subprocess.js', ['normal']);
* const special = fork('subprocess.js', ['special']);
*
* // Open up the server and send sockets to child. Use pauseOnConnect to prevent
* // the sockets from being read before they are sent to the child process.
* const server = require('node:net').createServer({ pauseOnConnect: true });
* const server = createServer({ pauseOnConnect: true });
* server.on('connection', (socket) => {
*
* // If this is special priority...
@@ -455,7 +458,7 @@ declare module "child_process" {
* as the connection may have been closed during the time it takes to send the
* connection to the child.
* @since v0.5.9
* @param sendHandle `undefined`, or a [`net.Socket`](https://nodejs.org/docs/latest-v20.x/api/net.html#class-netsocket), [`net.Server`](https://nodejs.org/docs/latest-v20.x/api/net.html#class-netserver), or [`dgram.Socket`](https://nodejs.org/docs/latest-v20.x/api/dgram.html#class-dgramsocket) object.
* @param sendHandle `undefined`, or a [`net.Socket`](https://nodejs.org/docs/latest-v24.x/api/net.html#class-netsocket), [`net.Server`](https://nodejs.org/docs/latest-v24.x/api/net.html#class-netserver), or [`dgram.Socket`](https://nodejs.org/docs/latest-v24.x/api/dgram.html#class-dgramsocket) object.
* @param options The `options` argument, if present, is an object used to parameterize the sending of certain types of handles. `options` supports the following properties:
*/
send(message: Serializable, callback?: (error: Error | null) => void): boolean;
@@ -490,7 +493,7 @@ declare module "child_process" {
* the child and the parent.
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
*
* const subprocess = spawn(process.argv[0], ['child_program.js'], {
* detached: true,
@@ -508,7 +511,7 @@ declare module "child_process" {
* to wait for the child to exit before exiting itself.
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
*
* const subprocess = spawn(process.argv[0], ['child_program.js'], {
* detached: true,
@@ -610,6 +613,10 @@ declare module "child_process" {
Readable | Writable | null | undefined, // extra, no modification
];
}
interface Control extends EventEmitter {
ref(): void;
unref(): void;
}
interface MessageOptions {
keepOpen?: boolean | undefined;
}
@@ -711,7 +718,7 @@ declare module "child_process" {
* exit code:
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
* const ls = spawn('ls', ['-lh', '/usr']);
*
* ls.stdout.on('data', (data) => {
@@ -730,7 +737,7 @@ declare module "child_process" {
* Example: A very elaborate way to run `ps ax | grep ssh`
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
* const ps = spawn('ps', ['ax']);
* const grep = spawn('grep', ['ssh']);
*
@@ -767,7 +774,7 @@ declare module "child_process" {
* Example of checking for failed `spawn`:
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
* const subprocess = spawn('bad_command');
*
* subprocess.on('error', (err) => {
@@ -785,7 +792,7 @@ declare module "child_process" {
* the error passed to the callback will be an `AbortError`:
*
* ```js
* const { spawn } = require('node:child_process');
* import { spawn } from 'node:child_process';
* const controller = new AbortController();
* const { signal } = controller;
* const grep = spawn('grep', ['ssh'], { signal });
@@ -884,18 +891,20 @@ declare module "child_process" {
signal?: AbortSignal | undefined;
maxBuffer?: number | undefined;
killSignal?: NodeJS.Signals | number | undefined;
encoding?: string | null | undefined;
}
interface ExecOptionsWithStringEncoding extends ExecOptions {
encoding: BufferEncoding;
encoding?: BufferEncoding | undefined;
}
interface ExecOptionsWithBufferEncoding extends ExecOptions {
encoding: BufferEncoding | null; // specify `null`.
encoding: "buffer" | null; // specify `null`.
}
// TODO: Just Plain Wrong™ (see also nodejs/node#57392)
interface ExecException extends Error {
cmd?: string | undefined;
killed?: boolean | undefined;
code?: number | undefined;
signal?: NodeJS.Signals | undefined;
cmd?: string;
killed?: boolean;
code?: number;
signal?: NodeJS.Signals;
stdout?: string;
stderr?: string;
}
@@ -906,7 +915,7 @@ declare module "child_process" {
* need to be dealt with accordingly:
*
* ```js
* const { exec } = require('node:child_process');
* import { exec } from 'node:child_process';
*
* exec('"/path/to/test file/test.sh" arg1 arg2');
* // Double quotes are used so that the space in the path is not interpreted as
@@ -932,7 +941,7 @@ declare module "child_process" {
* encoding, `Buffer` objects will be passed to the callback instead.
*
* ```js
* const { exec } = require('node:child_process');
* import { exec } from 'node:child_process';
* exec('cat *.js missing_file | wc -l', (error, stdout, stderr) => {
* if (error) {
* console.error(`exec error: ${error}`);
@@ -957,8 +966,9 @@ declare module "child_process" {
* callback, but with two additional properties `stdout` and `stderr`.
*
* ```js
* const util = require('node:util');
* const exec = util.promisify(require('node:child_process').exec);
* import util from 'node:util';
* import child_process from 'node:child_process';
* const exec = util.promisify(child_process.exec);
*
* async function lsExample() {
* const { stdout, stderr } = await exec('ls');
@@ -972,7 +982,7 @@ declare module "child_process" {
* the error passed to the callback will be an `AbortError`:
*
* ```js
* const { exec } = require('node:child_process');
* import { exec } from 'node:child_process';
* const controller = new AbortController();
* const { signal } = controller;
* const child = exec('grep ssh', { signal }, (error) => {
@@ -991,39 +1001,24 @@ declare module "child_process" {
// `options` with `"buffer"` or `null` for `encoding` means stdout/stderr are definitely `Buffer`.
function exec(
command: string,
options: {
encoding: "buffer" | null;
} & ExecOptions,
callback?: (error: ExecException | null, stdout: Buffer, stderr: Buffer) => void,
options: ExecOptionsWithBufferEncoding,
callback?: (error: ExecException | null, stdout: NonSharedBuffer, stderr: NonSharedBuffer) => void,
): ChildProcess;
// `options` with well known `encoding` means stdout/stderr are definitely `string`.
// `options` with well-known or absent `encoding` means stdout/stderr are definitely `string`.
function exec(
command: string,
options: {
encoding: BufferEncoding;
} & ExecOptions,
callback?: (error: ExecException | null, stdout: string, stderr: string) => void,
): ChildProcess;
// `options` with an `encoding` whose type is `string` means stdout/stderr could either be `Buffer` or `string`.
// There is no guarantee the `encoding` is unknown as `string` is a superset of `BufferEncoding`.
function exec(
command: string,
options: {
encoding: BufferEncoding;
} & ExecOptions,
callback?: (error: ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void,
): ChildProcess;
// `options` without an `encoding` means stdout/stderr are definitely `string`.
function exec(
command: string,
options: ExecOptions,
options: ExecOptionsWithStringEncoding,
callback?: (error: ExecException | null, stdout: string, stderr: string) => void,
): ChildProcess;
// fallback if nothing else matches. Worst case is always `string | Buffer`.
function exec(
command: string,
options: (ObjectEncodingOptions & ExecOptions) | undefined | null,
callback?: (error: ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void,
options: ExecOptions | undefined | null,
callback?: (
error: ExecException | null,
stdout: string | NonSharedBuffer,
stderr: string | NonSharedBuffer,
) => void,
): ChildProcess;
interface PromiseWithChild<T> extends Promise<T> {
child: ChildProcess;
@@ -1035,35 +1030,24 @@ declare module "child_process" {
}>;
function __promisify__(
command: string,
options: {
encoding: "buffer" | null;
} & ExecOptions,
options: ExecOptionsWithBufferEncoding,
): PromiseWithChild<{
stdout: Buffer;
stderr: Buffer;
stdout: NonSharedBuffer;
stderr: NonSharedBuffer;
}>;
function __promisify__(
command: string,
options: {
encoding: BufferEncoding;
} & ExecOptions,
options: ExecOptionsWithStringEncoding,
): PromiseWithChild<{
stdout: string;
stderr: string;
}>;
function __promisify__(
command: string,
options: ExecOptions,
options: ExecOptions | undefined | null,
): PromiseWithChild<{
stdout: string;
stderr: string;
}>;
function __promisify__(
command: string,
options?: (ObjectEncodingOptions & ExecOptions) | null,
): PromiseWithChild<{
stdout: string | Buffer;
stderr: string | Buffer;
stdout: string | NonSharedBuffer;
stderr: string | NonSharedBuffer;
}>;
}
interface ExecFileOptions extends CommonOptions, Abortable {
@@ -1072,20 +1056,21 @@ declare module "child_process" {
windowsVerbatimArguments?: boolean | undefined;
shell?: boolean | string | undefined;
signal?: AbortSignal | undefined;
encoding?: string | null | undefined;
}
interface ExecFileOptionsWithStringEncoding extends ExecFileOptions {
encoding: BufferEncoding;
encoding?: BufferEncoding | undefined;
}
interface ExecFileOptionsWithBufferEncoding extends ExecFileOptions {
encoding: "buffer" | null;
}
interface ExecFileOptionsWithOtherEncoding extends ExecFileOptions {
encoding: BufferEncoding;
}
/** @deprecated Use `ExecFileOptions` instead. */
interface ExecFileOptionsWithOtherEncoding extends ExecFileOptions {}
// TODO: execFile exceptions can take many forms... this accurately describes none of them
type ExecFileException =
& Omit<ExecException, "code">
& Omit<NodeJS.ErrnoException, "code">
& { code?: string | number | undefined | null };
& { code?: string | number | null };
/**
* The `child_process.execFile()` function is similar to {@link exec} except that it does not spawn a shell by default. Rather, the specified
* executable `file` is spawned directly as a new process making it slightly more
@@ -1096,7 +1081,7 @@ declare module "child_process" {
* supported.
*
* ```js
* const { execFile } = require('node:child_process');
* import { execFile } from 'node:child_process';
* const child = execFile('node', ['--version'], (error, stdout, stderr) => {
* if (error) {
* throw error;
@@ -1119,8 +1104,9 @@ declare module "child_process" {
* callback, but with two additional properties `stdout` and `stderr`.
*
* ```js
* const util = require('node:util');
* const execFile = util.promisify(require('node:child_process').execFile);
* import util from 'node:util';
* import child_process from 'node:child_process';
* const execFile = util.promisify(child_process.execFile);
* async function getVersion() {
* const { stdout } = await execFile('node', ['--version']);
* console.log(stdout);
@@ -1136,7 +1122,7 @@ declare module "child_process" {
* the error passed to the callback will be an `AbortError`:
*
* ```js
* const { execFile } = require('node:child_process');
* import { execFile } from 'node:child_process';
* const controller = new AbortController();
* const { signal } = controller;
* const child = execFile('node', ['--version'], { signal }, (error) => {
@@ -1149,91 +1135,63 @@ declare module "child_process" {
* @param args List of string arguments.
* @param callback Called with the output when process terminates.
*/
function execFile(file: string): ChildProcess;
function execFile(
file: string,
options: (ObjectEncodingOptions & ExecFileOptions) | undefined | null,
): ChildProcess;
function execFile(file: string, args?: readonly string[] | null): ChildProcess;
function execFile(
file: string,
args: readonly string[] | undefined | null,
options: (ObjectEncodingOptions & ExecFileOptions) | undefined | null,
): ChildProcess;
// no `options` definitely means stdout/stderr are `string`.
function execFile(
file: string,
callback: (error: ExecFileException | null, stdout: string, stderr: string) => void,
callback?: (error: ExecFileException | null, stdout: string, stderr: string) => void,
): ChildProcess;
function execFile(
file: string,
args: readonly string[] | undefined | null,
callback: (error: ExecFileException | null, stdout: string, stderr: string) => void,
callback?: (error: ExecFileException | null, stdout: string, stderr: string) => void,
): ChildProcess;
// `options` with `"buffer"` or `null` for `encoding` means stdout/stderr are definitely `Buffer`.
function execFile(
file: string,
options: ExecFileOptionsWithBufferEncoding,
callback: (error: ExecFileException | null, stdout: Buffer, stderr: Buffer) => void,
callback?: (error: ExecFileException | null, stdout: NonSharedBuffer, stderr: NonSharedBuffer) => void,
): ChildProcess;
function execFile(
file: string,
args: readonly string[] | undefined | null,
options: ExecFileOptionsWithBufferEncoding,
callback: (error: ExecFileException | null, stdout: Buffer, stderr: Buffer) => void,
callback?: (error: ExecFileException | null, stdout: NonSharedBuffer, stderr: NonSharedBuffer) => void,
): ChildProcess;
// `options` with well known `encoding` means stdout/stderr are definitely `string`.
// `options` with well-known or absent `encoding` means stdout/stderr are definitely `string`.
function execFile(
file: string,
options: ExecFileOptionsWithStringEncoding,
callback: (error: ExecFileException | null, stdout: string, stderr: string) => void,
callback?: (error: ExecFileException | null, stdout: string, stderr: string) => void,
): ChildProcess;
function execFile(
file: string,
args: readonly string[] | undefined | null,
options: ExecFileOptionsWithStringEncoding,
callback: (error: ExecFileException | null, stdout: string, stderr: string) => void,
): ChildProcess;
// `options` with an `encoding` whose type is `string` means stdout/stderr could either be `Buffer` or `string`.
// There is no guarantee the `encoding` is unknown as `string` is a superset of `BufferEncoding`.
function execFile(
file: string,
options: ExecFileOptionsWithOtherEncoding,
callback: (error: ExecFileException | null, stdout: string | Buffer, stderr: string | Buffer) => void,
): ChildProcess;
function execFile(
file: string,
args: readonly string[] | undefined | null,
options: ExecFileOptionsWithOtherEncoding,
callback: (error: ExecFileException | null, stdout: string | Buffer, stderr: string | Buffer) => void,
): ChildProcess;
// `options` without an `encoding` means stdout/stderr are definitely `string`.
function execFile(
file: string,
options: ExecFileOptions,
callback: (error: ExecFileException | null, stdout: string, stderr: string) => void,
): ChildProcess;
function execFile(
file: string,
args: readonly string[] | undefined | null,
options: ExecFileOptions,
callback: (error: ExecFileException | null, stdout: string, stderr: string) => void,
callback?: (error: ExecFileException | null, stdout: string, stderr: string) => void,
): ChildProcess;
// fallback if nothing else matches. Worst case is always `string | Buffer`.
function execFile(
file: string,
options: (ObjectEncodingOptions & ExecFileOptions) | undefined | null,
options: ExecFileOptions | undefined | null,
callback:
| ((error: ExecFileException | null, stdout: string | Buffer, stderr: string | Buffer) => void)
| ((
error: ExecFileException | null,
stdout: string | NonSharedBuffer,
stderr: string | NonSharedBuffer,
) => void)
| undefined
| null,
): ChildProcess;
function execFile(
file: string,
args: readonly string[] | undefined | null,
options: (ObjectEncodingOptions & ExecFileOptions) | undefined | null,
options: ExecFileOptions | undefined | null,
callback:
| ((error: ExecFileException | null, stdout: string | Buffer, stderr: string | Buffer) => void)
| ((
error: ExecFileException | null,
stdout: string | NonSharedBuffer,
stderr: string | NonSharedBuffer,
) => void)
| undefined
| null,
): ChildProcess;
@@ -1253,16 +1211,16 @@ declare module "child_process" {
file: string,
options: ExecFileOptionsWithBufferEncoding,
): PromiseWithChild<{
stdout: Buffer;
stderr: Buffer;
stdout: NonSharedBuffer;
stderr: NonSharedBuffer;
}>;
function __promisify__(
file: string,
args: readonly string[] | undefined | null,
options: ExecFileOptionsWithBufferEncoding,
): PromiseWithChild<{
stdout: Buffer;
stderr: Buffer;
stdout: NonSharedBuffer;
stderr: NonSharedBuffer;
}>;
function __promisify__(
file: string,
@@ -1281,48 +1239,18 @@ declare module "child_process" {
}>;
function __promisify__(
file: string,
options: ExecFileOptionsWithOtherEncoding,
options: ExecFileOptions | undefined | null,
): PromiseWithChild<{
stdout: string | Buffer;
stderr: string | Buffer;
stdout: string | NonSharedBuffer;
stderr: string | NonSharedBuffer;
}>;
function __promisify__(
file: string,
args: readonly string[] | undefined | null,
options: ExecFileOptionsWithOtherEncoding,
options: ExecFileOptions | undefined | null,
): PromiseWithChild<{
stdout: string | Buffer;
stderr: string | Buffer;
}>;
function __promisify__(
file: string,
options: ExecFileOptions,
): PromiseWithChild<{
stdout: string;
stderr: string;
}>;
function __promisify__(
file: string,
args: readonly string[] | undefined | null,
options: ExecFileOptions,
): PromiseWithChild<{
stdout: string;
stderr: string;
}>;
function __promisify__(
file: string,
options: (ObjectEncodingOptions & ExecFileOptions) | undefined | null,
): PromiseWithChild<{
stdout: string | Buffer;
stderr: string | Buffer;
}>;
function __promisify__(
file: string,
args: readonly string[] | undefined | null,
options: (ObjectEncodingOptions & ExecFileOptions) | undefined | null,
): PromiseWithChild<{
stdout: string | Buffer;
stderr: string | Buffer;
stdout: string | NonSharedBuffer;
stderr: string | NonSharedBuffer;
}>;
}
interface ForkOptions extends ProcessEnvOptions, MessagingOptions, Abortable {
@@ -1377,7 +1305,7 @@ declare module "child_process" {
* console.log(`Hello from ${process.argv[2]}!`);
* }, 1_000);
* } else {
* const { fork } = require('node:child_process');
* import { fork } from 'node:child_process';
* const controller = new AbortController();
* const { signal } = controller;
* const child = fork(__filename, ['child'], { signal });
@@ -1411,7 +1339,7 @@ declare module "child_process" {
stderr: T;
status: number | null;
signal: NodeJS.Signals | null;
error?: Error | undefined;
error?: Error;
}
/**
* The `child_process.spawnSync()` method is generally identical to {@link spawn} with the exception that the function will not return
@@ -1428,11 +1356,11 @@ declare module "child_process" {
* @param command The command to run.
* @param args List of string arguments.
*/
function spawnSync(command: string): SpawnSyncReturns<Buffer>;
function spawnSync(command: string): SpawnSyncReturns<NonSharedBuffer>;
function spawnSync(command: string, options: SpawnSyncOptionsWithStringEncoding): SpawnSyncReturns<string>;
function spawnSync(command: string, options: SpawnSyncOptionsWithBufferEncoding): SpawnSyncReturns<Buffer>;
function spawnSync(command: string, options?: SpawnSyncOptions): SpawnSyncReturns<string | Buffer>;
function spawnSync(command: string, args: readonly string[]): SpawnSyncReturns<Buffer>;
function spawnSync(command: string, options: SpawnSyncOptionsWithBufferEncoding): SpawnSyncReturns<NonSharedBuffer>;
function spawnSync(command: string, options?: SpawnSyncOptions): SpawnSyncReturns<string | NonSharedBuffer>;
function spawnSync(command: string, args: readonly string[]): SpawnSyncReturns<NonSharedBuffer>;
function spawnSync(
command: string,
args: readonly string[],
@@ -1442,12 +1370,12 @@ declare module "child_process" {
command: string,
args: readonly string[],
options: SpawnSyncOptionsWithBufferEncoding,
): SpawnSyncReturns<Buffer>;
): SpawnSyncReturns<NonSharedBuffer>;
function spawnSync(
command: string,
args?: readonly string[],
options?: SpawnSyncOptions,
): SpawnSyncReturns<string | Buffer>;
): SpawnSyncReturns<string | NonSharedBuffer>;
interface CommonExecOptions extends CommonOptions {
input?: string | NodeJS.ArrayBufferView | undefined;
/**
@@ -1489,10 +1417,10 @@ declare module "child_process" {
* @param command The command to run.
* @return The stdout from the command.
*/
function execSync(command: string): Buffer;
function execSync(command: string): NonSharedBuffer;
function execSync(command: string, options: ExecSyncOptionsWithStringEncoding): string;
function execSync(command: string, options: ExecSyncOptionsWithBufferEncoding): Buffer;
function execSync(command: string, options?: ExecSyncOptions): string | Buffer;
function execSync(command: string, options: ExecSyncOptionsWithBufferEncoding): NonSharedBuffer;
function execSync(command: string, options?: ExecSyncOptions): string | NonSharedBuffer;
interface ExecFileSyncOptions extends CommonExecOptions {
shell?: boolean | string | undefined;
}
@@ -1500,7 +1428,7 @@ declare module "child_process" {
encoding: BufferEncoding;
}
interface ExecFileSyncOptionsWithBufferEncoding extends ExecFileSyncOptions {
encoding?: "buffer" | null; // specify `null`.
encoding?: "buffer" | null | undefined; // specify `null`.
}
/**
* The `child_process.execFileSync()` method is generally identical to {@link execFile} with the exception that the method will not
@@ -1522,11 +1450,11 @@ declare module "child_process" {
* @param args List of string arguments.
* @return The stdout from the command.
*/
function execFileSync(file: string): Buffer;
function execFileSync(file: string): NonSharedBuffer;
function execFileSync(file: string, options: ExecFileSyncOptionsWithStringEncoding): string;
function execFileSync(file: string, options: ExecFileSyncOptionsWithBufferEncoding): Buffer;
function execFileSync(file: string, options?: ExecFileSyncOptions): string | Buffer;
function execFileSync(file: string, args: readonly string[]): Buffer;
function execFileSync(file: string, options: ExecFileSyncOptionsWithBufferEncoding): NonSharedBuffer;
function execFileSync(file: string, options?: ExecFileSyncOptions): string | NonSharedBuffer;
function execFileSync(file: string, args: readonly string[]): NonSharedBuffer;
function execFileSync(
file: string,
args: readonly string[],
@@ -1536,8 +1464,12 @@ declare module "child_process" {
file: string,
args: readonly string[],
options: ExecFileSyncOptionsWithBufferEncoding,
): Buffer;
function execFileSync(file: string, args?: readonly string[], options?: ExecFileSyncOptions): string | Buffer;
): NonSharedBuffer;
function execFileSync(
file: string,
args?: readonly string[],
options?: ExecFileSyncOptions,
): string | NonSharedBuffer;
}
declare module "node:child_process" {
export * from "child_process";

View File

@@ -1,7 +1,7 @@
/**
* Clusters of Node.js processes can be used to run multiple instances of Node.js
* that can distribute workloads among their application threads. When process isolation
* is not needed, use the [`worker_threads`](https://nodejs.org/docs/latest-v20.x/api/worker_threads.html)
* is not needed, use the [`worker_threads`](https://nodejs.org/docs/latest-v24.x/api/worker_threads.html)
* module instead, which allows running multiple application threads within a single Node.js instance.
*
* The cluster module allows easy creation of child processes that all share
@@ -50,7 +50,7 @@
* ```
*
* On Windows, it is not yet possible to set up a named pipe server in a worker.
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/cluster.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/cluster.js)
*/
declare module "cluster" {
import * as child from "node:child_process";
@@ -72,7 +72,7 @@ declare module "cluster" {
* String arguments passed to worker.
* @default process.argv.slice(2)
*/
args?: string[] | undefined;
args?: readonly string[] | undefined;
/**
* Whether or not to send output to parent's stdio.
* @default false
@@ -80,8 +80,8 @@ declare module "cluster" {
silent?: boolean | undefined;
/**
* Configures the stdio of forked processes. Because the cluster module relies on IPC to function, this configuration must
* contain an `'ipc'` entry. When this option is provided, it overrides `silent`. See [`child_prcess.spawn()`](https://nodejs.org/docs/latest-v20.x/api/child_process.html#child_processspawncommand-args-options)'s
* [`stdio`](https://nodejs.org/docs/latest-v20.x/api/child_process.html#optionsstdio).
* contain an `'ipc'` entry. When this option is provided, it overrides `silent`. See [`child_prcess.spawn()`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#child_processspawncommand-args-options)'s
* [`stdio`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#optionsstdio).
*/
stdio?: any[] | undefined;
/**
@@ -99,7 +99,7 @@ declare module "cluster" {
inspectPort?: number | (() => number) | undefined;
/**
* Specify the kind of serialization used for sending messages between processes. Possible values are `'json'` and `'advanced'`.
* See [Advanced serialization for `child_process`](https://nodejs.org/docs/latest-v20.x/api/child_process.html#advanced-serialization) for more details.
* See [Advanced serialization for `child_process`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#advanced-serialization) for more details.
* @default false
*/
serialization?: SerializationType | undefined;
@@ -142,10 +142,10 @@ declare module "cluster" {
*/
id: number;
/**
* All workers are created using [`child_process.fork()`](https://nodejs.org/docs/latest-v20.x/api/child_process.html#child_processforkmodulepath-args-options), the returned object
* All workers are created using [`child_process.fork()`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#child_processforkmodulepath-args-options), the returned object
* from this function is stored as `.process`. In a worker, the global `process` is stored.
*
* See: [Child Process module](https://nodejs.org/docs/latest-v20.x/api/child_process.html#child_processforkmodulepath-args-options).
* See: [Child Process module](https://nodejs.org/docs/latest-v24.x/api/child_process.html#child_processforkmodulepath-args-options).
*
* Workers will call `process.exit(0)` if the `'disconnect'` event occurs
* on `process` and `.exitedAfterDisconnect` is not `true`. This protects against
@@ -156,7 +156,7 @@ declare module "cluster" {
/**
* Send a message to a worker or primary, optionally with a handle.
*
* In the primary, this sends a message to a specific worker. It is identical to [`ChildProcess.send()`](https://nodejs.org/docs/latest-v20.x/api/child_process.html#subprocesssendmessage-sendhandle-options-callback).
* In the primary, this sends a message to a specific worker. It is identical to [`ChildProcess.send()`](https://nodejs.org/docs/latest-v24.x/api/child_process.html#subprocesssendmessage-sendhandle-options-callback).
*
* In a worker, this sends a message to the primary. It is identical to `process.send()`.
*
@@ -198,7 +198,7 @@ declare module "cluster" {
* This method is aliased as `worker.destroy()` for backwards compatibility.
*
* In a worker, `process.kill()` exists, but it is not this function;
* it is [`kill()`](https://nodejs.org/docs/latest-v20.x/api/process.html#processkillpid-signal).
* it is [`kill()`](https://nodejs.org/docs/latest-v24.x/api/process.html#processkillpid-signal).
* @since v0.9.12
* @param [signal='SIGTERM'] Name of the kill signal to send to the worker process.
*/
@@ -231,6 +231,8 @@ declare module "cluster" {
* the `'disconnect'` event has not been emitted after some time.
*
* ```js
* import net from 'node:net';
*
* if (cluster.isPrimary) {
* const worker = cluster.fork();
* let timeout;
@@ -248,7 +250,6 @@ declare module "cluster" {
* });
*
* } else if (cluster.isWorker) {
* const net = require('node:net');
* const server = net.createServer((socket) => {
* // Connections never end
* });
@@ -265,7 +266,7 @@ declare module "cluster" {
* @since v0.7.7
* @return A reference to `worker`.
*/
disconnect(): void;
disconnect(): this;
/**
* This function returns `true` if the worker is connected to its primary via its
* IPC channel, `false` otherwise. A worker is connected to its primary after it
@@ -411,7 +412,7 @@ declare module "cluster" {
readonly isWorker: boolean;
/**
* The scheduling policy, either `cluster.SCHED_RR` for round-robin or `cluster.SCHED_NONE` to leave it to the operating system. This is a
* global setting and effectively frozen once either the first worker is spawned, or [`.setupPrimary()`](https://nodejs.org/docs/latest-v20.x/api/cluster.html#clustersetupprimarysettings)
* global setting and effectively frozen once either the first worker is spawned, or [`.setupPrimary()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clustersetupprimarysettings)
* is called, whichever comes first.
*
* `SCHED_RR` is the default on all operating systems except Windows. Windows will change to `SCHED_RR` once libuv is able to effectively distribute
@@ -422,24 +423,24 @@ declare module "cluster" {
*/
schedulingPolicy: number;
/**
* After calling [`.setupPrimary()`](https://nodejs.org/docs/latest-v20.x/api/cluster.html#clustersetupprimarysettings)
* (or [`.fork()`](https://nodejs.org/docs/latest-v20.x/api/cluster.html#clusterforkenv)) this settings object will contain
* After calling [`.setupPrimary()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clustersetupprimarysettings)
* (or [`.fork()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clusterforkenv)) this settings object will contain
* the settings, including the default values.
*
* This object is not intended to be changed or set manually.
* @since v0.7.1
*/
readonly settings: ClusterSettings;
/** @deprecated since v16.0.0 - use [`.setupPrimary()`](https://nodejs.org/docs/latest-v20.x/api/cluster.html#clustersetupprimarysettings) instead. */
/** @deprecated since v16.0.0 - use [`.setupPrimary()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clustersetupprimarysettings) instead. */
setupMaster(settings?: ClusterSettings): void;
/**
* `setupPrimary` is used to change the default 'fork' behavior. Once called, the settings will be present in `cluster.settings`.
*
* Any settings changes only affect future calls to [`.fork()`](https://nodejs.org/docs/latest-v20.x/api/cluster.html#clusterforkenv)
* Any settings changes only affect future calls to [`.fork()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clusterforkenv)
* and have no effect on workers that are already running.
*
* The only attribute of a worker that cannot be set via `.setupPrimary()` is the `env` passed to
* [`.fork()`](https://nodejs.org/docs/latest-v20.x/api/cluster.html#clusterforkenv).
* [`.fork()`](https://nodejs.org/docs/latest-v24.x/api/cluster.html#clusterforkenv).
*
* The defaults above apply to the first call only; the defaults for later calls are the current values at the time of
* `cluster.setupPrimary()` is called.
@@ -480,7 +481,7 @@ declare module "cluster" {
* ```
* @since v0.7.0
*/
readonly worker?: Worker | undefined;
readonly worker?: Worker;
/**
* A hash that stores the active worker objects, keyed by `id` field. This makes it easy to loop through all the workers. It is only available in the primary process.
*
@@ -496,7 +497,7 @@ declare module "cluster" {
* ```
* @since v0.7.0
*/
readonly workers?: NodeJS.Dict<Worker> | undefined;
readonly workers?: NodeJS.Dict<Worker>;
readonly SCHED_NONE: number;
readonly SCHED_RR: number;
/**
@@ -549,10 +550,9 @@ declare module "cluster" {
prependListener(event: "exit", listener: (worker: Worker, code: number, signal: string) => void): this;
prependListener(event: "fork", listener: (worker: Worker) => void): this;
prependListener(event: "listening", listener: (worker: Worker, address: Address) => void): this;
// the handle is a net.Socket or net.Server object, or undefined.
prependListener(
event: "message",
listener: (worker: Worker, message: any, handle?: net.Socket | net.Server) => void,
listener: (worker: Worker, message: any, handle: net.Socket | net.Server) => void,
): this;
prependListener(event: "online", listener: (worker: Worker) => void): this;
prependListener(event: "setup", listener: (settings: ClusterSettings) => void): this;

View File

@@ -5,12 +5,12 @@
* The module exports two specific components:
*
* * A `Console` class with methods such as `console.log()`, `console.error()`, and `console.warn()` that can be used to write to any Node.js stream.
* * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and
* [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('node:console')`.
* * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
* [`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
*
* _**Warning**_: The global console object's methods are neither consistently
* synchronous like the browser APIs they resemble, nor are they consistently
* asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for
* asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
* more information.
*
* Example using the global `console`:
@@ -54,7 +54,7 @@
* myConsole.warn(`Danger ${name}! Danger!`);
* // Prints: Danger Will Robinson! Danger!, to err
* ```
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/console.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/console.js)
*/
declare module "console" {
import console = require("node:console");
@@ -70,7 +70,7 @@ declare module "node:console" {
* `console.assert()` writes a message if `value` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) or omitted. It only
* writes a message and does not otherwise affect execution. The output always
* starts with `"Assertion failed"`. If provided, `message` is formatted using
* [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args).
* [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args).
*
* If `value` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), nothing happens.
*
@@ -152,7 +152,7 @@ declare module "node:console" {
*/
debug(message?: any, ...optionalParams: any[]): void;
/**
* Uses [`util.inspect()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilinspectobject-options) on `obj` and prints the resulting string to `stdout`.
* Uses [`util.inspect()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilinspectobject-options) on `obj` and prints the resulting string to `stdout`.
* This function bypasses any custom `inspect()` function defined on `obj`.
* @since v0.1.101
*/
@@ -167,7 +167,7 @@ declare module "node:console" {
* Prints to `stderr` with newline. Multiple arguments can be passed, with the
* first used as the primary message and all additional used as substitution
* values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
* (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)).
* (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
*
* ```js
* const code = 5;
@@ -178,8 +178,8 @@ declare module "node:console" {
* ```
*
* If formatting elements (e.g. `%d`) are not found in the first string then
* [`util.inspect()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilinspectobject-options) is called on each argument and the
* resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)
* [`util.inspect()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilinspectobject-options) is called on each argument and the
* resulting string values are concatenated. See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)
* for more information.
* @since v0.1.100
*/
@@ -211,7 +211,7 @@ declare module "node:console" {
* Prints to `stdout` with newline. Multiple arguments can be passed, with the
* first used as the primary message and all additional used as substitution
* values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html)
* (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)).
* (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)).
*
* ```js
* const count = 5;
@@ -221,7 +221,7 @@ declare module "node:console" {
* // Prints: count: 5, to stdout
* ```
*
* See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.
* See [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args) for more information.
* @since v0.1.100
*/
log(message?: any, ...optionalParams: any[]): void;
@@ -297,7 +297,7 @@ declare module "node:console" {
*/
timeLog(label?: string, ...data: any[]): void;
/**
* Prints to `stderr` the string `'Trace: '`, followed by the [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)
* Prints to `stderr` the string `'Trace: '`, followed by the [`util.format()`](https://nodejs.org/docs/latest-v24.x/api/util.html#utilformatformat-args)
* formatted message and stack trace to the current position in the code.
*
* ```js
@@ -361,12 +361,12 @@ declare module "node:console" {
* The module exports two specific components:
*
* * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream.
* * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and
* [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without calling `require('console')`.
* * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstdout) and
* [`process.stderr`](https://nodejs.org/docs/latest-v24.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module.
*
* _**Warning**_: The global console object's methods are neither consistently
* synchronous like the browser APIs they resemble, nor are they consistently
* asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for
* asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v24.x/api/process.html#a-note-on-process-io) for
* more information.
*
* Example using the global `console`:
@@ -410,7 +410,7 @@ declare module "node:console" {
* myConsole.warn(`Danger ${name}! Danger!`);
* // Prints: Danger Will Robinson! Danger!, to err
* ```
* @see [source](https://github.com/nodejs/node/blob/v20.11.1/lib/console.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/console.js)
*/
namespace console {
interface ConsoleConstructorOptions {
@@ -431,9 +431,10 @@ declare module "node:console" {
colorMode?: boolean | "auto" | undefined;
/**
* Specifies options that are passed along to
* [`util.inspect()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilinspectobject-options).
* `util.inspect()`. Can be an options object or, if different options
* for stdout and stderr are desired, a `Map` from stream objects to options.
*/
inspectOptions?: InspectOptions | undefined;
inspectOptions?: InspectOptions | ReadonlyMap<NodeJS.WritableStream, InspectOptions> | undefined;
/**
* Set group indentation.
* @default 2

View File

@@ -1,16 +1,18 @@
/** @deprecated since v6.3.0 - use constants property exposed by the relevant module instead. */
/**
* @deprecated The `node:constants` module is deprecated. When requiring access to constants
* relevant to specific Node.js builtin modules, developers should instead refer
* to the `constants` property exposed by the relevant module. For instance,
* `require('node:fs').constants` and `require('node:os').constants`.
*/
declare module "constants" {
import { constants as osConstants, SignalConstants } from "node:os";
import { constants as cryptoConstants } from "node:crypto";
import { constants as fsConstants } from "node:fs";
const exp:
& typeof osConstants.errno
& typeof osConstants.priority
& SignalConstants
& typeof cryptoConstants
& typeof fsConstants;
export = exp;
const constants:
& typeof import("node:os").constants.dlopen
& typeof import("node:os").constants.errno
& typeof import("node:os").constants.priority
& typeof import("node:os").constants.signals
& typeof import("node:fs").constants
& typeof import("node:crypto").constants;
export = constants;
}
declare module "node:constants" {

File diff suppressed because it is too large Load Diff

View File

@@ -23,10 +23,11 @@
* server.bind(41234);
* // Prints: server listening 0.0.0.0:41234
* ```
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/dgram.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/dgram.js)
*/
declare module "dgram" {
import { AddressInfo } from "node:net";
import { NonSharedBuffer } from "node:buffer";
import { AddressInfo, BlockList } from "node:net";
import * as dns from "node:dns";
import { Abortable, EventEmitter } from "node:events";
interface RemoteInfo {
@@ -45,6 +46,7 @@ declare module "dgram" {
interface SocketOptions extends Abortable {
type: SocketType;
reuseAddr?: boolean | undefined;
reusePort?: boolean | undefined;
/**
* @default false
*/
@@ -58,6 +60,8 @@ declare module "dgram" {
callback: (err: NodeJS.ErrnoException | null, address: string, family: number) => void,
) => void)
| undefined;
receiveBlockList?: BlockList | undefined;
sendBlockList?: BlockList | undefined;
}
/**
* Creates a `dgram.Socket` object. Once the socket is created, calling `socket.bind()` will instruct the socket to begin listening for datagram
@@ -82,8 +86,8 @@ declare module "dgram" {
* @param options Available options are:
* @param callback Attached as a listener for `'message'` events. Optional.
*/
function createSocket(type: SocketType, callback?: (msg: Buffer, rinfo: RemoteInfo) => void): Socket;
function createSocket(options: SocketOptions, callback?: (msg: Buffer, rinfo: RemoteInfo) => void): Socket;
function createSocket(type: SocketType, callback?: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): Socket;
function createSocket(options: SocketOptions, callback?: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): Socket;
/**
* Encapsulates the datagram functionality.
*
@@ -352,22 +356,22 @@ declare module "dgram" {
* @param callback Called when the message has been sent.
*/
send(
msg: string | Uint8Array | readonly any[],
msg: string | NodeJS.ArrayBufferView | readonly any[],
port?: number,
address?: string,
callback?: (error: Error | null, bytes: number) => void,
): void;
send(
msg: string | Uint8Array | readonly any[],
msg: string | NodeJS.ArrayBufferView | readonly any[],
port?: number,
callback?: (error: Error | null, bytes: number) => void,
): void;
send(
msg: string | Uint8Array | readonly any[],
msg: string | NodeJS.ArrayBufferView | readonly any[],
callback?: (error: Error | null, bytes: number) => void,
): void;
send(
msg: string | Uint8Array,
msg: string | NodeJS.ArrayBufferView,
offset: number,
length: number,
port?: number,
@@ -375,14 +379,14 @@ declare module "dgram" {
callback?: (error: Error | null, bytes: number) => void,
): void;
send(
msg: string | Uint8Array,
msg: string | NodeJS.ArrayBufferView,
offset: number,
length: number,
port?: number,
callback?: (error: Error | null, bytes: number) => void,
): void;
send(
msg: string | Uint8Array,
msg: string | NodeJS.ArrayBufferView,
offset: number,
length: number,
callback?: (error: Error | null, bytes: number) => void,
@@ -553,37 +557,37 @@ declare module "dgram" {
addListener(event: "connect", listener: () => void): this;
addListener(event: "error", listener: (err: Error) => void): this;
addListener(event: "listening", listener: () => void): this;
addListener(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): this;
addListener(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this;
emit(event: string | symbol, ...args: any[]): boolean;
emit(event: "close"): boolean;
emit(event: "connect"): boolean;
emit(event: "error", err: Error): boolean;
emit(event: "listening"): boolean;
emit(event: "message", msg: Buffer, rinfo: RemoteInfo): boolean;
emit(event: "message", msg: NonSharedBuffer, rinfo: RemoteInfo): boolean;
on(event: string, listener: (...args: any[]) => void): this;
on(event: "close", listener: () => void): this;
on(event: "connect", listener: () => void): this;
on(event: "error", listener: (err: Error) => void): this;
on(event: "listening", listener: () => void): this;
on(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): this;
on(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this;
once(event: string, listener: (...args: any[]) => void): this;
once(event: "close", listener: () => void): this;
once(event: "connect", listener: () => void): this;
once(event: "error", listener: (err: Error) => void): this;
once(event: "listening", listener: () => void): this;
once(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): this;
once(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this;
prependListener(event: string, listener: (...args: any[]) => void): this;
prependListener(event: "close", listener: () => void): this;
prependListener(event: "connect", listener: () => void): this;
prependListener(event: "error", listener: (err: Error) => void): this;
prependListener(event: "listening", listener: () => void): this;
prependListener(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): this;
prependListener(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this;
prependOnceListener(event: string, listener: (...args: any[]) => void): this;
prependOnceListener(event: "close", listener: () => void): this;
prependOnceListener(event: "connect", listener: () => void): this;
prependOnceListener(event: "error", listener: (err: Error) => void): this;
prependOnceListener(event: "listening", listener: () => void): this;
prependOnceListener(event: "message", listener: (msg: Buffer, rinfo: RemoteInfo) => void): this;
prependOnceListener(event: "message", listener: (msg: NonSharedBuffer, rinfo: RemoteInfo) => void): this;
/**
* Calls `socket.close()` and returns a promise that fulfills when the socket has closed.
* @since v20.5.0

View File

@@ -20,7 +20,7 @@
* should generally include the module name to avoid collisions with data from
* other modules.
* @since v15.1.0, v14.17.0
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/diagnostics_channel.js)
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/diagnostics_channel.js)
*/
declare module "diagnostics_channel" {
import { AsyncLocalStorage } from "node:async_hooks";
@@ -189,7 +189,6 @@ declare module "diagnostics_channel" {
* });
* ```
* @since v15.1.0, v14.17.0
* @deprecated Since v18.7.0,v16.17.0 - Use {@link subscribe(name, onMessage)}
* @param onMessage The handler to receive channel messages
*/
subscribe(onMessage: ChannelListener): void;
@@ -210,7 +209,6 @@ declare module "diagnostics_channel" {
* channel.unsubscribe(onMessage);
* ```
* @since v15.1.0, v14.17.0
* @deprecated Since v18.7.0,v16.17.0 - Use {@link unsubscribe(name, onMessage)}
* @param onMessage The previous subscribed handler to remove
* @return `true` if the handler was found, `false` otherwise.
*/
@@ -259,7 +257,7 @@ declare module "diagnostics_channel" {
* @param store The store to unbind from the channel.
* @return `true` if the store was found, `false` otherwise.
*/
unbindStore(store: any): void;
unbindStore(store: AsyncLocalStorage<StoreType>): boolean;
/**
* Applies the given data to any AsyncLocalStorage instances bound to the channel
* for the duration of the given function, then publishes to the channel within
@@ -297,7 +295,12 @@ declare module "diagnostics_channel" {
* @param thisArg The receiver to be used for the function call.
* @param args Optional arguments to pass to the function.
*/
runStores(): void;
runStores<ThisArg = any, Args extends any[] = any[], Result = any>(
context: ContextType,
fn: (this: ThisArg, ...args: Args) => Result,
thisArg?: ThisArg,
...args: Args
): Result;
}
interface TracingChannelSubscribers<ContextType extends object> {
start: (message: ContextType) => void;
@@ -441,12 +444,12 @@ declare module "diagnostics_channel" {
* @param args Optional arguments to pass to the function
* @return The return value of the given function
*/
traceSync<ThisArg = any, Args extends any[] = any[]>(
fn: (this: ThisArg, ...args: Args) => any,
traceSync<ThisArg = any, Args extends any[] = any[], Result = any>(
fn: (this: ThisArg, ...args: Args) => Result,
context?: ContextType,
thisArg?: ThisArg,
...args: Args
): void;
): Result;
/**
* Trace a promise-returning function call. This will always produce a `start event` and `end event` around the synchronous portion of the
* function execution, and will produce an `asyncStart event` and `asyncEnd event` when a promise continuation is reached. It may also
@@ -476,12 +479,12 @@ declare module "diagnostics_channel" {
* @param args Optional arguments to pass to the function
* @return Chained from promise returned by the given function
*/
tracePromise<ThisArg = any, Args extends any[] = any[]>(
fn: (this: ThisArg, ...args: Args) => Promise<any>,
tracePromise<ThisArg = any, Args extends any[] = any[], Result = any>(
fn: (this: ThisArg, ...args: Args) => Promise<Result>,
context?: ContextType,
thisArg?: ThisArg,
...args: Args
): void;
): Promise<Result>;
/**
* Trace a callback-receiving function call. This will always produce a `start event` and `end event` around the synchronous portion of the
* function execution, and will produce a `asyncStart event` and `asyncEnd event` around the callback execution. It may also produce an `error event` if the given function throws an error or
@@ -540,13 +543,32 @@ declare module "diagnostics_channel" {
* @param args Optional arguments to pass to the function
* @return The return value of the given function
*/
traceCallback<Fn extends (this: any, ...args: any) => any>(
fn: Fn,
position: number | undefined,
context: ContextType | undefined,
thisArg: any,
...args: Parameters<Fn>
): void;
traceCallback<ThisArg = any, Args extends any[] = any[], Result = any>(
fn: (this: ThisArg, ...args: Args) => Result,
position?: number,
context?: ContextType,
thisArg?: ThisArg,
...args: Args
): Result;
/**
* `true` if any of the individual channels has a subscriber, `false` if not.
*
* This is a helper method available on a {@link TracingChannel} instance to check
* if any of the [TracingChannel Channels](https://nodejs.org/api/diagnostics_channel.html#tracingchannel-channels) have subscribers.
* A `true` is returned if any of them have at least one subscriber, a `false` is returned otherwise.
*
* ```js
* const diagnostics_channel = require('node:diagnostics_channel');
*
* const channels = diagnostics_channel.tracingChannel('my-channel');
*
* if (channels.hasSubscribers) {
* // Do something
* }
* ```
* @since v22.0.0, v20.13.0
*/
readonly hasSubscribers: boolean;
}
}
declare module "node:diagnostics_channel" {

View File

@@ -9,7 +9,7 @@
* system do, use {@link lookup}.
*
* ```js
* const dns = require('node:dns');
* import dns from 'node:dns';
*
* dns.lookup('example.org', (err, address, family) => {
* console.log('address: %j family: IPv%s', address, family);
@@ -23,7 +23,7 @@
* DNS queries, bypassing other name-resolution facilities.
*
* ```js
* const dns = require('node:dns');
* import dns from 'node:dns';
*
* dns.resolve4('archive.org', (err, addresses) => {
* if (err) throw err;
@@ -41,8 +41,8 @@
* });
* ```
*
* See the [Implementation considerations section](https://nodejs.org/docs/latest-v20.x/api/dns.html#implementation-considerations) for more information.
* @see [source](https://github.com/nodejs/node/blob/v20.13.1/lib/dns.js)
* See the [Implementation considerations section](https://nodejs.org/docs/latest-v24.x/api/dns.html#implementation-considerations) for more information.
* @see [source](https://github.com/nodejs/node/blob/v24.x/lib/dns.js)
*/
declare module "dns" {
import * as dnsPromises from "node:dns/promises";
@@ -71,7 +71,7 @@ declare module "dns" {
*/
family?: number | "IPv4" | "IPv6" | undefined;
/**
* One or more [supported `getaddrinfo`](https://nodejs.org/docs/latest-v20.x/api/dns.html#supported-getaddrinfo-flags) flags. Multiple flags may be
* One or more [supported `getaddrinfo`](https://nodejs.org/docs/latest-v24.x/api/dns.html#supported-getaddrinfo-flags) flags. Multiple flags may be
* passed by bitwise `OR`ing their values.
*/
hints?: number | undefined;
@@ -84,16 +84,17 @@ declare module "dns" {
* When `verbatim`, the resolved addresses are return unsorted. When `ipv4first`, the resolved addresses are sorted
* by placing IPv4 addresses before IPv6 addresses. When `ipv6first`, the resolved addresses are sorted by placing IPv6
* addresses before IPv4 addresses. Default value is configurable using
* {@link setDefaultResultOrder} or [`--dns-result-order`](https://nodejs.org/docs/latest-v20.x/api/cli.html#--dns-result-orderorder).
* {@link setDefaultResultOrder} or [`--dns-result-order`](https://nodejs.org/docs/latest-v24.x/api/cli.html#--dns-result-orderorder).
* @default `verbatim` (addresses are not reordered)
* @since v22.1.0
*/
order?: "ipv4first" | "ipv6first" | "verbatim" | undefined;
/**
* When `true`, the callback receives IPv4 and IPv6 addresses in the order the DNS resolver returned them. When `false`, IPv4
* addresses are placed before IPv6 addresses. This option will be deprecated in favor of `order`. When both are specified,
* `order` has higher precedence. New code should only use `order`. Default value is configurable using {@link setDefaultResultOrder}
* or [`--dns-result-order`](https://nodejs.org/docs/latest-v20.x/api/cli.html#--dns-result-orderorder).
* @default true (addresses are not reordered)
* @deprecated Please use `order` option
*/
verbatim?: boolean | undefined;
}
@@ -132,13 +133,13 @@ declare module "dns" {
* The implementation uses an operating system facility that can associate names
* with addresses and vice versa. This implementation can have subtle but
* important consequences on the behavior of any Node.js program. Please take some
* time to consult the [Implementation considerations section](https://nodejs.org/docs/latest-v20.x/api/dns.html#implementation-considerations)
* time to consult the [Implementation considerations section](https://nodejs.org/docs/latest-v24.x/api/dns.html#implementation-considerations)
* before using `dns.lookup()`.
*
* Example usage:
*
* ```js
* const dns = require('node:dns');
* import dns from 'node:dns';
* const options = {
* family: 6,
* hints: dns.ADDRCONFIG | dns.V4MAPPED,
@@ -154,7 +155,7 @@ declare module "dns" {
* // addresses: [{"address":"2606:2800:220:1:248:1893:25c8:1946","family":6}]
* ```
*
* If this method is invoked as its [util.promisify()](https://nodejs.org/docs/latest-v20.x/api/util.html#utilpromisifyoriginal) ed
* If this method is invoked as its [util.promisify()](https://nodejs.org/docs/latest-v24.x/api/util.html#utilpromisifyoriginal) ed
* version, and `all` is not set to `true`, it returns a `Promise` for an `Object` with `address` and `family` properties.
* @since v0.1.90
*/
@@ -194,18 +195,18 @@ declare module "dns" {
* If `address` is not a valid IP address, a `TypeError` will be thrown.
* The `port` will be coerced to a number. If it is not a legal port, a `TypeError` will be thrown.
*
* On an error, `err` is an [`Error`](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) object,
* On an error, `err` is an [`Error`](https://nodejs.org/docs/latest-v24.x/api/errors.html#class-error) object,
* where `err.code` is the error code.
*
* ```js
* const dns = require('node:dns');
* import dns from 'node:dns';
* dns.lookupService('127.0.0.1', 22, (err, hostname, service) => {
* console.log(hostname, service);
* // Prints: localhost ssh
* });
* ```
*
* If this method is invoked as its [util.promisify()](https://nodejs.org/docs/latest-v20.x/api/util.html#utilpromisifyoriginal) ed
* If this method is invoked as its [util.promisify()](https://nodejs.org/docs/latest-v24.x/api/util.html#utilpromisifyoriginal) ed
* version, it returns a `Promise` for an `Object` with `hostname` and `service` properties.
* @since v0.11.14
*/
@@ -249,6 +250,9 @@ declare module "dns" {
contactemail?: string | undefined;
contactphone?: string | undefined;
}
export interface AnyCaaRecord extends CaaRecord {
type: "CAA";
}
export interface MxRecord {
priority: number;
exchange: string;
@@ -288,6 +292,15 @@ declare module "dns" {
export interface AnySrvRecord extends SrvRecord {
type: "SRV";
}
export interface TlsaRecord {
certUsage: number;
selector: number;
match: number;
data: ArrayBuffer;
}
export interface AnyTlsaRecord extends TlsaRecord {
type: "TLSA";
}
export interface AnyTxtRecord {
type: "TXT";
entries: string[];
@@ -307,6 +320,7 @@ declare module "dns" {
export type AnyRecord =
| AnyARecord
| AnyAaaaRecord
| AnyCaaRecord
| AnyCnameRecord
| AnyMxRecord
| AnyNaptrRecord
@@ -314,6 +328,7 @@ declare module "dns" {
| AnyPtrRecord
| AnySoaRecord
| AnySrvRecord
| AnyTlsaRecord
| AnyTxtRecord;
/**
* Uses the DNS protocol to resolve a host name (e.g. `'nodejs.org'`) into an array
@@ -322,7 +337,7 @@ declare module "dns" {
*
* <omitted>
*
* On error, `err` is an [`Error`](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) object,
* On error, `err` is an [`Error`](https://nodejs.org/docs/latest-v24.x/api/errors.html#class-error) object,
* where `err.code` is one of the `DNS error codes`.
* @since v0.1.27
* @param hostname Host name to resolve.
@@ -334,12 +349,7 @@ declare module "dns" {
): void;
export function resolve(
hostname: string,
rrtype: "A",
callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void,
): void;
export function resolve(
hostname: string,
rrtype: "AAAA",
rrtype: "A" | "AAAA" | "CNAME" | "NS" | "PTR",
callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void,
): void;
export function resolve(
@@ -349,8 +359,8 @@ declare module "dns" {
): void;
export function resolve(
hostname: string,
rrtype: "CNAME",
callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void,
rrtype: "CAA",
callback: (err: NodeJS.ErrnoException | null, address: CaaRecord[]) => void,
): void;
export function resolve(
hostname: string,
@@ -362,16 +372,6 @@ declare module "dns" {
rrtype: "NAPTR",
callback: (err: NodeJS.ErrnoException | null, addresses: NaptrRecord[]) => void,
): void;
export function resolve(
hostname: string,
rrtype: "NS",
callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void,
): void;
export function resolve(
hostname: string,
rrtype: "PTR",
callback: (err: NodeJS.ErrnoException | null, addresses: string[]) => void,
): void;
export function resolve(
hostname: string,
rrtype: "SOA",
@@ -382,6 +382,11 @@ declare module "dns" {
rrtype: "SRV",
callback: (err: NodeJS.ErrnoException | null, addresses: SrvRecord[]) => void,
): void;
export function resolve(
hostname: string,
rrtype: "TLSA",
callback: (err: NodeJS.ErrnoException | null, addresses: TlsaRecord[]) => void,
): void;
export function resolve(
hostname: string,
rrtype: "TXT",
@@ -392,21 +397,42 @@ declare module "dns" {
rrtype: string,
callback: (
err: NodeJS.ErrnoException | null,
addresses: string[] | MxRecord[] | NaptrRecord[] | SoaRecord | SrvRecord[] | string[][] | AnyRecord[],
addresses:
| string[]
| CaaRecord[]
| MxRecord[]
| NaptrRecord[]
| SoaRecord
| SrvRecord[]
| TlsaRecord[]
| string[][]
| AnyRecord[],
) => void,
): void;
export namespace resolve {
function __promisify__(hostname: string, rrtype?: "A" | "AAAA" | "CNAME" | "NS" | "PTR"): Promise<string[]>;
function __promisify__(hostname: string, rrtype: "ANY"): Promise<AnyRecord[]>;
function __promisify__(hostname: string, rrtype: "CAA"): Promise<CaaRecord[]>;
function __promisify__(hostname: string, rrtype: "MX"): Promise<MxRecord[]>;
function __promisify__(hostname: string, rrtype: "NAPTR"): Promise<NaptrRecord[]>;
function __promisify__(hostname: string, rrtype: "SOA"): Promise<SoaRecord>;
function __promisify__(hostname: string, rrtype: "SRV"): Promise<SrvRecord[]>;
function __promisify__(hostname: string, rrtype: "TLSA"): Promise<TlsaRecord[]>;
function __promisify__(hostname: string, rrtype: "TXT"): Promise<string[][]>;
function __promisify__(
hostname: string,
rrtype: string,
): Promise<string[] | MxRecord[] | NaptrRecord[] | SoaRecord | SrvRecord[] | string[][] | AnyRecord[]>;
): Promise<
| string[]
| CaaRecord[]
| MxRecord[]
| NaptrRecord[]
| SoaRecord
| SrvRecord[]
| TlsaRecord[]
| string[][]
| AnyRecord[]
>;
}
/**
* Uses the DNS protocol to resolve a IPv4 addresses (`A` records) for the `hostname`. The `addresses` argument passed to the `callback` function
@@ -608,6 +634,33 @@ declare module "dns" {
export namespace resolveSrv {
function __promisify__(hostname: string): Promise<SrvRecord[]>;
}
/**
* Uses the DNS protocol to resolve certificate associations (`TLSA` records) for
* the `hostname`. The `records` argument passed to the `callback` function is an
* array of objects with these properties:
*
* * `certUsage`
* * `selector`
* * `match`
* * `data`
*
* ```js
* {
* certUsage: 3,
* selector: 1,
* match: 1,
* data: [ArrayBuffer]
* }
* ```
* @since v23.9.0, v22.15.0
*/
export function resolveTlsa(
hostname: string,
callback: (err: NodeJS.ErrnoException | null, addresses: TlsaRecord[]) => void,
): void;
export namespace resolveTlsa {
function __promisify__(hostname: string): Promise<TlsaRecord[]>;
}
/**
* Uses the DNS protocol to resolve text queries (`TXT` records) for the `hostname`. The `records` argument passed to the `callback` function is a
* two-dimensional array of the text records available for `hostname` (e.g.`[ ['v=spf1 ip4:0.0.0.0 ', '~all' ] ]`). Each sub-array contains TXT chunks of
@@ -663,8 +716,8 @@ declare module "dns" {
* Performs a reverse DNS query that resolves an IPv4 or IPv6 address to an
* array of host names.
*
* On error, `err` is an [`Error`](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) object, where `err.code` is
* one of the [DNS error codes](https://nodejs.org/docs/latest-v20.x/api/dns.html#error-codes).
* On error, `err` is an [`Error`](https://nodejs.org/docs/latest-v24.x/api/errors.html#class-error) object, where `err.code` is
* one of the [DNS error codes](https://nodejs.org/docs/latest-v24.x/api/dns.html#error-codes).
* @since v0.1.16
*/
export function reverse(
@@ -672,7 +725,7 @@ declare module "dns" {
callback: (err: NodeJS.ErrnoException | null, hostnames: string[]) => void,
): void;
/**
* Get the default value for `order` in {@link lookup} and [`dnsPromises.lookup()`](https://nodejs.org/docs/latest-v20.x/api/dns.html#dnspromiseslookuphostname-options).
* Get the default value for `order` in {@link lookup} and [`dnsPromises.lookup()`](https://nodejs.org/docs/latest-v24.x/api/dns.html#dnspromiseslookuphostname-options).
* The value could be:
*
* * `ipv4first`: for `order` defaulting to `ipv4first`.
@@ -727,7 +780,7 @@ declare module "dns" {
*/
export function getServers(): string[];
/**
* Set the default value of `order` in {@link lookup} and [`dnsPromises.lookup()`](https://nodejs.org/docs/latest-v20.x/api/dns.html#dnspromiseslookuphostname-options).
* Set the default value of `order` in {@link lookup} and [`dnsPromises.lookup()`](https://nodejs.org/docs/latest-v24.x/api/dns.html#dnspromiseslookuphostname-options).
* The value could be:
*
* * `ipv4first`: sets default `order` to `ipv4first`.
@@ -735,8 +788,8 @@ declare module "dns" {
* * `verbatim`: sets default `order` to `verbatim`.
*
* The default is `verbatim` and {@link setDefaultResultOrder} have higher
* priority than [`--dns-result-order`](https://nodejs.org/docs/latest-v20.x/api/cli.html#--dns-result-orderorder). When using
* [worker threads](https://nodejs.org/docs/latest-v20.x/api/worker_threads.html), {@link setDefaultResultOrder} from the main
* priority than [`--dns-result-order`](https://nodejs.org/docs/latest-v24.x/api/cli.html#--dns-result-orderorder). When using
* [worker threads](https://nodejs.org/docs/latest-v24.x/api/worker_threads.html), {@link setDefaultResultOrder} from the main
* thread won't affect the default dns orders in workers.
* @since v16.4.0, v14.18.0
* @param order must be `'ipv4first'`, `'ipv6first'` or `'verbatim'`.
@@ -776,17 +829,22 @@ declare module "dns" {
* The number of tries the resolver will try contacting each name server before giving up.
* @default 4
*/
tries?: number;
tries?: number | undefined;
/**
* The max retry timeout, in milliseconds.
* @default 0
*/
maxTimeout?: number | undefined;
}
/**
* An independent resolver for DNS requests.
*
* Creating a new resolver uses the default server settings. Setting
* the servers used for a resolver using [`resolver.setServers()`](https://nodejs.org/docs/latest-v20.x/api/dns.html#dnssetserversservers) does not affect
* the servers used for a resolver using [`resolver.setServers()`](https://nodejs.org/docs/latest-v24.x/api/dns.html#dnssetserversservers) does not affect
* other resolvers:
*
* ```js
* const { Resolver } = require('node:dns');
* import { Resolver } from 'node:dns';
* const resolver = new Resolver();
* resolver.setServers(['4.4.4.4']);
*
@@ -837,6 +895,7 @@ declare module "dns" {
resolvePtr: typeof resolvePtr;
resolveSoa: typeof resolveSoa;
resolveSrv: typeof resolveSrv;
resolveTlsa: typeof resolveTlsa;
resolveTxt: typeof resolveTxt;
reverse: typeof reverse;
/**

View File

@@ -1,7 +1,7 @@
/**
* The `dns.promises` API provides an alternative set of asynchronous DNS methods
* that return `Promise` objects rather than using callbacks. The API is accessible
* via `require('node:dns').promises` or `require('node:dns/promises')`.
* via `import { promises as dnsPromises } from 'node:dns'` or `import dnsPromises from 'node:dns/promises'`.
* @since v10.6.0
*/
declare module "dns/promises" {
@@ -20,6 +20,7 @@ declare module "dns/promises" {
ResolveWithTtlOptions,
SoaRecord,
SrvRecord,
TlsaRecord,
} from "node:dns";
/**
* Returns an array of IP address strings, formatted according to [RFC 5952](https://tools.ietf.org/html/rfc5952#section-6),
@@ -60,7 +61,7 @@ declare module "dns/promises" {
* Example usage:
*
* ```js
* const dns = require('node:dns');
* import dns from 'node:dns';
* const dnsPromises = dns.promises;
* const options = {
* family: 6,
@@ -96,7 +97,7 @@ declare module "dns/promises" {
* On error, the `Promise` is rejected with an [`Error`](https://nodejs.org/docs/latest-v20.x/api/errors.html#class-error) object, where `err.code` is the error code.
*
* ```js
* const dnsPromises = require('node:dns').promises;
* import dnsPromises from 'node:dns';
* dnsPromises.lookupService('127.0.0.1', 22).then((result) => {
* console.log(result.hostname, result.service);
* // Prints: localhost ssh
@@ -126,22 +127,26 @@ declare module "dns/promises" {
* @param [rrtype='A'] Resource record type.
*/
function resolve(hostname: string): Promise<string[]>;
function resolve(hostname: string, rrtype: "A"): Promise<string[]>;
function resolve(hostname: string, rrtype: "AAAA"): Promise<string[]>;
function resolve(hostname: string, rrtype: "A" | "AAAA" | "CNAME" | "NS" | "PTR"): Promise<string[]>;
function resolve(hostname: string, rrtype: "ANY"): Promise<AnyRecord[]>;
function resolve(hostname: string, rrtype: "CAA"): Promise<CaaRecord[]>;
function resolve(hostname: string, rrtype: "CNAME"): Promise<string[]>;
function resolve(hostname: string, rrtype: "MX"): Promise<MxRecord[]>;
function resolve(hostname: string, rrtype: "NAPTR"): Promise<NaptrRecord[]>;
function resolve(hostname: string, rrtype: "NS"): Promise<string[]>;
function resolve(hostname: string, rrtype: "PTR"): Promise<string[]>;
function resolve(hostname: string, rrtype: "SOA"): Promise<SoaRecord>;
function resolve(hostname: string, rrtype: "SRV"): Promise<SrvRecord[]>;
function resolve(hostname: string, rrtype: "TLSA"): Promise<TlsaRecord[]>;
function resolve(hostname: string, rrtype: "TXT"): Promise<string[][]>;
function resolve(
hostname: string,
rrtype: string,
): Promise<string[] | MxRecord[] | NaptrRecord[] | SoaRecord | SrvRecord[] | string[][] | AnyRecord[]>;
function resolve(hostname: string, rrtype: string): Promise<
| string[]
| CaaRecord[]
| MxRecord[]
| NaptrRecord[]
| SoaRecord
| SrvRecord[]
| TlsaRecord[]
| string[][]
| AnyRecord[]
>;
/**
* Uses the DNS protocol to resolve IPv4 addresses (`A` records) for the `hostname`. On success, the `Promise` is resolved with an array of IPv4
* addresses (e.g. `['74.125.79.104', '74.125.79.105', '74.125.79.106']`).
@@ -292,6 +297,27 @@ declare module "dns/promises" {
* @since v10.6.0
*/
function resolveSrv(hostname: string): Promise<SrvRecord[]>;
/**
* Uses the DNS protocol to resolve certificate associations (`TLSA` records) for
* the `hostname`. On success, the `Promise` is resolved with an array of objectsAdd commentMore actions
* with these properties:
*
* * `certUsage`
* * `selector`
* * `match`
* * `data`
*
* ```js
* {
* certUsage: 3,
* selector: 1,
* match: 1,
* data: [ArrayBuffer]
* }
* ```
* @since v23.9.0, v22.15.0
*/
function resolveTlsa(hostname: string): Promise<TlsaRecord[]>;
/**
* Uses the DNS protocol to resolve text queries (`TXT` records) for the `hostname`. On success, the `Promise` is resolved with a two-dimensional array
* of the text records available for `hostname` (e.g.`[ ['v=spf1 ip4:0.0.0.0 ', '~all' ] ]`). Each sub-array contains TXT chunks of
@@ -394,8 +420,8 @@ declare module "dns/promises" {
* other resolvers:
*
* ```js
* const { Resolver } = require('node:dns').promises;
* const resolver = new Resolver();
* import { promises } from 'node:dns';
* const resolver = new promises.Resolver();
* resolver.setServers(['4.4.4.4']);
*
* // This request will use the server at 4.4.4.4, independent of global settings.
@@ -450,6 +476,7 @@ declare module "dns/promises" {
resolvePtr: typeof resolvePtr;
resolveSoa: typeof resolveSoa;
resolveSrv: typeof resolveSrv;
resolveTlsa: typeof resolveTlsa;
resolveTxt: typeof resolveTxt;
reverse: typeof reverse;
/**

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