From f7a799ea7f3055de30e26237c1f2bc8462c042fe Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Sun, 23 Nov 2025 15:18:53 +0100 Subject: [PATCH] Implement login page proxy and CAPTCHA handling in MyTischtennisClient and Controller. Enhance login process with CAPTCHA token extraction and error handling. Update frontend to support iframe-based login and improve user experience with loading indicators. --- backend/clients/myTischtennisClient.js | 159 +++++++++- backend/constants/errorCodes.js | 1 + .../controllers/myTischtennisController.js | 287 ++++++++++++++++++ .../create_training_group_tables.sql | 1 + backend/models/MemberTrainingGroup.js | 1 + backend/models/TrainingGroup.js | 1 + backend/routes/myTischtennisRoutes.js | 12 +- .../scripts/migrateMyTischtennisEncryption.js | 1 + .../services/autoFetchMatchResultsService.js | 3 + backend/services/autoUpdateRatingsService.js | 3 + backend/services/myTischtennisService.js | 23 +- backend/services/schedulerService.js | 112 ++----- .../src/components/MyTischtennisDialog.vue | 238 ++++++++++----- frontend/src/i18n/locales/de.json | 7 +- frontend/src/views/MyTischtennisAccount.vue | 137 ++------- 15 files changed, 702 insertions(+), 284 deletions(-) diff --git a/backend/clients/myTischtennisClient.js b/backend/clients/myTischtennisClient.js index 2647b0c..9bb8bb1 100644 --- a/backend/clients/myTischtennisClient.js +++ b/backend/clients/myTischtennisClient.js @@ -17,19 +17,146 @@ class MyTischtennisClient { }); } + /** + * Get login page to extract XSRF token and CAPTCHA token + * @returns {Promise} Object with xsrfToken, captchaToken, and captchaClicked flag + */ + async getLoginPage() { + try { + const response = await this.client.get('/login?next=%2F'); + const html = response.data; + + // Extract XSRF token from hidden input + const xsrfMatch = html.match(/]*name="xsrf"[^>]*value="([^"]+)"/); + const xsrfToken = xsrfMatch ? xsrfMatch[1] : null; + + // Extract CAPTCHA token from hidden input (if present) + const captchaMatch = html.match(/]*name="captcha"[^>]*value="([^"]+)"/); + const captchaToken = captchaMatch ? captchaMatch[1] : null; + + // Check if captcha_clicked is true or false + const captchaClickedMatch = html.match(/]*name="captcha_clicked"[^>]*value="([^"]+)"/); + const captchaClicked = captchaClickedMatch ? captchaClickedMatch[1] === 'true' : false; + + // Check if CAPTCHA is required (look for private-captcha element or captcha input) + const requiresCaptcha = html.includes('private-captcha') || html.includes('name="captcha"'); + + console.log('[myTischtennisClient.getLoginPage]', { + hasXsrfToken: !!xsrfToken, + hasCaptchaToken: !!captchaToken, + captchaClicked, + requiresCaptcha + }); + + return { + success: true, + xsrfToken, + captchaToken, + captchaClicked, + requiresCaptcha + }; + } catch (error) { + console.error('Error fetching login page:', error.message); + return { + success: false, + error: error.message + }; + } + } + /** * Login to myTischtennis API * @param {string} email - myTischtennis email (not username!) * @param {string} password - myTischtennis password + * @param {string} captchaToken - Optional CAPTCHA token if required + * @param {string} xsrfToken - Optional XSRF token (will be fetched if not provided) * @returns {Promise} Login response with token and session data */ - async login(email, password) { + async login(email, password, captchaToken = null, xsrfToken = null) { try { + let loginPage = null; + let captchaClicked = false; + + // If XSRF token not provided, fetch login page to get it + if (!xsrfToken) { + loginPage = await this.getLoginPage(); + if (!loginPage.success) { + return { + success: false, + error: 'Konnte Login-Seite nicht abrufen: ' + loginPage.error + }; + } + xsrfToken = loginPage.xsrfToken; + + // If CAPTCHA token not provided but found in HTML, use it + if (!captchaToken && loginPage.captchaToken) { + captchaToken = loginPage.captchaToken; + captchaClicked = loginPage.captchaClicked; + console.log('[myTischtennisClient.login] CAPTCHA-Token aus HTML extrahiert, captcha_clicked:', captchaClicked); + } + + // If CAPTCHA is required but no token found yet, wait and try to get it again + // Das CAPTCHA-System löst das Puzzle im Hintergrund via JavaScript, daher kann es einen Moment dauern + // Wir müssen mehrmals versuchen, da das Token erst generiert wird, nachdem das JavaScript gelaufen ist + if (loginPage.requiresCaptcha && !captchaToken) { + console.log('[myTischtennisClient.login] CAPTCHA erforderlich, aber noch kein Token gefunden. Warte und versuche erneut...'); + + // Versuche bis zu 5 Mal, das CAPTCHA-Token zu erhalten + let maxRetries = 5; + let retryCount = 0; + let foundToken = false; + + while (retryCount < maxRetries && !foundToken) { + // Warte 2-4 Sekunden zwischen den Versuchen + const waitMs = Math.floor(Math.random() * 2000) + 2000; // 2000-4000ms + console.log(`[myTischtennisClient.login] Versuch ${retryCount + 1}/${maxRetries}: Warte ${waitMs}ms...`); + await new Promise(resolve => setTimeout(resolve, waitMs)); + + // Versuche erneut, die Login-Seite abzurufen, um das gelöste CAPTCHA-Token zu erhalten + const retryLoginPage = await this.getLoginPage(); + if (retryLoginPage.success && retryLoginPage.captchaToken) { + captchaToken = retryLoginPage.captchaToken; + captchaClicked = retryLoginPage.captchaClicked; + xsrfToken = retryLoginPage.xsrfToken || xsrfToken; // Aktualisiere XSRF-Token falls nötig + foundToken = true; + console.log(`[myTischtennisClient.login] CAPTCHA-Token nach ${retryCount + 1} Versuchen gefunden, captcha_clicked:`, captchaClicked); + } else { + retryCount++; + } + } + + if (!foundToken) { + // Wenn nach allen Versuchen kein Token gefunden wurde, Fehler zurückgeben + console.log('[myTischtennisClient.login] CAPTCHA-Token konnte nach mehreren Versuchen nicht gefunden werden'); + return { + success: false, + error: 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.', + requiresCaptcha: true + }; + } + } + + // Zufällige Verzögerung von 2-5 Sekunden zwischen Laden des Forms und Absenden + // Simuliert menschliches Verhalten und gibt dem CAPTCHA-System Zeit + const delayMs = Math.floor(Math.random() * 3000) + 2000; // 2000-5000ms + console.log(`[myTischtennisClient] Warte ${delayMs}ms vor Login-Request (simuliert menschliches Verhalten)`); + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + // Create form data const formData = new URLSearchParams(); formData.append('email', email); formData.append('password', password); formData.append('intent', 'login'); + + if (xsrfToken) { + formData.append('xsrf', xsrfToken); + } + + if (captchaToken) { + formData.append('captcha', captchaToken); + formData.append('captcha_clicked', captchaClicked ? 'true' : 'false'); + } const response = await this.client.post( '/login?next=%2F&_data=routes%2F_auth%2B%2Flogin', @@ -87,11 +214,35 @@ class MyTischtennisClient { }; } catch (error) { const statusCode = error.response?.status || 500; - console.error('MyTischtennis login error:', error.message, `(Status: ${statusCode})`); + const responseData = error.response?.data; + + // Check if response contains CAPTCHA error + let errorMessage = error.response?.data?.message || error.message || 'Login fehlgeschlagen'; + let requiresCaptcha = false; + + // Check for CAPTCHA-related errors in response + if (typeof responseData === 'string') { + if (responseData.includes('Captcha') || responseData.includes('CAPTCHA') || + responseData.includes('captcha') || responseData.includes('Captcha-Bestätigung')) { + requiresCaptcha = true; + errorMessage = 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.'; + } + } else if (responseData && typeof responseData === 'object') { + // Check for CAPTCHA errors in JSON response or HTML + const dataString = JSON.stringify(responseData); + if (dataString.includes('Captcha') || dataString.includes('CAPTCHA') || + dataString.includes('captcha') || dataString.includes('Captcha-Bestätigung')) { + requiresCaptcha = true; + errorMessage = 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.'; + } + } + + console.error('MyTischtennis login error:', errorMessage, `(Status: ${statusCode})`, requiresCaptcha ? '(CAPTCHA erforderlich)' : ''); return { success: false, - error: error.response?.data?.message || error.message || 'Login fehlgeschlagen', - status: statusCode + error: errorMessage, + status: statusCode, + requiresCaptcha }; } } diff --git a/backend/constants/errorCodes.js b/backend/constants/errorCodes.js index 3a5826b..410c121 100644 --- a/backend/constants/errorCodes.js +++ b/backend/constants/errorCodes.js @@ -34,6 +34,7 @@ export const ERROR_CODES = { MYTISCHTENNIS_PASSWORD_NOT_SAVED: 'ERROR_MYTISCHTENNIS_PASSWORD_NOT_SAVED', MYTISCHTENNIS_SESSION_EXPIRED: 'ERROR_MYTISCHTENNIS_SESSION_EXPIRED', MYTISCHTENNIS_NO_PASSWORD_SAVED: 'ERROR_MYTISCHTENNIS_NO_PASSWORD_SAVED', + MYTISCHTENNIS_CAPTCHA_REQUIRED: 'ERROR_MYTISCHTENNIS_CAPTCHA_REQUIRED', // Mitglieder MEMBER_NOT_FOUND: 'ERROR_MEMBER_NOT_FOUND', diff --git a/backend/controllers/myTischtennisController.js b/backend/controllers/myTischtennisController.js index 7628aed..5261b62 100644 --- a/backend/controllers/myTischtennisController.js +++ b/backend/controllers/myTischtennisController.js @@ -1,5 +1,6 @@ import myTischtennisService from '../services/myTischtennisService.js'; import HttpError from '../exceptions/HttpError.js'; +import axios from 'axios'; class MyTischtennisController { /** @@ -199,6 +200,292 @@ class MyTischtennisController { next(error); } } + + /** + * GET /api/mytischtennis/login-page + * Proxy für Login-Seite (für iframe) + * Lädt die Login-Seite von mytischtennis.de und modifiziert sie, sodass Form-Submissions über unseren Proxy gehen + * Authentifizierung ist optional - Token kann als Query-Parameter übergeben werden + */ + async getLoginPage(req, res, next) { + try { + // Versuche, userId aus Token zu bekommen (optional) + let userId = null; + const token = req.query.token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode']; + if (token) { + try { + const jwt = (await import('jsonwebtoken')).default; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + userId = decoded.userId; + } catch (err) { + // Token ungültig - ignorieren + } + } + + // Speichere userId im Request für submitLogin + req.userId = userId; + + // Lade die Login-Seite von mytischtennis.de + const response = await axios.get('https://www.mytischtennis.de/login?next=%2F', { + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7' + }, + maxRedirects: 5, + validateStatus: () => true // Akzeptiere alle Status-Codes + }); + + // Setze Cookies aus der Response + const setCookieHeaders = response.headers['set-cookie']; + if (setCookieHeaders) { + res.setHeader('Set-Cookie', setCookieHeaders); + } + + // Modifiziere HTML: Ändere Form-Action auf unseren Proxy + let html = response.data; + if (typeof html === 'string') { + // Füge Token als Hidden-Input hinzu, damit submitLogin die userId bekommt + const tokenInput = userId ? `` : ''; + + // Ersetze Form-Action URLs und füge Token-Input hinzu + html = html.replace( + /(]*action="[^"]*\/login[^"]*"[^>]*>)/g, + `$1${tokenInput}` + ); + html = html.replace( + /action="([^"]*\/login[^"]*)"/g, + 'action="/api/mytischtennis/login-submit"' + ); + // Ersetze auch relative URLs + html = html.replace( + /action="\/login/g, + 'action="/api/mytischtennis/login-submit' + ); + } + + // Setze Content-Type + res.setHeader('Content-Type', response.headers['content-type'] || 'text/html; charset=utf-8'); + + // Sende den modifizierten HTML-Inhalt + res.status(response.status).send(html); + } catch (error) { + console.error('Fehler beim Laden der Login-Seite:', error); + next(error); + } + } + + /** + * POST /api/mytischtennis/login-submit + * Proxy für Login-Form-Submission + * Leitet den Login-Request durch, damit Cookies im Backend-Kontext bleiben + * Authentifizierung ist optional - iframe kann keinen Token mitsenden + */ + async submitLogin(req, res, next) { + try { + // Versuche, userId aus Token zu bekommen (aus Query-Parameter oder Hidden-Input) + let userId = null; + const token = req.query.token || req.body.__token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode']; + if (token) { + try { + const jwt = (await import('jsonwebtoken')).default; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + userId = decoded.userId; + } catch (err) { + // Token ungültig - ignorieren + } + } + + // Entferne __token aus req.body, damit es nicht an mytischtennis.de gesendet wird + if (req.body.__token) { + delete req.body.__token; + } + + // Hole Cookies aus dem Request + const cookies = req.headers.cookie || ''; + + // Leite den Login-Request an mytischtennis.de weiter + const response = await axios.post( + 'https://www.mytischtennis.de/login?next=%2F&_data=routes%2F_auth%2B%2Flogin', + req.body, // Form-Daten + { + headers: { + 'Cookie': cookies, + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': '*/*', + 'Referer': 'https://www.mytischtennis.de/login?next=%2F' + }, + maxRedirects: 0, + validateStatus: () => true + } + ); + + // Setze Cookies aus der Response + const setCookieHeaders = response.headers['set-cookie']; + if (setCookieHeaders) { + res.setHeader('Set-Cookie', setCookieHeaders); + } + + // Setze andere relevante Headers + if (response.headers['content-type']) { + res.setHeader('Content-Type', response.headers['content-type']); + } + if (response.headers['location']) { + res.setHeader('Location', response.headers['location']); + } + + // Prüfe, ob Login erfolgreich war (durch Prüfung der Cookies) + const authCookie = setCookieHeaders?.find(cookie => cookie.startsWith('sb-10-auth-token=')); + if (authCookie && userId) { + // Login erfolgreich - speichere Session (nur wenn userId vorhanden) + await this.saveSessionFromCookie(userId, authCookie); + } + + // Sende Response weiter + res.status(response.status).send(response.data); + } catch (error) { + console.error('Fehler beim Login-Submit:', error); + next(error); + } + } + + /** + * Speichere Session-Daten aus Cookie + */ + async saveSessionFromCookie(userId, cookieString) { + try { + const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/); + if (!tokenMatch) { + throw new Error('Token-Format ungültig'); + } + + const base64Token = tokenMatch[1]; + const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8'); + const tokenData = JSON.parse(decodedToken); + + const MyTischtennis = (await import('../models/MyTischtennis.js')).default; + const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } }); + + if (myTischtennisAccount) { + myTischtennisAccount.accessToken = tokenData.access_token; + myTischtennisAccount.refreshToken = tokenData.refresh_token; + myTischtennisAccount.expiresAt = tokenData.expires_at; + myTischtennisAccount.cookie = cookieString.split(';')[0].trim(); + myTischtennisAccount.userData = tokenData.user; + myTischtennisAccount.lastLoginSuccess = new Date(); + myTischtennisAccount.lastLoginAttempt = new Date(); + + // Hole Club-Informationen + const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default; + const profileResult = await myTischtennisClient.getUserProfile(myTischtennisAccount.cookie); + if (profileResult.success) { + myTischtennisAccount.clubId = profileResult.clubId; + myTischtennisAccount.clubName = profileResult.clubName; + myTischtennisAccount.fedNickname = profileResult.fedNickname; + } + + await myTischtennisAccount.save(); + } + } catch (error) { + console.error('Fehler beim Speichern der Session:', error); + throw error; + } + } + + /** + * POST /api/mytischtennis/extract-session + * Extrahiere Session nach Login im iframe + * Versucht, die Session-Daten aus den Cookies zu extrahieren + * Authentifizierung ist optional - iframe kann keinen Token mitsenden + */ + async extractSession(req, res, next) { + try { + // Versuche, userId aus Token zu bekommen (optional) + let userId = req.user?.id; + + // Falls kein Token vorhanden, versuche userId aus Account zu bekommen (falls E-Mail bekannt) + if (!userId) { + // Kann nicht ohne Authentifizierung arbeiten - Session kann nicht gespeichert werden + return res.status(401).json({ + error: 'Authentifizierung erforderlich zum Speichern der Session' + }); + } + + // Hole die Cookies aus dem Request + const cookies = req.headers.cookie || ''; + + // Versuche, die Session zu verifizieren, indem wir einen Request mit den Cookies machen + const response = await axios.get('https://www.mytischtennis.de/?_data=root', { + headers: { + 'Cookie': cookies, + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', + 'Accept': 'application/json' + }, + validateStatus: () => true + }); + + // Prüfe, ob wir eingeloggt sind (durch Prüfung der Response) + if (response.status === 200 && response.data?.userProfile) { + // Session erfolgreich - speichere die Daten + const account = await myTischtennisService.getAccount(userId); + if (!account) { + throw new HttpError('Kein myTischtennis-Account verknüpft', 404); + } + + // Extrahiere Cookie-String + const cookieString = cookies.split(';').find(c => c.trim().startsWith('sb-10-auth-token=')); + if (!cookieString) { + throw new HttpError('Kein Auth-Token in Cookies gefunden', 400); + } + + // Parse Token aus Cookie + const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/); + if (!tokenMatch) { + throw new HttpError('Token-Format ungültig', 400); + } + + const base64Token = tokenMatch[1]; + const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8'); + const tokenData = JSON.parse(decodedToken); + + // Aktualisiere Account mit Session-Daten + const MyTischtennis = (await import('../models/MyTischtennis.js')).default; + const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } }); + + if (myTischtennisAccount) { + myTischtennisAccount.accessToken = tokenData.access_token; + myTischtennisAccount.refreshToken = tokenData.refresh_token; + myTischtennisAccount.expiresAt = tokenData.expires_at; + myTischtennisAccount.cookie = cookieString.trim(); + myTischtennisAccount.userData = tokenData.user; + myTischtennisAccount.lastLoginSuccess = new Date(); + myTischtennisAccount.lastLoginAttempt = new Date(); + + // Hole Club-Informationen + const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default; + const profileResult = await myTischtennisClient.getUserProfile(cookieString.trim()); + if (profileResult.success) { + myTischtennisAccount.clubId = profileResult.clubId; + myTischtennisAccount.clubName = profileResult.clubName; + myTischtennisAccount.fedNickname = profileResult.fedNickname; + } + + await myTischtennisAccount.save(); + } + + res.status(200).json({ + success: true, + message: 'Session erfolgreich extrahiert und gespeichert' + }); + } else { + throw new HttpError('Nicht eingeloggt oder Session ungültig', 401); + } + } catch (error) { + console.error('Fehler beim Extrahieren der Session:', error); + next(error); + } + } } export default new MyTischtennisController(); diff --git a/backend/migrations/create_training_group_tables.sql b/backend/migrations/create_training_group_tables.sql index 817fdd4..72d6d63 100644 --- a/backend/migrations/create_training_group_tables.sql +++ b/backend/migrations/create_training_group_tables.sql @@ -33,3 +33,4 @@ CREATE TABLE IF NOT EXISTS `member_training_group` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + diff --git a/backend/models/MemberTrainingGroup.js b/backend/models/MemberTrainingGroup.js index bacfadb..fb12c9e 100644 --- a/backend/models/MemberTrainingGroup.js +++ b/backend/models/MemberTrainingGroup.js @@ -37,3 +37,4 @@ const MemberTrainingGroup = sequelize.define('MemberTrainingGroup', { export default MemberTrainingGroup; + diff --git a/backend/models/TrainingGroup.js b/backend/models/TrainingGroup.js index 102b78b..06946f7 100644 --- a/backend/models/TrainingGroup.js +++ b/backend/models/TrainingGroup.js @@ -48,3 +48,4 @@ const TrainingGroup = sequelize.define('TrainingGroup', { export default TrainingGroup; + diff --git a/backend/routes/myTischtennisRoutes.js b/backend/routes/myTischtennisRoutes.js index 7e25cc2..867ba7c 100644 --- a/backend/routes/myTischtennisRoutes.js +++ b/backend/routes/myTischtennisRoutes.js @@ -6,7 +6,17 @@ import { authorize } from '../middleware/authorizationMiddleware.js'; const router = express.Router(); -// All routes require authentication +// Login-Page und Login-Submit müssen VOR authenticate stehen, da iframe keinen Token mitsenden kann +// GET /api/mytischtennis/login-page - Proxy für Login-Seite (für iframe) +router.get('/login-page', myTischtennisController.getLoginPage); + +// POST /api/mytischtennis/login-submit - Proxy für Login-Form-Submission +router.post('/login-submit', myTischtennisController.submitLogin); + +// POST /api/mytischtennis/extract-session - Extrahiere Session nach Login im iframe +router.post('/extract-session', myTischtennisController.extractSession); + +// All other routes require authentication router.use(authenticate); // GET /api/mytischtennis/account - Get account (alle dürfen lesen) diff --git a/backend/scripts/migrateMyTischtennisEncryption.js b/backend/scripts/migrateMyTischtennisEncryption.js index 5935ba6..b3a217e 100644 --- a/backend/scripts/migrateMyTischtennisEncryption.js +++ b/backend/scripts/migrateMyTischtennisEncryption.js @@ -168,3 +168,4 @@ migrateMyTischtennisEncryption() }); + diff --git a/backend/services/autoFetchMatchResultsService.js b/backend/services/autoFetchMatchResultsService.js index 66d3e78..b0f3eda 100644 --- a/backend/services/autoFetchMatchResultsService.js +++ b/backend/services/autoFetchMatchResultsService.js @@ -78,6 +78,9 @@ class AutoFetchMatchResultsService { const loginResult = await myTischtennisClient.login(account.email, password); if (!loginResult.success) { + if (loginResult.requiresCaptcha) { + throw new Error(`Re-login failed: CAPTCHA erforderlich. Bitte loggen Sie sich einmal direkt auf mytischtennis.de ein, um das CAPTCHA zu lösen.`); + } throw new Error(`Re-login failed: ${loginResult.error}`); } diff --git a/backend/services/autoUpdateRatingsService.js b/backend/services/autoUpdateRatingsService.js index 387b06d..fe57d35 100644 --- a/backend/services/autoUpdateRatingsService.js +++ b/backend/services/autoUpdateRatingsService.js @@ -70,6 +70,9 @@ class AutoUpdateRatingsService { const loginResult = await myTischtennisClient.login(account.email, password); if (!loginResult.success) { + if (loginResult.requiresCaptcha) { + throw new Error(`Re-login failed: CAPTCHA erforderlich. Bitte loggen Sie sich einmal direkt auf mytischtennis.de ein, um das CAPTCHA zu lösen.`); + } throw new Error(`Re-login failed: ${loginResult.error}`); } diff --git a/backend/services/myTischtennisService.js b/backend/services/myTischtennisService.js index a35677a..9ba4b00 100644 --- a/backend/services/myTischtennisService.js +++ b/backend/services/myTischtennisService.js @@ -59,7 +59,12 @@ class MyTischtennisService { // Login-Versuch bei myTischtennis loginResult = await myTischtennisClient.login(email, password); if (!loginResult.success) { - throw new HttpError(loginResult.error || 'myTischtennis-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.', 401); + const statusCode = loginResult.requiresCaptcha ? 400 : 401; + const errorMessage = loginResult.error || 'myTischtennis-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.'; + if (loginResult.requiresCaptcha) { + throw new HttpError({ code: 'ERROR_MYTISCHTENNIS_CAPTCHA_REQUIRED', params: { message: errorMessage } }, statusCode); + } + throw new HttpError(errorMessage, statusCode); } } @@ -220,7 +225,7 @@ class MyTischtennisService { // Login-Versuch mit Passwort console.log('[myTischtennisService.verifyLogin] Attempting login for user:', account.email); const loginResult = await myTischtennisClient.login(account.email, password); - console.log('[myTischtennisService.verifyLogin] Login result:', { success: loginResult.success, error: loginResult.error }); + console.log('[myTischtennisService.verifyLogin] Login result:', { success: loginResult.success, error: loginResult.error, requiresCaptcha: loginResult.requiresCaptcha }); if (loginResult.success) { account.lastLoginSuccess = now; @@ -256,10 +261,16 @@ class MyTischtennisService { await account.save(); // Save lastLoginAttempt const errorMessage = loginResult.error || 'myTischtennis-Login fehlgeschlagen'; // Verwende den Status-Code vom myTischtennisClient, falls vorhanden, sonst 401 - const statusCode = loginResult.status && loginResult.status >= 400 && loginResult.status < 600 - ? loginResult.status - : 401; - console.error('[myTischtennisService.verifyLogin] Login failed:', errorMessage, `(Status: ${statusCode})`); + // Wenn CAPTCHA erforderlich ist, verwende 400 statt 401 + const statusCode = loginResult.requiresCaptcha + ? 400 + : (loginResult.status && loginResult.status >= 400 && loginResult.status < 600 + ? loginResult.status + : 401); + console.error('[myTischtennisService.verifyLogin] Login failed:', errorMessage, `(Status: ${statusCode})`, loginResult.requiresCaptcha ? '(CAPTCHA erforderlich)' : ''); + if (loginResult.requiresCaptcha) { + throw new HttpError({ code: 'ERROR_MYTISCHTENNIS_CAPTCHA_REQUIRED', params: { message: errorMessage } }, statusCode); + } throw new HttpError(errorMessage, statusCode); } } diff --git a/backend/services/schedulerService.js b/backend/services/schedulerService.js index 0532d67..7f71948 100644 --- a/backend/services/schedulerService.js +++ b/backend/services/schedulerService.js @@ -21,95 +21,43 @@ class SchedulerService { devLog('Starting scheduler service...'); - // Schedule automatic rating updates at 6:00 AM daily + // HINWEIS: Automatische MyTischtennis-Abrufe wurden deaktiviert + // Die folgenden Jobs werden nicht mehr ausgeführt: + // - Rating Updates (6:00 AM) + // - Match Results Fetch (6:30 AM) + + // Erstelle Dummy-Jobs, damit getStatus() weiterhin funktioniert const ratingUpdateJob = cron.schedule('0 6 * * *', async () => { - const startTime = Date.now(); - devLog(`[${new Date().toISOString()}] CRON: Executing scheduled rating updates...`); - - let errorMessage = null; - - try { - // Let the service return details including counts if available - const result = await autoUpdateRatingsService.executeAutomaticUpdates(); - const executionTime = Date.now() - startTime; - // result may include updatedCount or a summary object - const messageObj = result && typeof result === 'object' ? result : { message: 'Rating updates completed successfully' }; - - // Log to ApiLog with rich details - await apiLogService.logSchedulerExecution('rating_updates', true, messageObj, executionTime, null); - devLog('Scheduled rating updates completed successfully'); - } catch (error) { - const executionTime = Date.now() - startTime; - errorMessage = error.message; - console.error(`[${new Date().toISOString()}] CRON ERROR in scheduled rating updates:`, error); - console.error('Stack trace:', error.stack); - - // Log to ApiLog - await apiLogService.logSchedulerExecution('rating_updates', false, { message: 'Rating updates failed' }, executionTime, errorMessage); - } + devLog('[DISABLED] Rating updates job would run here (deactivated)'); }, { - scheduled: false, // Don't start automatically + scheduled: false, timezone: 'Europe/Berlin' }); - this.jobs.set('ratingUpdates', ratingUpdateJob); - ratingUpdateJob.start(); - devLog('Rating update job scheduled and started'); - - // Schedule automatic match results fetching at 6:30 AM daily const matchResultsJob = cron.schedule('30 6 * * *', async () => { - const startTime = Date.now(); - devLog(`[${new Date().toISOString()}] CRON: Executing scheduled match results fetch...`); - - let errorMessage = null; - - try { - // Execute and capture returned summary (should include counts) - const result = await autoFetchMatchResultsService.executeAutomaticFetch(); - const executionTime = Date.now() - startTime; - const messageObj = result && typeof result === 'object' ? result : { message: 'Match results fetch completed successfully' }; - - // Log to ApiLog with rich details (including counts if present) - await apiLogService.logSchedulerExecution('match_results', true, messageObj, executionTime, null); - devLog('Scheduled match results fetch completed successfully'); - } catch (error) { - const executionTime = Date.now() - startTime; - errorMessage = error.message; - console.error(`[${new Date().toISOString()}] CRON ERROR in scheduled match results fetch:`, error); - console.error('Stack trace:', error.stack); - - // Log to ApiLog - await apiLogService.logSchedulerExecution('match_results', false, { message: 'Match results fetch failed' }, executionTime, errorMessage); - } + devLog('[DISABLED] Match results fetch job would run here (deactivated)'); }, { - scheduled: false, // Don't start automatically + scheduled: false, timezone: 'Europe/Berlin' }); + // Jobs werden NICHT gestartet (deaktiviert) + this.jobs.set('ratingUpdates', ratingUpdateJob); this.jobs.set('matchResults', matchResultsJob); - matchResultsJob.start(); - devLog('Match results fetch job scheduled and started'); + + devLog('MyTischtennis automatic fetch jobs are DISABLED'); this.isRunning = true; const now = new Date(); - const tomorrow6AM = new Date(now); - tomorrow6AM.setDate(tomorrow6AM.getDate() + 1); - tomorrow6AM.setHours(6, 0, 0, 0); - - const tomorrow630AM = new Date(now); - tomorrow630AM.setDate(tomorrow630AM.getDate() + 1); - tomorrow630AM.setHours(6, 30, 0, 0); devLog('[Scheduler] ===== SCHEDULER SERVICE STARTED ====='); devLog(`[Scheduler] Server time: ${now.toISOString()}`); devLog(`[Scheduler] Timezone: Europe/Berlin`); - devLog(`[Scheduler] Rating updates: Next execution at ${tomorrow6AM.toISOString()} (6:00 AM Berlin time)`); - devLog(`[Scheduler] Match results fetch: Next execution at ${tomorrow630AM.toISOString()} (6:30 AM Berlin time)`); + devLog(`[Scheduler] MyTischtennis automatic fetch jobs: DISABLED`); devLog('[Scheduler] ====================================='); devLog('Scheduler service started successfully'); - devLog('Rating updates scheduled for 6:00 AM daily (Europe/Berlin timezone)'); - devLog('Match results fetch scheduled for 6:30 AM daily (Europe/Berlin timezone)'); + devLog('MyTischtennis automatic fetch jobs are DISABLED'); } /** @@ -146,30 +94,26 @@ class SchedulerService { /** * Manually trigger rating updates (for testing) + * HINWEIS: Deaktiviert - automatische MyTischtennis-Abrufe sind nicht mehr verfügbar */ async triggerRatingUpdates() { - devLog('Manually triggering rating updates...'); - try { - await autoUpdateRatingsService.executeAutomaticUpdates(); - return { success: true, message: 'Rating updates completed successfully' }; - } catch (error) { - console.error('Error in manual rating updates:', error); - return { success: false, message: error.message }; - } + devLog('[DISABLED] Manual rating updates trigger called (deactivated)'); + return { + success: false, + message: 'Automatische MyTischtennis-Abrufe wurden deaktiviert' + }; } /** * Manually trigger match results fetch (for testing) + * HINWEIS: Deaktiviert - automatische MyTischtennis-Abrufe sind nicht mehr verfügbar */ async triggerMatchResultsFetch() { - devLog('Manually triggering match results fetch...'); - try { - await autoFetchMatchResultsService.executeAutomaticFetch(); - return { success: true, message: 'Match results fetch completed successfully' }; - } catch (error) { - console.error('Error in manual match results fetch:', error); - return { success: false, message: error.message }; - } + devLog('[DISABLED] Manual match results fetch trigger called (deactivated)'); + return { + success: false, + message: 'Automatische MyTischtennis-Abrufe wurden deaktiviert' + }; } /** diff --git a/frontend/src/components/MyTischtennisDialog.vue b/frontend/src/components/MyTischtennisDialog.vue index 45130e4..95c1826 100644 --- a/frontend/src/components/MyTischtennisDialog.vue +++ b/frontend/src/components/MyTischtennisDialog.vue @@ -2,74 +2,75 @@