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.
This commit is contained in:
@@ -17,19 +17,146 @@ class MyTischtennisClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get login page to extract XSRF token and CAPTCHA token
|
||||
* @returns {Promise<Object>} Object with xsrfToken, captchaToken, and captchaClicked flag
|
||||
*/
|
||||
async getLoginPage() {
|
||||
try {
|
||||
const response = await this.client.get('/login?next=%2F');
|
||||
const html = response.data;
|
||||
|
||||
// Extract XSRF token from hidden input
|
||||
const xsrfMatch = html.match(/<input[^>]*name="xsrf"[^>]*value="([^"]+)"/);
|
||||
const xsrfToken = xsrfMatch ? xsrfMatch[1] : null;
|
||||
|
||||
// Extract CAPTCHA token from hidden input (if present)
|
||||
const captchaMatch = html.match(/<input[^>]*name="captcha"[^>]*value="([^"]+)"/);
|
||||
const captchaToken = captchaMatch ? captchaMatch[1] : null;
|
||||
|
||||
// Check if captcha_clicked is true or false
|
||||
const captchaClickedMatch = html.match(/<input[^>]*name="captcha_clicked"[^>]*value="([^"]+)"/);
|
||||
const captchaClicked = captchaClickedMatch ? captchaClickedMatch[1] === 'true' : false;
|
||||
|
||||
// Check if CAPTCHA is required (look for private-captcha element or captcha input)
|
||||
const requiresCaptcha = html.includes('private-captcha') || html.includes('name="captcha"');
|
||||
|
||||
console.log('[myTischtennisClient.getLoginPage]', {
|
||||
hasXsrfToken: !!xsrfToken,
|
||||
hasCaptchaToken: !!captchaToken,
|
||||
captchaClicked,
|
||||
requiresCaptcha
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
xsrfToken,
|
||||
captchaToken,
|
||||
captchaClicked,
|
||||
requiresCaptcha
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching login page:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login to myTischtennis API
|
||||
* @param {string} email - myTischtennis email (not username!)
|
||||
* @param {string} password - myTischtennis password
|
||||
* @param {string} captchaToken - Optional CAPTCHA token if required
|
||||
* @param {string} xsrfToken - Optional XSRF token (will be fetched if not provided)
|
||||
* @returns {Promise<Object>} Login response with token and session data
|
||||
*/
|
||||
async login(email, password) {
|
||||
async login(email, password, captchaToken = null, xsrfToken = null) {
|
||||
try {
|
||||
let loginPage = null;
|
||||
let captchaClicked = false;
|
||||
|
||||
// If XSRF token not provided, fetch login page to get it
|
||||
if (!xsrfToken) {
|
||||
loginPage = await this.getLoginPage();
|
||||
if (!loginPage.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Konnte Login-Seite nicht abrufen: ' + loginPage.error
|
||||
};
|
||||
}
|
||||
xsrfToken = loginPage.xsrfToken;
|
||||
|
||||
// If CAPTCHA token not provided but found in HTML, use it
|
||||
if (!captchaToken && loginPage.captchaToken) {
|
||||
captchaToken = loginPage.captchaToken;
|
||||
captchaClicked = loginPage.captchaClicked;
|
||||
console.log('[myTischtennisClient.login] CAPTCHA-Token aus HTML extrahiert, captcha_clicked:', captchaClicked);
|
||||
}
|
||||
|
||||
// If CAPTCHA is required but no token found yet, wait and try to get it again
|
||||
// Das CAPTCHA-System löst das Puzzle im Hintergrund via JavaScript, daher kann es einen Moment dauern
|
||||
// Wir müssen mehrmals versuchen, da das Token erst generiert wird, nachdem das JavaScript gelaufen ist
|
||||
if (loginPage.requiresCaptcha && !captchaToken) {
|
||||
console.log('[myTischtennisClient.login] CAPTCHA erforderlich, aber noch kein Token gefunden. Warte und versuche erneut...');
|
||||
|
||||
// Versuche bis zu 5 Mal, das CAPTCHA-Token zu erhalten
|
||||
let maxRetries = 5;
|
||||
let retryCount = 0;
|
||||
let foundToken = false;
|
||||
|
||||
while (retryCount < maxRetries && !foundToken) {
|
||||
// Warte 2-4 Sekunden zwischen den Versuchen
|
||||
const waitMs = Math.floor(Math.random() * 2000) + 2000; // 2000-4000ms
|
||||
console.log(`[myTischtennisClient.login] Versuch ${retryCount + 1}/${maxRetries}: Warte ${waitMs}ms...`);
|
||||
await new Promise(resolve => setTimeout(resolve, waitMs));
|
||||
|
||||
// Versuche erneut, die Login-Seite abzurufen, um das gelöste CAPTCHA-Token zu erhalten
|
||||
const retryLoginPage = await this.getLoginPage();
|
||||
if (retryLoginPage.success && retryLoginPage.captchaToken) {
|
||||
captchaToken = retryLoginPage.captchaToken;
|
||||
captchaClicked = retryLoginPage.captchaClicked;
|
||||
xsrfToken = retryLoginPage.xsrfToken || xsrfToken; // Aktualisiere XSRF-Token falls nötig
|
||||
foundToken = true;
|
||||
console.log(`[myTischtennisClient.login] CAPTCHA-Token nach ${retryCount + 1} Versuchen gefunden, captcha_clicked:`, captchaClicked);
|
||||
} else {
|
||||
retryCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundToken) {
|
||||
// Wenn nach allen Versuchen kein Token gefunden wurde, Fehler zurückgeben
|
||||
console.log('[myTischtennisClient.login] CAPTCHA-Token konnte nach mehreren Versuchen nicht gefunden werden');
|
||||
return {
|
||||
success: false,
|
||||
error: 'CAPTCHA erforderlich. Bitte lösen Sie das CAPTCHA auf der MyTischtennis-Website.',
|
||||
requiresCaptcha: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Zufällige Verzögerung von 2-5 Sekunden zwischen Laden des Forms und Absenden
|
||||
// Simuliert menschliches Verhalten und gibt dem CAPTCHA-System Zeit
|
||||
const delayMs = Math.floor(Math.random() * 3000) + 2000; // 2000-5000ms
|
||||
console.log(`[myTischtennisClient] Warte ${delayMs}ms vor Login-Request (simuliert menschliches Verhalten)`);
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
|
||||
// Create form data
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('email', email);
|
||||
formData.append('password', password);
|
||||
formData.append('intent', 'login');
|
||||
|
||||
if (xsrfToken) {
|
||||
formData.append('xsrf', xsrfToken);
|
||||
}
|
||||
|
||||
if (captchaToken) {
|
||||
formData.append('captcha', captchaToken);
|
||||
formData.append('captcha_clicked', captchaClicked ? 'true' : 'false');
|
||||
}
|
||||
|
||||
const response = await this.client.post(
|
||||
'/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import myTischtennisService from '../services/myTischtennisService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import axios from 'axios';
|
||||
|
||||
class MyTischtennisController {
|
||||
/**
|
||||
@@ -199,6 +200,292 @@ class MyTischtennisController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mytischtennis/login-page
|
||||
* Proxy für Login-Seite (für iframe)
|
||||
* Lädt die Login-Seite von mytischtennis.de und modifiziert sie, sodass Form-Submissions über unseren Proxy gehen
|
||||
* Authentifizierung ist optional - Token kann als Query-Parameter übergeben werden
|
||||
*/
|
||||
async getLoginPage(req, res, next) {
|
||||
try {
|
||||
// Versuche, userId aus Token zu bekommen (optional)
|
||||
let userId = null;
|
||||
const token = req.query.token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode'];
|
||||
if (token) {
|
||||
try {
|
||||
const jwt = (await import('jsonwebtoken')).default;
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
userId = decoded.userId;
|
||||
} catch (err) {
|
||||
// Token ungültig - ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Speichere userId im Request für submitLogin
|
||||
req.userId = userId;
|
||||
|
||||
// Lade die Login-Seite von mytischtennis.de
|
||||
const response = await axios.get('https://www.mytischtennis.de/login?next=%2F', {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
|
||||
},
|
||||
maxRedirects: 5,
|
||||
validateStatus: () => true // Akzeptiere alle Status-Codes
|
||||
});
|
||||
|
||||
// Setze Cookies aus der Response
|
||||
const setCookieHeaders = response.headers['set-cookie'];
|
||||
if (setCookieHeaders) {
|
||||
res.setHeader('Set-Cookie', setCookieHeaders);
|
||||
}
|
||||
|
||||
// Modifiziere HTML: Ändere Form-Action auf unseren Proxy
|
||||
let html = response.data;
|
||||
if (typeof html === 'string') {
|
||||
// Füge Token als Hidden-Input hinzu, damit submitLogin die userId bekommt
|
||||
const tokenInput = userId ? `<input type="hidden" name="__token" value="${token}" />` : '';
|
||||
|
||||
// Ersetze Form-Action URLs und füge Token-Input hinzu
|
||||
html = html.replace(
|
||||
/(<form[^>]*action="[^"]*\/login[^"]*"[^>]*>)/g,
|
||||
`$1${tokenInput}`
|
||||
);
|
||||
html = html.replace(
|
||||
/action="([^"]*\/login[^"]*)"/g,
|
||||
'action="/api/mytischtennis/login-submit"'
|
||||
);
|
||||
// Ersetze auch relative URLs
|
||||
html = html.replace(
|
||||
/action="\/login/g,
|
||||
'action="/api/mytischtennis/login-submit'
|
||||
);
|
||||
}
|
||||
|
||||
// Setze Content-Type
|
||||
res.setHeader('Content-Type', response.headers['content-type'] || 'text/html; charset=utf-8');
|
||||
|
||||
// Sende den modifizierten HTML-Inhalt
|
||||
res.status(response.status).send(html);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Login-Seite:', error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/login-submit
|
||||
* Proxy für Login-Form-Submission
|
||||
* Leitet den Login-Request durch, damit Cookies im Backend-Kontext bleiben
|
||||
* Authentifizierung ist optional - iframe kann keinen Token mitsenden
|
||||
*/
|
||||
async submitLogin(req, res, next) {
|
||||
try {
|
||||
// Versuche, userId aus Token zu bekommen (aus Query-Parameter oder Hidden-Input)
|
||||
let userId = null;
|
||||
const token = req.query.token || req.body.__token || req.headers['authorization']?.split(' ')[1] || req.headers['authcode'];
|
||||
if (token) {
|
||||
try {
|
||||
const jwt = (await import('jsonwebtoken')).default;
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
userId = decoded.userId;
|
||||
} catch (err) {
|
||||
// Token ungültig - ignorieren
|
||||
}
|
||||
}
|
||||
|
||||
// Entferne __token aus req.body, damit es nicht an mytischtennis.de gesendet wird
|
||||
if (req.body.__token) {
|
||||
delete req.body.__token;
|
||||
}
|
||||
|
||||
// Hole Cookies aus dem Request
|
||||
const cookies = req.headers.cookie || '';
|
||||
|
||||
// Leite den Login-Request an mytischtennis.de weiter
|
||||
const response = await axios.post(
|
||||
'https://www.mytischtennis.de/login?next=%2F&_data=routes%2F_auth%2B%2Flogin',
|
||||
req.body, // Form-Daten
|
||||
{
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept': '*/*',
|
||||
'Referer': 'https://www.mytischtennis.de/login?next=%2F'
|
||||
},
|
||||
maxRedirects: 0,
|
||||
validateStatus: () => true
|
||||
}
|
||||
);
|
||||
|
||||
// Setze Cookies aus der Response
|
||||
const setCookieHeaders = response.headers['set-cookie'];
|
||||
if (setCookieHeaders) {
|
||||
res.setHeader('Set-Cookie', setCookieHeaders);
|
||||
}
|
||||
|
||||
// Setze andere relevante Headers
|
||||
if (response.headers['content-type']) {
|
||||
res.setHeader('Content-Type', response.headers['content-type']);
|
||||
}
|
||||
if (response.headers['location']) {
|
||||
res.setHeader('Location', response.headers['location']);
|
||||
}
|
||||
|
||||
// Prüfe, ob Login erfolgreich war (durch Prüfung der Cookies)
|
||||
const authCookie = setCookieHeaders?.find(cookie => cookie.startsWith('sb-10-auth-token='));
|
||||
if (authCookie && userId) {
|
||||
// Login erfolgreich - speichere Session (nur wenn userId vorhanden)
|
||||
await this.saveSessionFromCookie(userId, authCookie);
|
||||
}
|
||||
|
||||
// Sende Response weiter
|
||||
res.status(response.status).send(response.data);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Login-Submit:', error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichere Session-Daten aus Cookie
|
||||
*/
|
||||
async saveSessionFromCookie(userId, cookieString) {
|
||||
try {
|
||||
const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/);
|
||||
if (!tokenMatch) {
|
||||
throw new Error('Token-Format ungültig');
|
||||
}
|
||||
|
||||
const base64Token = tokenMatch[1];
|
||||
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
|
||||
const tokenData = JSON.parse(decodedToken);
|
||||
|
||||
const MyTischtennis = (await import('../models/MyTischtennis.js')).default;
|
||||
const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
if (myTischtennisAccount) {
|
||||
myTischtennisAccount.accessToken = tokenData.access_token;
|
||||
myTischtennisAccount.refreshToken = tokenData.refresh_token;
|
||||
myTischtennisAccount.expiresAt = tokenData.expires_at;
|
||||
myTischtennisAccount.cookie = cookieString.split(';')[0].trim();
|
||||
myTischtennisAccount.userData = tokenData.user;
|
||||
myTischtennisAccount.lastLoginSuccess = new Date();
|
||||
myTischtennisAccount.lastLoginAttempt = new Date();
|
||||
|
||||
// Hole Club-Informationen
|
||||
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
|
||||
const profileResult = await myTischtennisClient.getUserProfile(myTischtennisAccount.cookie);
|
||||
if (profileResult.success) {
|
||||
myTischtennisAccount.clubId = profileResult.clubId;
|
||||
myTischtennisAccount.clubName = profileResult.clubName;
|
||||
myTischtennisAccount.fedNickname = profileResult.fedNickname;
|
||||
}
|
||||
|
||||
await myTischtennisAccount.save();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern der Session:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/extract-session
|
||||
* Extrahiere Session nach Login im iframe
|
||||
* Versucht, die Session-Daten aus den Cookies zu extrahieren
|
||||
* Authentifizierung ist optional - iframe kann keinen Token mitsenden
|
||||
*/
|
||||
async extractSession(req, res, next) {
|
||||
try {
|
||||
// Versuche, userId aus Token zu bekommen (optional)
|
||||
let userId = req.user?.id;
|
||||
|
||||
// Falls kein Token vorhanden, versuche userId aus Account zu bekommen (falls E-Mail bekannt)
|
||||
if (!userId) {
|
||||
// Kann nicht ohne Authentifizierung arbeiten - Session kann nicht gespeichert werden
|
||||
return res.status(401).json({
|
||||
error: 'Authentifizierung erforderlich zum Speichern der Session'
|
||||
});
|
||||
}
|
||||
|
||||
// Hole die Cookies aus dem Request
|
||||
const cookies = req.headers.cookie || '';
|
||||
|
||||
// Versuche, die Session zu verifizieren, indem wir einen Request mit den Cookies machen
|
||||
const response = await axios.get('https://www.mytischtennis.de/?_data=root', {
|
||||
headers: {
|
||||
'Cookie': cookies,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
// Prüfe, ob wir eingeloggt sind (durch Prüfung der Response)
|
||||
if (response.status === 200 && response.data?.userProfile) {
|
||||
// Session erfolgreich - speichere die Daten
|
||||
const account = await myTischtennisService.getAccount(userId);
|
||||
if (!account) {
|
||||
throw new HttpError('Kein myTischtennis-Account verknüpft', 404);
|
||||
}
|
||||
|
||||
// Extrahiere Cookie-String
|
||||
const cookieString = cookies.split(';').find(c => c.trim().startsWith('sb-10-auth-token='));
|
||||
if (!cookieString) {
|
||||
throw new HttpError('Kein Auth-Token in Cookies gefunden', 400);
|
||||
}
|
||||
|
||||
// Parse Token aus Cookie
|
||||
const tokenMatch = cookieString.match(/sb-10-auth-token=base64-([^;]+)/);
|
||||
if (!tokenMatch) {
|
||||
throw new HttpError('Token-Format ungültig', 400);
|
||||
}
|
||||
|
||||
const base64Token = tokenMatch[1];
|
||||
const decodedToken = Buffer.from(base64Token, 'base64').toString('utf-8');
|
||||
const tokenData = JSON.parse(decodedToken);
|
||||
|
||||
// Aktualisiere Account mit Session-Daten
|
||||
const MyTischtennis = (await import('../models/MyTischtennis.js')).default;
|
||||
const myTischtennisAccount = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
if (myTischtennisAccount) {
|
||||
myTischtennisAccount.accessToken = tokenData.access_token;
|
||||
myTischtennisAccount.refreshToken = tokenData.refresh_token;
|
||||
myTischtennisAccount.expiresAt = tokenData.expires_at;
|
||||
myTischtennisAccount.cookie = cookieString.trim();
|
||||
myTischtennisAccount.userData = tokenData.user;
|
||||
myTischtennisAccount.lastLoginSuccess = new Date();
|
||||
myTischtennisAccount.lastLoginAttempt = new Date();
|
||||
|
||||
// Hole Club-Informationen
|
||||
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
|
||||
const profileResult = await myTischtennisClient.getUserProfile(cookieString.trim());
|
||||
if (profileResult.success) {
|
||||
myTischtennisAccount.clubId = profileResult.clubId;
|
||||
myTischtennisAccount.clubName = profileResult.clubName;
|
||||
myTischtennisAccount.fedNickname = profileResult.fedNickname;
|
||||
}
|
||||
|
||||
await myTischtennisAccount.save();
|
||||
}
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'Session erfolgreich extrahiert und gespeichert'
|
||||
});
|
||||
} else {
|
||||
throw new HttpError('Nicht eingeloggt oder Session ungültig', 401);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Extrahieren der Session:', error);
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MyTischtennisController();
|
||||
|
||||
@@ -33,3 +33,4 @@ CREATE TABLE IF NOT EXISTS `member_training_group` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -37,3 +37,4 @@ const MemberTrainingGroup = sequelize.define('MemberTrainingGroup', {
|
||||
export default MemberTrainingGroup;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -48,3 +48,4 @@ const TrainingGroup = sequelize.define('TrainingGroup', {
|
||||
export default TrainingGroup;
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -168,3 +168,4 @@ migrateMyTischtennisEncryption()
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,74 +2,75 @@
|
||||
<div class="modal-overlay" @click.self="$emit('close')">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>{{ account ? $t('myTischtennisDialog.editAccount') : $t('myTischtennisDialog.linkAccount') }}</h3>
|
||||
<h3>{{ loginMode ? $t('myTischtennisDialog.login') : (account ? $t('myTischtennisDialog.editAccount') : $t('myTischtennisDialog.linkAccount')) }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="mtt-email">{{ $t('myTischtennisDialog.email') }}:</label>
|
||||
<input
|
||||
type="email"
|
||||
id="mtt-email"
|
||||
v-model="formData.email"
|
||||
:placeholder="$t('myTischtennisDialog.emailPlaceholder')"
|
||||
required
|
||||
/>
|
||||
<!-- Im Login-Modus: Zeige MyTischtennis-Login-Formular in iframe -->
|
||||
<div v-if="loginMode" class="login-iframe-container">
|
||||
<iframe
|
||||
ref="loginIframe"
|
||||
:src="loginUrl"
|
||||
class="login-iframe"
|
||||
@load="onIframeLoad"
|
||||
></iframe>
|
||||
<div v-if="loading" class="iframe-loading">
|
||||
{{ $t('myTischtennisDialog.loadingLoginForm') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="mtt-password">{{ $t('myTischtennisDialog.password') }}:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="mtt-password"
|
||||
v-model="formData.password"
|
||||
:placeholder="account && account.savePassword ? $t('myTischtennisDialog.passwordPlaceholderKeep') : $t('myTischtennisDialog.passwordPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
<!-- Im Bearbeiten-Modus: Zeige normales Formular -->
|
||||
<template v-else>
|
||||
<div class="form-group">
|
||||
<label for="mtt-email">{{ $t('myTischtennisDialog.email') }}:</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="formData.savePassword"
|
||||
type="email"
|
||||
id="mtt-email"
|
||||
v-model="formData.email"
|
||||
:placeholder="$t('myTischtennisDialog.emailPlaceholder')"
|
||||
required
|
||||
/>
|
||||
<span>{{ $t('myTischtennisDialog.savePassword') }}</span>
|
||||
</label>
|
||||
<p class="hint">
|
||||
{{ $t('myTischtennisDialog.savePasswordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="mtt-password">{{ $t('myTischtennisDialog.password') }}:</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="formData.autoUpdateRatings"
|
||||
:disabled="!formData.savePassword"
|
||||
type="password"
|
||||
id="mtt-password"
|
||||
v-model="formData.password"
|
||||
:placeholder="account && account.savePassword ? $t('myTischtennisDialog.passwordPlaceholderKeep') : $t('myTischtennisDialog.passwordPlaceholder')"
|
||||
/>
|
||||
<span>{{ $t('myTischtennisDialog.autoUpdateRatings') }}</span>
|
||||
</label>
|
||||
<p class="hint">
|
||||
{{ $t('myTischtennisDialog.autoUpdateRatingsHint') }}
|
||||
</p>
|
||||
<p v-if="!formData.savePassword" class="warning">
|
||||
⚠️ {{ $t('myTischtennisDialog.autoUpdateWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="formData.password">
|
||||
<label for="app-password">{{ $t('myTischtennisDialog.appPassword') }}:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="app-password"
|
||||
v-model="formData.userPassword"
|
||||
:placeholder="$t('myTischtennisDialog.appPasswordPlaceholder')"
|
||||
required
|
||||
/>
|
||||
<p class="hint">
|
||||
{{ $t('myTischtennisDialog.appPasswordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="formData.savePassword"
|
||||
/>
|
||||
<span>{{ $t('myTischtennisDialog.savePassword') }}</span>
|
||||
</label>
|
||||
<p class="hint">
|
||||
{{ $t('myTischtennisDialog.savePasswordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Auto-Update-Checkbox entfernt - automatische Abrufe wurden deaktiviert -->
|
||||
|
||||
<div class="form-group" v-if="formData.password">
|
||||
<label for="app-password">{{ $t('myTischtennisDialog.appPassword') }}:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="app-password"
|
||||
v-model="formData.userPassword"
|
||||
:placeholder="$t('myTischtennisDialog.appPasswordPlaceholder')"
|
||||
required
|
||||
/>
|
||||
<p class="hint">
|
||||
{{ $t('myTischtennisDialog.appPasswordHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
@@ -80,7 +81,7 @@
|
||||
<button class="btn-secondary" @click="$emit('close')" :disabled="saving">
|
||||
{{ $t('myTischtennisDialog.cancel') }}
|
||||
</button>
|
||||
<button class="btn-primary" @click="saveAccount" :disabled="!canSave || saving">
|
||||
<button v-if="!loginMode" class="btn-primary" @click="saveAccount()" :disabled="!canSave || saving">
|
||||
{{ saving ? $t('myTischtennisDialog.saving') : $t('myTischtennisDialog.save') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -97,6 +98,10 @@ export default {
|
||||
account: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
loginMode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -105,34 +110,104 @@ export default {
|
||||
email: this.account?.email || '',
|
||||
password: '',
|
||||
savePassword: this.account?.savePassword || false,
|
||||
autoUpdateRatings: this.account?.autoUpdateRatings || false,
|
||||
autoUpdateRatings: false, // Automatische Updates deaktiviert
|
||||
userPassword: ''
|
||||
},
|
||||
saving: false,
|
||||
error: null
|
||||
loading: false,
|
||||
error: null,
|
||||
urlCheckInterval: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
loginUrl() {
|
||||
// Verwende Backend-Proxy für Login-Seite, damit Cookies im Backend-Kontext bleiben
|
||||
// Verwende absolute URL für iframe
|
||||
const baseUrl = import.meta.env.VITE_BACKEND || window.location.origin;
|
||||
// Füge Token als Query-Parameter hinzu, damit Backend userId extrahieren kann
|
||||
const token = this.$store.state.token;
|
||||
const tokenParam = token ? `?token=${encodeURIComponent(token)}` : '';
|
||||
return `${baseUrl}/api/mytischtennis/login-page${tokenParam}`;
|
||||
},
|
||||
canSave() {
|
||||
// E-Mail ist erforderlich
|
||||
if (!this.formData.email.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Im Login-Modus: Passwort ist erforderlich
|
||||
if (this.loginMode) {
|
||||
return !!this.formData.password;
|
||||
}
|
||||
|
||||
// Wenn ein Passwort eingegeben wurde, muss auch das App-Passwort eingegeben werden
|
||||
if (this.formData.password && !this.formData.userPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Automatische Updates erfordern gespeichertes Passwort
|
||||
if (this.formData.autoUpdateRatings && !this.formData.savePassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.loginMode) {
|
||||
this.loading = true;
|
||||
// URL-Überwachung wird erst gestartet, nachdem das iframe geladen wurde
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// URL-Überwachung stoppen
|
||||
if (this.urlCheckInterval) {
|
||||
clearInterval(this.urlCheckInterval);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onIframeLoad() {
|
||||
this.loading = false;
|
||||
console.log('[MyTischtennisDialog] Iframe geladen');
|
||||
|
||||
// Starte URL-Überwachung erst NACH dem Laden des iframes
|
||||
// Warte 3 Sekunden, damit der Benutzer Zeit hat, sich einzuloggen
|
||||
setTimeout(() => {
|
||||
this.startUrlMonitoring();
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
checkIframeUrl() {
|
||||
try {
|
||||
const iframe = this.$refs.loginIframe;
|
||||
if (!iframe || !iframe.contentWindow) return;
|
||||
|
||||
// Versuche, die URL zu lesen (funktioniert nur bei gleicher Origin)
|
||||
// Da mytischtennis.de eine andere Origin ist, können wir die URL nicht direkt lesen
|
||||
// Stattdessen überwachen wir über PostMessage oder Polling
|
||||
} catch (error) {
|
||||
// Cross-Origin-Zugriff nicht möglich - das ist normal
|
||||
console.log('[MyTischtennisDialog] Cross-Origin-Zugriff nicht möglich (erwartet)');
|
||||
}
|
||||
},
|
||||
|
||||
startUrlMonitoring() {
|
||||
// Überwache, ob der Login erfolgreich war
|
||||
// Prüfe, ob bereits eine gültige Session existiert
|
||||
this.urlCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
// Prüfe, ob bereits eine gültige Session existiert
|
||||
// Nach erfolgreichem Login im iframe sollte submitLogin die Session gespeichert haben
|
||||
const sessionResponse = await apiClient.get('/mytischtennis/session');
|
||||
if (sessionResponse.data && sessionResponse.data.session && sessionResponse.data.session.accessToken) {
|
||||
// Session vorhanden - Login erfolgreich!
|
||||
clearInterval(this.urlCheckInterval);
|
||||
this.urlCheckInterval = null;
|
||||
this.$emit('logged-in');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
// Noch nicht eingeloggt oder Fehler - ignorieren
|
||||
// (wird alle 3 Sekunden wieder versucht)
|
||||
}
|
||||
}, 3000); // Alle 3 Sekunden prüfen
|
||||
},
|
||||
|
||||
async saveAccount() {
|
||||
if (!this.canSave) return;
|
||||
|
||||
@@ -143,7 +218,7 @@ export default {
|
||||
const payload = {
|
||||
email: this.formData.email,
|
||||
savePassword: this.formData.savePassword,
|
||||
autoUpdateRatings: this.formData.autoUpdateRatings
|
||||
autoUpdateRatings: false // Automatische Updates immer deaktiviert
|
||||
};
|
||||
|
||||
// Nur password und userPassword hinzufügen, wenn ein Passwort eingegeben wurde
|
||||
@@ -191,6 +266,33 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-iframe-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
min-height: 600px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.iframe-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 1rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 4px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
|
||||
@@ -1155,7 +1155,11 @@
|
||||
"cancel": "Abbrechen",
|
||||
"saving": "Speichere...",
|
||||
"save": "Speichern",
|
||||
"errorSaving": "Fehler beim Speichern des Accounts"
|
||||
"errorSaving": "Fehler beim Speichern des Accounts",
|
||||
"login": "Einloggen",
|
||||
"loggingIn": "Logge ein...",
|
||||
"errorLogin": "Fehler beim Einloggen",
|
||||
"loadingLoginForm": "Lade Login-Formular..."
|
||||
},
|
||||
"trainingDetails": {
|
||||
"title": "Trainings-Details",
|
||||
@@ -1536,6 +1540,7 @@
|
||||
"ERROR_MYTISCHTENNIS_PASSWORD_NOT_SAVED": "Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.",
|
||||
"ERROR_MYTISCHTENNIS_SESSION_EXPIRED": "Session abgelaufen. Bitte erneut einloggen.",
|
||||
"ERROR_MYTISCHTENNIS_NO_PASSWORD_SAVED": "Kein Passwort gespeichert und Session abgelaufen. Bitte Passwort eingeben.",
|
||||
"ERROR_MYTISCHTENNIS_CAPTCHA_REQUIRED": "CAPTCHA erforderlich. MyTischtennis verwendet jetzt ein CAPTCHA beim Login. Bitte loggen Sie sich einmal direkt auf mytischtennis.de ein, um das CAPTCHA zu lösen, oder kontaktieren Sie den Support.",
|
||||
"ERROR_MEMBER_NOT_FOUND": "Mitglied nicht gefunden.",
|
||||
"ERROR_MEMBER_ALREADY_EXISTS": "Mitglied existiert bereits.",
|
||||
"ERROR_MEMBER_FIRSTNAME_REQUIRED": "Vorname ist erforderlich.",
|
||||
|
||||
@@ -34,75 +34,12 @@
|
||||
<span>{{ formatDate(account.lastLoginAttempt) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastUpdateRatings">
|
||||
<label>{{ $t('myTischtennisAccount.lastFetch') }}</label>
|
||||
<span>{{ formatDate(account.lastUpdateRatings) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.autoUpdateRatings !== undefined">
|
||||
<label>{{ $t('myTischtennisAccount.autoUpdates') }}</label>
|
||||
<span>{{ account.autoUpdateRatings ? $t('myTischtennisAccount.enabled') : $t('myTischtennisAccount.disabled') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn-primary" @click="openEditDialog">{{ $t('myTischtennisAccount.editAccount') }}</button>
|
||||
<button type="button" class="btn-secondary" @click="testConnection">{{ $t('myTischtennisAccount.loginAgain') }}</button>
|
||||
<button class="btn-info" @click="openHistoryDialog" v-if="account">{{ $t('myTischtennisAccount.updateHistory') }}</button>
|
||||
<button class="btn-danger" @click="deleteAccount">{{ $t('myTischtennisAccount.unlinkAccount') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fetch Statistics Section -->
|
||||
<div class="info-section fetch-stats-section" v-if="account">
|
||||
<h2>{{ $t('myTischtennisAccount.fetchStatistics') }}</h2>
|
||||
|
||||
<div v-if="loadingStats" class="loading-stats">{{ $t('myTischtennisAccount.loadingStatistics') }}</div>
|
||||
|
||||
<div v-else-if="latestFetches" class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📊</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ $t('myTischtennisAccount.playerRatings') }}</h3>
|
||||
<div v-if="latestFetches.ratings">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.ratings.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.ratings.recordsProcessed }} {{ $t('myTischtennisAccount.playersUpdated') }}</p>
|
||||
<p class="stat-time" v-if="latestFetches.ratings.executionTime">{{ latestFetches.ratings.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">{{ $t('myTischtennisAccount.neverFetched') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🏓</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ $t('myTischtennisAccount.matchResults') }}</h3>
|
||||
<div v-if="latestFetches.match_results">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.match_results.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.match_results.recordsProcessed }} {{ $t('myTischtennisAccount.results') }}</p>
|
||||
<p class="stat-time" v-if="latestFetches.match_results.executionTime">{{ latestFetches.match_results.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">{{ $t('myTischtennisAccount.neverFetched') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📋</div>
|
||||
<div class="stat-content">
|
||||
<h3>{{ $t('myTischtennisAccount.leagueTables') }}</h3>
|
||||
<div v-if="latestFetches.league_table">
|
||||
<p class="stat-date">{{ formatDateRelative(latestFetches.league_table.lastFetch) }}</p>
|
||||
<p class="stat-detail">{{ latestFetches.league_table.recordsProcessed }} {{ $t('myTischtennisAccount.teams') }}</p>
|
||||
<p class="stat-time" v-if="latestFetches.league_table.executionTime">{{ latestFetches.league_table.executionTime }}ms</p>
|
||||
</div>
|
||||
<p v-else class="stat-never">{{ $t('myTischtennisAccount.neverFetched') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-secondary refresh-stats-btn" @click="loadLatestFetches">
|
||||
🔄 {{ $t('myTischtennisAccount.refreshStatistics') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-account">
|
||||
@@ -126,15 +63,12 @@
|
||||
<MyTischtennisDialog
|
||||
v-if="showDialog"
|
||||
:account="account"
|
||||
:login-mode="loginMode"
|
||||
@close="closeDialog"
|
||||
@saved="onAccountSaved"
|
||||
@logged-in="onLoggedIn"
|
||||
/>
|
||||
|
||||
<!-- History Dialog -->
|
||||
<MyTischtennisHistoryDialog
|
||||
v-if="showHistoryDialog"
|
||||
@close="closeHistoryDialog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -163,7 +97,6 @@
|
||||
import apiClient from '../apiClient.js';
|
||||
import { getSafeErrorMessage } from '../utils/errorMessages.js';
|
||||
import MyTischtennisDialog from '../components/MyTischtennisDialog.vue';
|
||||
import MyTischtennisHistoryDialog from '../components/MyTischtennisHistoryDialog.vue';
|
||||
|
||||
import InfoDialog from '../components/InfoDialog.vue';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
@@ -171,7 +104,6 @@ export default {
|
||||
name: 'MyTischtennisAccount',
|
||||
components: {
|
||||
MyTischtennisDialog,
|
||||
MyTischtennisHistoryDialog,
|
||||
InfoDialog,
|
||||
ConfirmDialog
|
||||
},
|
||||
@@ -194,17 +126,14 @@ export default {
|
||||
resolveCallback: null
|
||||
},
|
||||
loading: true,
|
||||
loadingStats: false,
|
||||
account: null,
|
||||
accountStatus: null,
|
||||
latestFetches: null,
|
||||
showDialog: false,
|
||||
showHistoryDialog: false
|
||||
loginMode: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadAccount();
|
||||
this.loadLatestFetches();
|
||||
},
|
||||
methods: {
|
||||
// Dialog Helper Methods
|
||||
@@ -262,19 +191,13 @@ export default {
|
||||
},
|
||||
|
||||
openEditDialog() {
|
||||
this.loginMode = false;
|
||||
this.showDialog = true;
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
},
|
||||
|
||||
openHistoryDialog() {
|
||||
this.showHistoryDialog = true;
|
||||
},
|
||||
|
||||
closeHistoryDialog() {
|
||||
this.showHistoryDialog = false;
|
||||
this.loginMode = false;
|
||||
},
|
||||
|
||||
async onAccountSaved() {
|
||||
@@ -286,33 +209,19 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
async onLoggedIn() {
|
||||
this.closeDialog();
|
||||
await this.loadAccount();
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: this.$t('myTischtennisAccount.loginSuccessful'),
|
||||
type: 'success'
|
||||
});
|
||||
},
|
||||
|
||||
async testConnection() {
|
||||
console.log('[testConnection] Starting connection test...');
|
||||
try {
|
||||
const response = await apiClient.post('/mytischtennis/verify');
|
||||
console.log('[testConnection] Response:', response);
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: this.$t('myTischtennisAccount.loginSuccessful'),
|
||||
type: 'success'
|
||||
});
|
||||
await this.loadAccount(); // Aktualisiere Account-Daten inkl. clubId, fedNickname
|
||||
} catch (error) {
|
||||
console.error('[testConnection] Error:', error);
|
||||
const message = getSafeErrorMessage(error, 'Login fehlgeschlagen');
|
||||
|
||||
if (error.response?.status === 400 && message.includes('Kein Passwort gespeichert')) {
|
||||
// Passwort-Dialog öffnen
|
||||
this.showDialog = true;
|
||||
this.showInfo(this.$t('myTischtennisAccount.passwordRequired'), message, '', 'warning');
|
||||
} else {
|
||||
this.showInfo(this.$t('myTischtennisAccount.loginFailed'), message, '', 'error');
|
||||
}
|
||||
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
// Öffne das Login-Dialog mit vorausgefüllter E-Mail
|
||||
this.showDialog = true;
|
||||
this.loginMode = true;
|
||||
},
|
||||
|
||||
async deleteAccount() {
|
||||
@@ -336,18 +245,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
async loadLatestFetches() {
|
||||
this.loadingStats = true;
|
||||
try {
|
||||
const response = await apiClient.get('/mytischtennis/latest-fetches');
|
||||
this.latestFetches = response.data.latestFetches;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Fetch-Statistiken:', error);
|
||||
} finally {
|
||||
this.loadingStats = false;
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
|
||||
Reference in New Issue
Block a user