From 89329607dc54dbacb36966ed0b2c00261e43abd0 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 29 Oct 2025 18:07:43 +0100 Subject: [PATCH] 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. --- .../controllers/myTischtennisUrlController.js | 205 +++++++++++++++--- .../middleware/requestLoggingMiddleware.js | 7 +- backend/services/apiLogService.js | 8 +- .../services/autoFetchMatchResultsService.js | 102 +++++++-- frontend/src/views/LogsView.vue | 23 ++ 5 files changed, 295 insertions(+), 50 deletions(-) diff --git a/backend/controllers/myTischtennisUrlController.js b/backend/controllers/myTischtennisUrlController.js index 802aae9..c35ad54 100644 --- a/backend/controllers/myTischtennisUrlController.js +++ b/backend/controllers/myTischtennisUrlController.js @@ -1,6 +1,7 @@ import myTischtennisUrlParserService from '../services/myTischtennisUrlParserService.js'; import myTischtennisService from '../services/myTischtennisService.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'; @@ -228,6 +229,10 @@ class MyTischtennisUrlController { throw new HttpError(400, 'clubTeamId is required'); } + if (!userIdOrEmail) { + throw new HttpError(401, 'User-ID fehlt. Bitte melden Sie sich an.'); + } + // Get actual user ID (userid header might be email address) let userId = userIdOrEmail; if (isNaN(userIdOrEmail)) { @@ -246,17 +251,63 @@ class MyTischtennisUrlController { try { session = await myTischtennisService.getSession(userId); console.log('Session found:', !!session); + if (session) { + console.log('Session details:', { + hasCookie: !!session.cookie, + hasAccessToken: !!session.accessToken, + expiresAt: session.expiresAt ? new Date(session.expiresAt * 1000).toISOString() : null + }); + } } catch (sessionError) { console.log('Session invalid, attempting login...', sessionError.message); + console.error('Session error details:', { + message: sessionError.message, + stack: sessionError.stack, + name: sessionError.name + }); // Versuche automatischen Login mit gespeicherten Credentials try { - await myTischtennisService.verifyLogin(userId); + console.log('Attempting automatic login for userId:', userId); + + // Check if account exists and has password + const accountCheck = await myTischtennisService.getAccount(userId); + if (!accountCheck) { + throw new Error('MyTischtennis-Account nicht gefunden'); + } + + console.log('Account found:', { + email: accountCheck.email, + hasPassword: !!accountCheck.encryptedPassword, + hasAccessToken: !!accountCheck.accessToken, + hasCookie: !!accountCheck.cookie + }); + + if (!accountCheck.encryptedPassword) { + throw new Error('Kein Passwort gespeichert. Bitte melden Sie sich in den MyTischtennis-Einstellungen an und speichern Sie Ihr Passwort.'); + } + + console.log('Calling verifyLogin...'); + const verifyResult = await myTischtennisService.verifyLogin(userId); + console.log('verifyLogin result:', verifyResult); + session = await myTischtennisService.getSession(userId); - console.log('Automatic login successful'); + console.log('Automatic login successful, session:', { + hasCookie: !!session?.cookie, + hasAccessToken: !!session?.accessToken + }); } catch (loginError) { - console.error('Automatic login failed:', loginError.message); - throw new HttpError(401, 'MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.'); + console.error('Automatic login failed - DETAILED ERROR:', { + message: loginError.message, + stack: loginError.stack, + name: loginError.name, + userId: userId, + response: loginError.response?.data, + status: loginError.response?.status + }); + + const errorMessage = loginError.message || 'Automatischer Login fehlgeschlagen'; + throw new HttpError(401, `MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen: ${errorMessage}. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.`); } } @@ -291,7 +342,7 @@ class MyTischtennisUrlController { }); if (!team) { - throw new HttpError(404, 'Team not found'); + throw new HttpError(404, `Team mit ID ${clubTeamId} nicht gefunden`); } console.log('Team data:', { @@ -309,11 +360,74 @@ class MyTischtennisUrlController { } : null }); - if (!team.myTischtennisTeamId || !team.league || !team.league.myTischtennisGroupId) { - throw new HttpError(400, 'Team is not configured for myTischtennis'); + // Verbesserte Validierung mit detaillierten Fehlermeldungen + if (!team.myTischtennisTeamId) { + throw new HttpError(400, `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.`); + } + + // Stelle sicher, dass die myTischtennisTeamId auch wirklich verwendet wird + console.log(`Verwende myTischtennisTeamId: ${team.myTischtennisTeamId} (nicht die interne clubTeamId: ${team.id})`); + + if (!team.league) { + throw new HttpError(400, 'Team ist keiner Liga zugeordnet. Bitte ordnen Sie das Team einer Liga zu.'); + } + + if (!team.league.myTischtennisGroupId) { + throw new HttpError(400, 'Liga ist nicht für myTischtennis konfiguriert: myTischtennisGroupId fehlt. Bitte konfigurieren Sie die Liga zuerst über die MyTischtennis-URL.'); + } + + // Validate season before proceeding + if (!team.league.season || !team.league.season.season) { + throw new HttpError(400, 'Liga ist keiner Saison zugeordnet. Bitte ordnen Sie die Liga einer Saison zu.'); + } + + // 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, '_')); + const 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 + const requestStartTime = Date.now(); + console.log('=== ABOUT TO FETCH FROM MYTISCHTENNIS ==='); + console.log('URL:', myTischtennisUrl); + console.log('myTischtennisTeamId:', team.myTischtennisTeamId); + console.log('clubTeamId:', team.id); + + 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) { + console.error('Error logging request (non-critical):', logError); } // Fetch data for this specific team + // Note: fetchTeamResults will also log and update with actual response const result = await autoFetchMatchResultsService.fetchTeamResults( { userId: account.userId, @@ -326,32 +440,63 @@ class MyTischtennisUrlController { team ); - // Also fetch and update league table data - let tableUpdateResult = null; + // Also fetch and update league table data + let tableUpdateResult = null; try { await autoFetchMatchResultsService.fetchAndUpdateLeagueTable(account.userId, team.league.id); - tableUpdateResult = 'League table updated successfully'; - console.log('✓ League table updated for league:', team.league.id); - } catch (error) { - console.error('Error fetching league table data:', error); - tableUpdateResult = 'League table update failed: ' + error.message; - // Don't fail the entire request if table update fails - } - - res.json({ - success: true, - message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`, - data: { - fetchedCount: result.fetchedCount, - teamName: team.name, - tableUpdate: tableUpdateResult + tableUpdateResult = 'League table updated successfully'; + console.log('✓ League table updated for league:', team.league.id); + } catch (error) { + console.error('Error fetching league table data:', error); + tableUpdateResult = 'League table update failed: ' + error.message; + // Don't fail the entire request if table update fails } - }); - } catch (error) { - console.error('Error in fetchTeamData:', error); - console.error('Error stack:', error.stack); - next(error); - } + + res.json({ + success: true, + message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`, + data: { + fetchedCount: result.fetchedCount, + teamName: team.name, + tableUpdate: tableUpdateResult + } + }); + } catch (error) { + console.error('Error in fetchTeamData:', error); + console.error('Error stack:', error.stack); + + // Update log with error information if we got far enough to build the URL + if (typeof myTischtennisUrl !== 'undefined' && account && team) { + const requestExecutionTime = Date.now() - requestStartTime; + 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) { + console.error('Error logging failed request:', logError); + } + } + + next(error); + } } /** diff --git a/backend/middleware/requestLoggingMiddleware.js b/backend/middleware/requestLoggingMiddleware.js index 2c69b5b..9a4dfaf 100644 --- a/backend/middleware/requestLoggingMiddleware.js +++ b/backend/middleware/requestLoggingMiddleware.js @@ -41,8 +41,8 @@ export const requestLoggingMiddleware = async (req, res, next) => { 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.) - // Nur Daten-Abrufe von API-Endpunkten werden geloggt // Exclude any endpoint containing 'status' or root paths if ( path.includes('/status') || @@ -54,6 +54,11 @@ export const requestLoggingMiddleware = async (req, res, next) => { 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; diff --git a/backend/services/apiLogService.js b/backend/services/apiLogService.js index 210af44..94d30c2 100644 --- a/backend/services/apiLogService.js +++ b/backend/services/apiLogService.js @@ -102,7 +102,13 @@ class ApiLogService { } if (path) { - where.path = { [Op.like]: `%${path}%` }; + // Handle "NOT:" prefix for excluding paths (e.g., "NOT:/mytischtennis") + if (path.startsWith('NOT:')) { + const excludePath = path.substring(4); + where.path = { [Op.not]: { [Op.like]: `%${excludePath}%` } }; + } else { + where.path = { [Op.like]: `%${path}%` }; + } } if (statusCode !== null) { diff --git a/backend/services/autoFetchMatchResultsService.js b/backend/services/autoFetchMatchResultsService.js index 212e3ab..09e5b85 100644 --- a/backend/services/autoFetchMatchResultsService.js +++ b/backend/services/autoFetchMatchResultsService.js @@ -1,5 +1,6 @@ import myTischtennisService from './myTischtennisService.js'; import myTischtennisFetchLogService from './myTischtennisFetchLogService.js'; +import apiLogService from './apiLogService.js'; import myTischtennisClient from '../clients/myTischtennisClient.js'; import MyTischtennis from '../models/MyTischtennis.js'; import ClubTeam from '../models/ClubTeam.js'; @@ -182,6 +183,14 @@ class AutoFetchMatchResultsService { * Fetch results for a specific team */ async fetchTeamResults(account, team) { + // Validate required data + if (!team.league) { + throw new Error('Team league is required'); + } + if (!team.league.season || !team.league.season.season) { + throw new Error('Team league season is required'); + } + const league = team.league; const season = league.season; @@ -207,26 +216,83 @@ class AutoFetchMatchResultsService { const playerStatsUrl = `https://www.mytischtennis.de/click-tt/${league.association}/${seasonStr}/ligen/${league.groupname}/gruppe/${league.myTischtennisGroupId}/mannschaft/${team.myTischtennisTeamId}/${teamnameEncoded}/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`; devLog(`Fetching player stats from: ${playerStatsUrl}`); + + const fetchStartTime = Date.now(); + let playerStatsResponse = null; + let fetchExecutionTime = 0; + let responseStatus = 0; + let responseBodyData = null; + let fetchError = null; - const playerStatsResponse = await fetch(playerStatsUrl, { - headers: { - 'Cookie': account.cookie || '', - 'Authorization': `Bearer ${account.accessToken}`, - 'Accept': 'application/json' + try { + playerStatsResponse = await fetch(playerStatsUrl, { + headers: { + 'Cookie': account.cookie || '', + 'Authorization': `Bearer ${account.accessToken}`, + 'Accept': 'application/json' + } + }); + + fetchExecutionTime = Date.now() - fetchStartTime; + responseStatus = playerStatsResponse.status; + + if (playerStatsResponse.ok) { + const playerStatsData = await playerStatsResponse.json(); + responseBodyData = playerStatsData; + + // Log complete response for debugging + console.log('=== PLAYER STATS RESPONSE START ==='); + console.log(JSON.stringify(playerStatsData, null, 2)); + console.log('=== PLAYER STATS RESPONSE END ==='); + + const playerCount = await this.processTeamData(team, playerStatsData); + totalProcessed += playerCount; + devLog(`Processed ${playerCount} player statistics`); + } else { + // Read error response body + try { + responseBodyData = await playerStatsResponse.text(); + } catch (e) { + responseBodyData = `Error reading response: ${e.message}`; + } } - }); - - if (playerStatsResponse.ok) { - const playerStatsData = await playerStatsResponse.json(); - - // Log complete response for debugging - console.log('=== PLAYER STATS RESPONSE START ==='); - console.log(JSON.stringify(playerStatsData, null, 2)); - console.log('=== PLAYER STATS RESPONSE END ==='); - - const playerCount = await this.processTeamData(team, playerStatsData); - totalProcessed += playerCount; - devLog(`Processed ${playerCount} player statistics`); + } catch (error) { + fetchExecutionTime = Date.now() - fetchStartTime; + fetchError = error.message || String(error); + responseStatus = 0; + console.error('Error fetching from myTischtennis:', error); + } + + // Log external request to myTischtennis - IMMER, auch bei Fehlern + try { + await apiLogService.logRequest({ + userId: account.userId, + method: 'GET', + path: playerStatsUrl.replace('https://www.mytischtennis.de', ''), // Relative path for consistency + statusCode: responseStatus || (fetchError ? 0 : 200), + requestBody: JSON.stringify({ + url: playerStatsUrl, + myTischtennisTeamId: team.myTischtennisTeamId, + clubTeamId: team.id, + teamName: team.name, + association: league.association, + groupname: league.groupname, + groupId: league.myTischtennisGroupId + }), + responseBody: responseBodyData ? (typeof responseBodyData === 'string' ? responseBodyData.substring(0, 10000) : JSON.stringify(responseBodyData).substring(0, 10000)) : null, + executionTime: fetchExecutionTime, + errorMessage: fetchError || (playerStatsResponse && !playerStatsResponse.ok ? `myTischtennis API returned ${responseStatus}` : null), + logType: 'api_request', + schedulerJobType: 'mytischtennis_fetch' + }); + } catch (logError) { + console.error('Error logging myTischtennis request:', logError); + // Don't throw - logging failures shouldn't break the main operation + } + + // Re-throw error if fetch failed + if (fetchError) { + throw new Error(`Failed to fetch from myTischtennis: ${fetchError}`); } // Note: Match results are already included in the player stats response above diff --git a/frontend/src/views/LogsView.vue b/frontend/src/views/LogsView.vue index eda727c..5cf1786 100644 --- a/frontend/src/views/LogsView.vue +++ b/frontend/src/views/LogsView.vue @@ -7,6 +7,15 @@
+
+ + +
+