Compare commits
8 Commits
mytischten
...
httv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7be98ffeeb | ||
|
|
1ef1711eea | ||
|
|
85981a880d | ||
|
|
84503b6404 | ||
|
|
bcc3ce036d | ||
|
|
0fe0514660 | ||
|
|
431ec861ba | ||
|
|
648b608036 |
603
backend/clients/hettvClient.js
Normal file
603
backend/clients/hettvClient.js
Normal file
@@ -0,0 +1,603 @@
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const BASE_URL = 'https://ttde-id.liga.nu';
|
||||
const CLICK_TT_BASE = 'https://httv.click-tt.de';
|
||||
|
||||
class HettvClient {
|
||||
constructor() {
|
||||
this.baseURL = BASE_URL;
|
||||
this.client = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
|
||||
},
|
||||
maxRedirects: 5, // Folge den OAuth2-Redirects
|
||||
validateStatus: (status) => status >= 200 && status < 400
|
||||
});
|
||||
|
||||
// Einfache Cookie-Jar nach Host -> { name: value }
|
||||
this.cookieJar = new Map();
|
||||
this.defaultHeaders = {
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:141.0) Gecko/20100101 Firefox/141.0',
|
||||
'Accept-Language': 'de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Login to HeTTV via OAuth2
|
||||
* @param {string} username - HeTTV username (email)
|
||||
* @param {string} password - HeTTV password
|
||||
* @returns {Promise<Object>} Login response with session data
|
||||
*/
|
||||
async login(username, password) {
|
||||
try {
|
||||
console.log('[HettvClient] - Starting login for:', username);
|
||||
|
||||
// Schritt 1: OAuth2-Authorization-Endpoint aufrufen - das sollte zur Login-Seite weiterleiten
|
||||
const oauthParams = new URLSearchParams({
|
||||
'scope': 'nuLiga',
|
||||
'response_type': 'code',
|
||||
'redirect_uri': 'https://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/oAuthLogin',
|
||||
'state': 'nonce=' + Math.random().toString(36).substring(2, 15),
|
||||
'client_id': 'XtVpGjXKAhz3BZuu'
|
||||
});
|
||||
|
||||
// OAuth2 Start
|
||||
|
||||
// Der OAuth2-Endpoint sollte direkt zur Login-Seite weiterleiten
|
||||
const loginPageResponse = await this.client.get(`/oauth2/authz/ttde?${oauthParams.toString()}`, {
|
||||
maxRedirects: 5, // Folge den Redirects zur Login-Seite
|
||||
validateStatus: (status) => status >= 200 && status < 400,
|
||||
headers: {
|
||||
...this.defaultHeaders
|
||||
}
|
||||
});
|
||||
|
||||
// Login-Seite erreicht
|
||||
|
||||
// Session-Cookie aus der Login-Seite extrahieren
|
||||
const setCookies = loginPageResponse.headers['set-cookie'];
|
||||
if (!setCookies || !Array.isArray(setCookies)) {
|
||||
console.error('[HettvClient] - No cookies from login page');
|
||||
return {
|
||||
success: false,
|
||||
error: 'Keine Session-Cookie von Login-Seite erhalten'
|
||||
};
|
||||
}
|
||||
|
||||
const sessionCookie = setCookies.find(cookie => cookie.startsWith('nusportingress='));
|
||||
if (!sessionCookie) {
|
||||
console.error('[HettvClient] - No nusportingress cookie from login page');
|
||||
return {
|
||||
success: false,
|
||||
error: 'Keine nusportingress Session von Login-Seite erhalten'
|
||||
};
|
||||
}
|
||||
|
||||
// Extrahiere t:formdata aus dem HTML der Login-Seite
|
||||
const htmlContent = loginPageResponse.data;
|
||||
// HTML erhalten
|
||||
|
||||
// Suche nach t:formdata im HTML - verschiedene mögliche Formate
|
||||
let formDataMatch = htmlContent.match(/name="t:formdata"\s+value="([^"]+)"/);
|
||||
|
||||
if (!formDataMatch) {
|
||||
// Versuche andere Formate
|
||||
formDataMatch = htmlContent.match(/name='t:formdata'\s+value='([^']+)'/);
|
||||
}
|
||||
|
||||
if (!formDataMatch) {
|
||||
// Suche nach hidden input mit t:formdata (value vor name)
|
||||
formDataMatch = htmlContent.match(/<input[^>]*value="([^"]+)"[^>]*name="t:formdata"/);
|
||||
}
|
||||
|
||||
if (!formDataMatch) {
|
||||
// Suche nach hidden input mit t:formdata (name vor value)
|
||||
formDataMatch = htmlContent.match(/<input[^>]*name="t:formdata"[^>]*value="([^"]+)"/);
|
||||
}
|
||||
|
||||
if (!formDataMatch) {
|
||||
// Suche nach t:formdata ohne Anführungszeichen
|
||||
formDataMatch = htmlContent.match(/name=t:formdata\s+value=([^\s>]+)/);
|
||||
}
|
||||
|
||||
if (!formDataMatch) {
|
||||
console.error('[HettvClient] - No t:formdata found in login page');
|
||||
console.log('[HettvClient] - HTML snippet:', htmlContent.substring(0, 2000));
|
||||
|
||||
// Debug: Suche nach allen hidden inputs
|
||||
const hiddenInputs = htmlContent.match(/<input[^>]*type="hidden"[^>]*>/g);
|
||||
console.log('[HettvClient] - Hidden inputs found:', hiddenInputs);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: 'Keine t:formdata von Login-Seite erhalten'
|
||||
};
|
||||
}
|
||||
|
||||
const tFormData = formDataMatch[1];
|
||||
// CSRF-Token gefunden
|
||||
|
||||
// Schritt 2: Login mit den korrekten Daten durchführen
|
||||
// Verwende die Session-Cookie für den Login-Request
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('t:submit', '["submit_0","submit_0"]');
|
||||
formData.append('t:ac', 'ttde');
|
||||
formData.append('t:formdata', tFormData);
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
|
||||
const loginResponse = await this.client.post('/oauth2/login.loginform', formData.toString(), {
|
||||
headers: {
|
||||
'Cookie': sessionCookie.split(';')[0],
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
...this.defaultHeaders,
|
||||
'Referer': `${BASE_URL}/oauth2/login.loginform`
|
||||
},
|
||||
maxRedirects: 5,
|
||||
validateStatus: (status) => status >= 200 && status < 400
|
||||
});
|
||||
|
||||
// Login-Antwort erhalten
|
||||
|
||||
// Prüfe ob wir erfolgreich eingeloggt sind
|
||||
// Login-Response geprüft
|
||||
|
||||
// Prüfe den Response-Inhalt um zu sehen ob wir noch auf der Login-Seite sind
|
||||
const responseContent = loginResponse.data;
|
||||
const isLoginPage = responseContent.includes('click-TT ID') &&
|
||||
responseContent.includes('Username') &&
|
||||
responseContent.includes('Password');
|
||||
|
||||
// Login-Page-Erkennung durchgeführt
|
||||
|
||||
if (isLoginPage) {
|
||||
console.log('[HettvClient] - Still on login page, login failed');
|
||||
console.log('[HettvClient] - Response snippet:', responseContent.substring(0, 500));
|
||||
return {
|
||||
success: false,
|
||||
error: 'Login fehlgeschlagen - ungültige Zugangsdaten'
|
||||
};
|
||||
}
|
||||
|
||||
// Prüfe auf OAuth2-Redirect oder Erfolg
|
||||
const hasOAuthRedirect = responseContent.includes('oauth2') ||
|
||||
responseContent.includes('redirect') ||
|
||||
loginResponse.status >= 300;
|
||||
|
||||
// OAuth Redirect erkannt
|
||||
|
||||
// Extrahiere die finale Session-Cookie
|
||||
const finalCookies = loginResponse.headers['set-cookie'];
|
||||
const finalSessionCookie = finalCookies?.find(cookie => cookie.startsWith('nusportingress='));
|
||||
|
||||
const sessionId = (finalSessionCookie || sessionCookie).match(/nusportingress=([^;]+)/)?.[1];
|
||||
|
||||
console.log('[HettvClient] - Login erfolgreich (HeTTV).');
|
||||
|
||||
// Versuche die finale OAuth-Weiterleitung zu httv.click-tt.de aufzurufen, um PHPSESSID zu erhalten
|
||||
let finalUrl = loginResponse.request?.res?.responseUrl;
|
||||
console.log('[HettvClient] - Login finalUrl:', finalUrl);
|
||||
let phpSessIdCookie = null;
|
||||
let finalHtml = null;
|
||||
try {
|
||||
if (finalUrl && finalUrl.includes('oAuthLogin')) {
|
||||
const clickTTClient = axios.create({
|
||||
timeout: 15000,
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status >= 200 && status < 400
|
||||
});
|
||||
|
||||
// Folge der Redirect-Kette manuell, übernehme Cookies
|
||||
let currentUrl = finalUrl;
|
||||
let lastResp = null;
|
||||
let hop = 0;
|
||||
const maxHops = 10;
|
||||
while (hop++ < maxHops && currentUrl) {
|
||||
lastResp = await clickTTClient.get(currentUrl, {
|
||||
headers: {
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
...this.defaultHeaders,
|
||||
'Referer': hop === 1 ? `${BASE_URL}/oauth2/login.loginform` : (lastResp?.request?.res?.responseUrl || currentUrl),
|
||||
'Cookie': this._cookieHeaderForUrl(currentUrl)
|
||||
}
|
||||
});
|
||||
|
||||
this._ingestSetCookiesFromResponse(currentUrl, lastResp.headers['set-cookie']);
|
||||
|
||||
const loc = lastResp.headers['location'];
|
||||
if (loc) {
|
||||
// Absolut vs relativ
|
||||
if (/^https?:\/\//i.test(loc)) {
|
||||
currentUrl = loc;
|
||||
} else {
|
||||
const u = new URL(currentUrl);
|
||||
currentUrl = `${u.origin}${loc}`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
break; // keine weitere Location => final
|
||||
}
|
||||
|
||||
const clickTTResp = lastResp;
|
||||
finalHtml = typeof clickTTResp.data === 'string' ? clickTTResp.data : '';
|
||||
const ctSetCookies = clickTTResp.headers['set-cookie'];
|
||||
if (Array.isArray(ctSetCookies)) {
|
||||
phpSessIdCookie = ctSetCookies.find(c => c.startsWith('PHPSESSID='))?.split(';')[0] || null;
|
||||
}
|
||||
// Finale click-TT URL ermittelt
|
||||
}
|
||||
} catch (e) {
|
||||
// Finale click-TT Seite konnte nicht geladen werden
|
||||
}
|
||||
|
||||
// Baue kombinierte Cookie-Kette (falls PHPSESSID vorhanden)
|
||||
const baseCookie = (finalSessionCookie || sessionCookie).split(';')[0];
|
||||
const combinedCookie = phpSessIdCookie ? `${baseCookie}; ${phpSessIdCookie}` : baseCookie;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionId: sessionId,
|
||||
cookie: combinedCookie,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
expiresAt: null,
|
||||
user: {
|
||||
finalUrl: finalUrl || null,
|
||||
htmlSnippet: finalHtml ? finalHtml.substring(0, 2000) : null
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('HeTTV login error:', error.message);
|
||||
console.error('Error details:', error.response?.status, error.response?.statusText);
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || 'Login fehlgeschlagen',
|
||||
status: error.response?.status || 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify login credentials
|
||||
* @param {string} username - HeTTV username
|
||||
* @param {string} password - HeTTV password
|
||||
* @returns {Promise<boolean>} True if credentials are valid
|
||||
*/
|
||||
async verifyCredentials(username, password) {
|
||||
const result = await this.login(username, password);
|
||||
return result.success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated request to click-TT
|
||||
* @param {string} endpoint - API endpoint
|
||||
* @param {string} cookie - JSESSIONID cookie
|
||||
* @param {Object} options - Additional axios options
|
||||
* @returns {Promise<Object>} API response
|
||||
*/
|
||||
async authenticatedRequest(endpoint, cookie, options = {}, finalUrl = null) {
|
||||
try {
|
||||
// Bestimme Basis-URL dynamisch aus finalUrl, falls vorhanden
|
||||
let baseURL = CLICK_TT_BASE;
|
||||
if (finalUrl) {
|
||||
try {
|
||||
const url = new URL(finalUrl);
|
||||
baseURL = url.origin;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const isAbsolute = /^https?:\/\//i.test(endpoint);
|
||||
const client = axios.create({
|
||||
baseURL: isAbsolute ? undefined : baseURL,
|
||||
timeout: 15000,
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status >= 200 && status < 400
|
||||
});
|
||||
|
||||
// Manuelles Redirect-Following inkl. Cookies/Referer
|
||||
let currentUrl = isAbsolute ? endpoint : `${baseURL}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
|
||||
let lastResp = null;
|
||||
const trace = [];
|
||||
let hop = 0;
|
||||
const maxHops = 10;
|
||||
|
||||
console.log(`[HettvClient] - Starting redirect chain from: ${currentUrl}`);
|
||||
|
||||
while (hop++ < maxHops && currentUrl) {
|
||||
console.log(`[HettvClient] - Redirect ${hop}: GET ${currentUrl}`);
|
||||
|
||||
lastResp = await client.request({
|
||||
method: options.method || 'GET',
|
||||
url: currentUrl,
|
||||
data: options.data,
|
||||
headers: {
|
||||
...this.defaultHeaders,
|
||||
...(options.headers || {}),
|
||||
'Cookie': this._mergeCookieHeader(cookie, this._cookieHeaderForUrl(currentUrl)),
|
||||
'Referer': hop === 1 ? (finalUrl || baseURL) : (lastResp?.request?.res?.responseUrl || currentUrl)
|
||||
}
|
||||
});
|
||||
|
||||
this._ingestSetCookiesFromResponse(currentUrl, lastResp.headers['set-cookie']);
|
||||
const loc = lastResp.headers['location'];
|
||||
|
||||
console.log(`[HettvClient] - Response: ${lastResp.status} ${lastResp.statusText}`);
|
||||
console.log(`[HettvClient] - Location header: ${loc || 'none'}`);
|
||||
console.log(`[HettvClient] - Set-Cookie header: ${lastResp.headers['set-cookie'] ? 'present' : 'none'}`);
|
||||
console.log(`[HettvClient] - Content-Type: ${lastResp.headers['content-type'] || 'none'}`);
|
||||
|
||||
// Speichere jede Seite zur Analyse
|
||||
try {
|
||||
const dir = path.resolve(process.cwd(), 'backend', 'uploads');
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const filename = `hettv_redirect_${hop}_${Date.now()}.html`;
|
||||
const filePath = path.join(dir, filename);
|
||||
const content = typeof lastResp.data === 'string' ? lastResp.data : JSON.stringify(lastResp.data, null, 2);
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
console.log(`[HettvClient] - Saved page to: ${filename}`);
|
||||
} catch (e) {
|
||||
console.log(`[HettvClient] - Could not save page ${hop}:`, e.message);
|
||||
}
|
||||
|
||||
trace.push({
|
||||
url: currentUrl,
|
||||
status: lastResp.status,
|
||||
location: loc || null
|
||||
});
|
||||
|
||||
if (loc) {
|
||||
const newUrl = /^https?:\/\//i.test(loc) ? loc : `${new URL(currentUrl).origin}${loc}`;
|
||||
console.log(`[HettvClient] - Following redirect to: ${newUrl}`);
|
||||
currentUrl = newUrl;
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[HettvClient] - Final response: ${lastResp.status} (no more redirects)`);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: lastResp?.data,
|
||||
trace
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('HeTTV API error:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || 'API-Anfrage fehlgeschlagen',
|
||||
status: error.response?.status || 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to main HeTTV page and find Downloads menu
|
||||
* @param {string} cookie - Session cookie
|
||||
* @returns {Promise<Object>} Response with main page content and download links
|
||||
*/
|
||||
async getMainPageWithDownloads(cookie, finalUrl = null) {
|
||||
try {
|
||||
console.log('[HettvClient] - Loading main HeTTV page...');
|
||||
|
||||
// Kandidaten für Einstiegs-URL bestimmen
|
||||
let origin = CLICK_TT_BASE;
|
||||
if (finalUrl) {
|
||||
try { origin = new URL(finalUrl).origin; } catch (_) {}
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
// Direkt zu HeTTV navigieren
|
||||
candidates.push('http://httv.click-tt.de/');
|
||||
candidates.push('http://httv.click-tt.de/wa/');
|
||||
candidates.push('http://httv.click-tt.de/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/');
|
||||
|
||||
// Wenn wir eine finalUrl haben, verwende diese auch
|
||||
if (finalUrl) {
|
||||
candidates.push(finalUrl);
|
||||
}
|
||||
|
||||
console.log('[HettvClient] - URL candidates:', candidates);
|
||||
|
||||
let mainPageResponse = null;
|
||||
let mainTrace = [];
|
||||
let lastError = null;
|
||||
for (const candidate of candidates) {
|
||||
const resp = await this.authenticatedRequest(candidate, cookie, {}, finalUrl);
|
||||
if (resp.success && typeof resp.data === 'string' && resp.data.length > 0) {
|
||||
mainPageResponse = resp;
|
||||
mainTrace = resp.trace || [];
|
||||
break;
|
||||
}
|
||||
lastError = resp;
|
||||
}
|
||||
|
||||
if (!mainPageResponse) {
|
||||
return lastError || { success: false, error: 'HeTTV Einstiegsseite nicht erreichbar', status: 404 };
|
||||
}
|
||||
|
||||
const htmlContent = mainPageResponse.data;
|
||||
console.log('[HettvClient] - Main page loaded, HTML length:', htmlContent.length);
|
||||
|
||||
// Erkenne Fehlerseite (Session ungültig)
|
||||
if (/click-TT\s*-\s*Fehlerseite/i.test(htmlContent) || /ungültige oder nicht mehr gültige URL/i.test(htmlContent)) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Session ungültig oder abgelaufen',
|
||||
status: 401,
|
||||
data: { htmlSnippet: htmlContent.substring(0, 1000) }
|
||||
};
|
||||
}
|
||||
|
||||
// Speichere HTML zur Analyse
|
||||
let savedFile = null;
|
||||
try {
|
||||
const dir = path.resolve(process.cwd(), 'backend', 'uploads');
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const filename = `hettv_main_${Date.now()}.html`;
|
||||
const filePath = path.join(dir, filename);
|
||||
fs.writeFileSync(filePath, htmlContent, 'utf8');
|
||||
savedFile = filePath;
|
||||
} catch (e) {
|
||||
// Ignoriere Speicherfehler still, nur für Debug
|
||||
}
|
||||
|
||||
// Suche nach Downloads-Links im HTML
|
||||
const downloadLinks = [];
|
||||
|
||||
// 1) URL-Heuristiken
|
||||
const urlPatterns = [
|
||||
/href="([^"]*download[^"]*)"/gi,
|
||||
/href="([^"]*downloads[^"]*)"/gi,
|
||||
/href="([^"]*Download[^"]*)"/gi,
|
||||
/href="([^"]*Downloads[^"]*)"/gi
|
||||
];
|
||||
|
||||
urlPatterns.forEach(pattern => {
|
||||
let match;
|
||||
while ((match = pattern.exec(htmlContent)) !== null) {
|
||||
const link = match[1];
|
||||
if (link && !downloadLinks.includes(link)) {
|
||||
downloadLinks.push(link);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Linktext-Heuristik: <a ...>Downloads</a>
|
||||
const anchorPattern = /<a[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
let aMatch;
|
||||
while ((aMatch = anchorPattern.exec(htmlContent)) !== null) {
|
||||
const href = aMatch[1];
|
||||
const text = aMatch[2].replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (/\bdownloads?\b/i.test(text)) {
|
||||
if (href && !downloadLinks.includes(href)) {
|
||||
downloadLinks.push(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fallback: Menüpunkte in Navigationen (role="navigation" etc.)
|
||||
if (downloadLinks.length === 0) {
|
||||
const navSectionRegex = /<nav[\s\S]*?<\/nav>/gi;
|
||||
let nav;
|
||||
while ((nav = navSectionRegex.exec(htmlContent)) !== null) {
|
||||
const section = nav[0];
|
||||
let m;
|
||||
anchorPattern.lastIndex = 0;
|
||||
while ((m = anchorPattern.exec(section)) !== null) {
|
||||
const href = m[1];
|
||||
const text = m[2].replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (/\bdownloads?\b/i.test(text)) {
|
||||
if (href && !downloadLinks.includes(href)) {
|
||||
downloadLinks.push(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[HettvClient] - Found download links:', downloadLinks);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
htmlContent: htmlContent,
|
||||
downloadLinks: downloadLinks,
|
||||
htmlSnippet: htmlContent.substring(0, 2000), // Erste 2000 Zeichen für Analyse
|
||||
savedFile,
|
||||
trace: mainTrace,
|
||||
lastUrl: mainTrace.length ? mainTrace[mainTrace.length - 1].url : null,
|
||||
lastStatus: mainTrace.length ? mainTrace[mainTrace.length - 1].status : null
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('HeTTV main page error:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Fehler beim Laden der Hauptseite',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specific download page
|
||||
* @param {string} downloadUrl - URL to the download page
|
||||
* @param {string} cookie - Session cookie
|
||||
* @returns {Promise<Object>} Response with download page content
|
||||
*/
|
||||
async loadDownloadPage(downloadUrl, cookie, finalUrl = null) {
|
||||
try {
|
||||
console.log('[HettvClient] - Loading download page:', downloadUrl);
|
||||
|
||||
const response = await this.authenticatedRequest(downloadUrl, cookie, {}, finalUrl);
|
||||
if (!response.success) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const htmlContent = response.data;
|
||||
console.log('[HettvClient] - Download page loaded, HTML length:', htmlContent.length);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
url: downloadUrl,
|
||||
htmlContent: htmlContent,
|
||||
htmlSnippet: htmlContent.substring(0, 3000) // Erste 3000 Zeichen für Analyse
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('HeTTV download page error:', error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message || 'Fehler beim Laden der Download-Seite',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// --- Cookie-Helfer ---
|
||||
_ingestSetCookiesFromResponse(currentUrl, setCookies) {
|
||||
if (!Array.isArray(setCookies) || setCookies.length === 0) return;
|
||||
const { host } = new URL(currentUrl);
|
||||
if (!this.cookieJar.has(host)) this.cookieJar.set(host, new Map());
|
||||
const jar = this.cookieJar.get(host);
|
||||
setCookies.forEach((cookieStr) => {
|
||||
const pair = cookieStr.split(';')[0];
|
||||
const eq = pair.indexOf('=');
|
||||
if (eq > 0) {
|
||||
const name = pair.substring(0, eq).trim();
|
||||
const value = pair.substring(eq + 1).trim();
|
||||
jar.set(name, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_cookieHeaderForUrl(currentUrl) {
|
||||
const { host } = new URL(currentUrl);
|
||||
const jar = this.cookieJar.get(host);
|
||||
if (!jar || jar.size === 0) return '';
|
||||
return Array.from(jar.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
|
||||
}
|
||||
|
||||
_mergeCookieHeader(primary, secondary) {
|
||||
const items = [];
|
||||
if (primary) items.push(primary);
|
||||
if (secondary) items.push(secondary);
|
||||
return items.filter(Boolean).join('; ');
|
||||
}
|
||||
}
|
||||
|
||||
export default new HettvClient();
|
||||
|
||||
@@ -1,77 +1,76 @@
|
||||
import ClubService from '../services/clubService.js';
|
||||
import { getUserByToken } from '../utils/userUtils.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
|
||||
export const getClubs = async (req, res) => {
|
||||
try {
|
||||
console.log('[getClubs] - get clubs');
|
||||
devLog('[getClubs] - get clubs');
|
||||
const clubs = await ClubService.getAllClubs();
|
||||
console.log('[getClubs] - prepare response');
|
||||
devLog('[getClubs] - prepare response');
|
||||
res.status(200).json(clubs);
|
||||
console.log('[getClubs] - done');
|
||||
devLog('[getClubs] - done');
|
||||
} catch (error) {
|
||||
console.log('[getClubs] - error');
|
||||
console.log(error);
|
||||
console.error('[getClubs] - error:', error);
|
||||
res.status(500).json({ error: "internalerror" });
|
||||
}
|
||||
};
|
||||
|
||||
export const addClub = async (req, res) => {
|
||||
console.log('[addClub] - Read out parameters');
|
||||
devLog('[addClub] - Read out parameters');
|
||||
const { authcode: token } = req.headers;
|
||||
const { name: clubName } = req.body;
|
||||
|
||||
try {
|
||||
console.log('[addClub] - find club by name');
|
||||
devLog('[addClub] - find club by name');
|
||||
const club = await ClubService.findClubByName(clubName);
|
||||
console.log('[addClub] - get user');
|
||||
devLog('[addClub] - get user');
|
||||
const user = await getUserByToken(token);
|
||||
console.log('[addClub] - check if club already exists');
|
||||
devLog('[addClub] - check if club already exists');
|
||||
if (club) {
|
||||
res.status(409).json({ error: "alreadyexists" });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[addClub] - create club');
|
||||
devLog('[addClub] - create club');
|
||||
const newClub = await ClubService.createClub(clubName);
|
||||
console.log('[addClub] - add user to new club');
|
||||
devLog('[addClub] - add user to new club');
|
||||
await ClubService.addUserToClub(user.id, newClub.id);
|
||||
console.log('[addClub] - prepare response');
|
||||
devLog('[addClub] - prepare response');
|
||||
res.status(200).json(newClub);
|
||||
console.log('[addClub] - done');
|
||||
devLog('[addClub] - done');
|
||||
} catch (error) {
|
||||
console.log('[addClub] - error');
|
||||
console.log(error);
|
||||
console.error('[addClub] - error:', error);
|
||||
res.status(500).json({ error: "internalerror" });
|
||||
}
|
||||
};
|
||||
|
||||
export const getClub = async (req, res) => {
|
||||
console.log('[getClub] - start');
|
||||
devLog('[getClub] - start');
|
||||
try {
|
||||
const { authcode: token } = req.headers;
|
||||
const { clubid: clubId } = req.params;
|
||||
console.log('[getClub] - get user');
|
||||
devLog('[getClub] - get user');
|
||||
const user = await getUserByToken(token);
|
||||
console.log('[getClub] - get users club');
|
||||
devLog('[getClub] - get users club');
|
||||
const access = await ClubService.getUserClubAccess(user.id, clubId);
|
||||
console.log('[getClub] - check access');
|
||||
devLog('[getClub] - check access');
|
||||
if (access.length === 0 || !access[0].approved) {
|
||||
res.status(403).json({ error: "noaccess", status: access.length === 0 ? "notrequested" : "requested" });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[getClub] - get club');
|
||||
devLog('[getClub] - get club');
|
||||
const club = await ClubService.findClubById(clubId);
|
||||
console.log('[getClub] - check club exists');
|
||||
devLog('[getClub] - check club exists');
|
||||
if (!club) {
|
||||
return res.status(404).json({ message: 'Club not found' });
|
||||
}
|
||||
|
||||
console.log('[getClub] - set response');
|
||||
devLog('[getClub] - set response');
|
||||
res.status(200).json(club);
|
||||
console.log('[getClub] - done');
|
||||
devLog('[getClub] - done');
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error('[getClub] - error:', error);
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
};
|
||||
@@ -82,7 +81,7 @@ export const requestClubAccess = async (req, res) => {
|
||||
|
||||
try {
|
||||
const user = await getUserByToken(token);
|
||||
console.log(user);
|
||||
devLog('[requestClubAccess] - user:', user);
|
||||
|
||||
await ClubService.requestAccessToClub(user.id, clubId);
|
||||
res.status(200).json({});
|
||||
@@ -92,6 +91,7 @@ export const requestClubAccess = async (req, res) => {
|
||||
} else if (error.message === 'clubnotfound') {
|
||||
res.status(404).json({ err: "clubnotfound" });
|
||||
} else {
|
||||
console.error('[requestClubAccess] - error:', error);
|
||||
res.status(500).json({ err: "internalerror" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import diaryService from '../services/diaryService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const getDatesForClub = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.params;
|
||||
@@ -38,7 +39,7 @@ const updateTrainingTimes = async (req, res) => {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { dateId, trainingStart, trainingEnd } = req.body;
|
||||
if (!dateId || !trainingStart) {
|
||||
console.log(dateId, trainingStart, trainingEnd);
|
||||
devLog(dateId, trainingStart, trainingEnd);
|
||||
throw new HttpError('notallfieldsfilled', 400);
|
||||
}
|
||||
const updatedDate = await diaryService.updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import diaryDateActivityService from '../services/diaryDateActivityService.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const createDiaryDateActivity = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
@@ -15,7 +16,7 @@ export const createDiaryDateActivity = async (req, res) => {
|
||||
});
|
||||
res.status(201).json(activityItem);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Error creating activity' });
|
||||
}
|
||||
};
|
||||
@@ -58,7 +59,7 @@ export const updateDiaryDateActivityOrder = async (req, res) => {
|
||||
const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId);
|
||||
res.status(200).json(updatedActivity);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Error updating activity order' });
|
||||
}
|
||||
};
|
||||
@@ -70,7 +71,7 @@ export const getDiaryDateActivities = async (req, res) => {
|
||||
const activities = await diaryDateActivityService.getActivities(userToken, clubId, diaryDateId);
|
||||
res.status(200).json(activities);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Error getting activities' });
|
||||
}
|
||||
}
|
||||
@@ -82,7 +83,7 @@ export const addGroupActivity = async(req, res) => {
|
||||
const activityItem = await diaryDateActivityService.addGroupActivity(userToken, clubId, diaryDateId, groupId, activity);
|
||||
res.status(201).json(activityItem);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Error adding group activity' });
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import diaryDateTagService from "../services/diaryDateTagService.js"
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const getDiaryDateMemberTags = async (req, res) => {
|
||||
console.log("getDiaryDateMemberTags");
|
||||
devLog("getDiaryDateMemberTags");
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId, memberId } = req.params;
|
||||
@@ -14,7 +15,7 @@ export const getDiaryDateMemberTags = async (req, res) => {
|
||||
}
|
||||
|
||||
export const addDiaryDateTag = async (req, res) => {
|
||||
console.log("addDiaryDateTag");
|
||||
devLog("addDiaryDateTag");
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import DiaryMemberService from '../services/diaryMemberService.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const getMemberTags = async (req, res) => {
|
||||
try {
|
||||
const { diaryDateId, memberId } = req.query;
|
||||
const { clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
console.log(diaryDateId, memberId, clubId);
|
||||
devLog(diaryDateId, memberId, clubId);
|
||||
const tags = await DiaryMemberService.getTagsForMemberAndDate(userToken, clubId, diaryDateId, memberId);
|
||||
res.status(200).json(tags);
|
||||
} catch (error) {
|
||||
@@ -19,7 +20,7 @@ const getMemberNotes = async (req, res) => {
|
||||
const { diaryDateId, memberId } = req.query;
|
||||
const { clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
console.log('---------->', userToken, clubId);
|
||||
devLog('---------->', userToken, clubId);
|
||||
const notes = await DiaryMemberService.getNotesForMember(userToken, clubId, diaryDateId, memberId);
|
||||
res.status(200).json(notes);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DiaryTag, DiaryDateTag } from '../models/index.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const getTags = async (req, res) => {
|
||||
try {
|
||||
const tags = await DiaryTag.findAll();
|
||||
@@ -12,11 +13,11 @@ export const getTags = async (req, res) => {
|
||||
export const createTag = async (req, res) => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
console.log(name);
|
||||
devLog(name);
|
||||
const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } });
|
||||
res.status(201).json(newTag);
|
||||
} catch (error) {
|
||||
console.log('[createTag] - Error:', error);
|
||||
devLog('[createTag] - Error:', error);
|
||||
res.status(500).json({ error: 'Error creating tag' });
|
||||
}
|
||||
};
|
||||
|
||||
172
backend/controllers/externalServiceController.js
Normal file
172
backend/controllers/externalServiceController.js
Normal file
@@ -0,0 +1,172 @@
|
||||
import externalServiceService from '../services/externalServiceService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
class ExternalServiceController {
|
||||
/**
|
||||
* GET /api/mytischtennis/account?service=mytischtennis
|
||||
* Get current user's external service account
|
||||
*/
|
||||
async getAccount(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const service = req.query.service || 'mytischtennis';
|
||||
const account = await externalServiceService.getAccount(userId, service);
|
||||
|
||||
if (!account) {
|
||||
return res.status(200).json({ account: null });
|
||||
}
|
||||
|
||||
res.status(200).json({ account });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mytischtennis/status?service=mytischtennis
|
||||
* Check account configuration status
|
||||
*/
|
||||
async getStatus(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const service = req.query.service || 'mytischtennis';
|
||||
const status = await externalServiceService.checkAccountStatus(userId, service);
|
||||
res.status(200).json(status);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/account
|
||||
* Create or update external service account
|
||||
*/
|
||||
async upsertAccount(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { email, password, savePassword, userPassword, service = 'mytischtennis' } = req.body;
|
||||
|
||||
if (!email) {
|
||||
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
|
||||
}
|
||||
|
||||
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
|
||||
if (password && !userPassword) {
|
||||
throw new HttpError(400, 'App-Passwort erforderlich zum Setzen des myTischtennis-Passworts');
|
||||
}
|
||||
|
||||
const account = await externalServiceService.upsertAccount(
|
||||
userId,
|
||||
email,
|
||||
password,
|
||||
savePassword || false,
|
||||
userPassword,
|
||||
service
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
message: `${service}-Account erfolgreich gespeichert`,
|
||||
account
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/mytischtennis/account?service=mytischtennis
|
||||
* Delete external service account
|
||||
*/
|
||||
async deleteAccount(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const service = req.query.service || 'mytischtennis';
|
||||
const deleted = await externalServiceService.deleteAccount(userId, service);
|
||||
|
||||
if (!deleted) {
|
||||
throw new HttpError(404, `Kein ${service}-Account gefunden`);
|
||||
}
|
||||
|
||||
res.status(200).json({ message: `${service}-Account gelöscht` });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/verify
|
||||
* Verify login credentials
|
||||
*/
|
||||
async verifyLogin(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { password, service = 'mytischtennis' } = req.body;
|
||||
|
||||
const result = await externalServiceService.verifyLogin(userId, password, service);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Login erfolgreich',
|
||||
success: true,
|
||||
accessToken: result.accessToken,
|
||||
expiresAt: result.expiresAt,
|
||||
clubId: result.clubId,
|
||||
clubName: result.clubName
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mytischtennis/session?service=mytischtennis
|
||||
* Get stored session data for authenticated requests
|
||||
*/
|
||||
async getSession(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const service = req.query.service || 'mytischtennis';
|
||||
const session = await externalServiceService.getSession(userId, service);
|
||||
|
||||
res.status(200).json({ session });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/external-service/hettv/main-page
|
||||
* Load HeTTV main page and find download links
|
||||
*/
|
||||
async loadHettvMainPage(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const result = await externalServiceService.loadHettvMainPage(userId);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/external-service/hettv/download-page
|
||||
* Load specific HeTTV download page
|
||||
*/
|
||||
async loadHettvDownloadPage(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { downloadUrl } = req.body;
|
||||
|
||||
if (!downloadUrl) {
|
||||
throw new HttpError(400, 'Download-URL ist erforderlich');
|
||||
}
|
||||
|
||||
const result = await externalServiceService.loadHettvDownloadPage(userId, downloadUrl);
|
||||
res.status(200).json(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ExternalServiceController();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import groupService from '../services/groupService.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const addGroup = async(req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
@@ -9,7 +10,7 @@ const addGroup = async(req, res) => {
|
||||
res.status(201).json(result);
|
||||
} catch (error) {
|
||||
console.error('[addGroup] - Error:', error);
|
||||
console.log(req.params, req.headers, req.body)
|
||||
devLog(req.params, req.headers, req.body)
|
||||
res.status(error.statusCode || 500).json({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import MatchService from '../services/matchService.js';
|
||||
import fs from 'fs';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const uploadCSV = async (req, res) => {
|
||||
try {
|
||||
const { clubId } = req.body;
|
||||
@@ -21,7 +22,7 @@ export const uploadCSV = async (req, res) => {
|
||||
|
||||
export const getLeaguesForCurrentSeason = async (req, res) => {
|
||||
try {
|
||||
console.log(req.headers, req.params);
|
||||
devLog(req.headers, req.params);
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { clubId } = req.params;
|
||||
const leagues = await MatchService.getLeaguesForCurrentSeason(userToken, clubId);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import MemberService from "../services/memberService.js";
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const getClubMembers = async(req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
@@ -9,24 +10,24 @@ const getClubMembers = async(req, res) => {
|
||||
}
|
||||
res.status(200).json(await MemberService.getClubMembers(userToken, clubId, showAll));
|
||||
} catch(error) {
|
||||
console.log('[getClubMembers] - Error: ', error);
|
||||
devLog('[getClubMembers] - Error: ', error);
|
||||
res.status(500).json({ error: 'systemerror' });
|
||||
}
|
||||
}
|
||||
|
||||
const getWaitingApprovals = async(req, res) => {
|
||||
try {
|
||||
console.log('[getWaitingApprovals] - Start');
|
||||
devLog('[getWaitingApprovals] - Start');
|
||||
const { id: clubId } = req.params;
|
||||
console.log('[getWaitingApprovals] - get token');
|
||||
devLog('[getWaitingApprovals] - get token');
|
||||
const { authcode: userToken } = req.headers;
|
||||
console.log('[getWaitingApprovals] - load for waiting approvals');
|
||||
devLog('[getWaitingApprovals] - load for waiting approvals');
|
||||
const waitingApprovals = await MemberService.getApprovalRequests(userToken, clubId);
|
||||
console.log('[getWaitingApprovals] - set response');
|
||||
devLog('[getWaitingApprovals] - set response');
|
||||
res.status(200).json(waitingApprovals);
|
||||
console.log('[getWaitingApprovals] - done');
|
||||
devLog('[getWaitingApprovals] - done');
|
||||
} catch(error) {
|
||||
console.log('[getWaitingApprovals] - Error: ', error);
|
||||
devLog('[getWaitingApprovals] - Error: ', error);
|
||||
res.status(403).json({ error: error });
|
||||
}
|
||||
}
|
||||
@@ -59,7 +60,7 @@ const uploadMemberImage = async (req, res) => {
|
||||
};
|
||||
|
||||
const getMemberImage = async (req, res) => {
|
||||
console.log('[getMemberImage]');
|
||||
devLog('[getMemberImage]');
|
||||
try {
|
||||
const { clubId, memberId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
@@ -76,7 +77,7 @@ const getMemberImage = async (req, res) => {
|
||||
};
|
||||
|
||||
const updateRatingsFromMyTischtennis = async (req, res) => {
|
||||
console.log('[updateRatingsFromMyTischtennis]');
|
||||
devLog('[updateRatingsFromMyTischtennis]');
|
||||
try {
|
||||
const { id: clubId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import MemberNoteService from "../services/memberNoteService.js";
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const getMemberNotes = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { memberId } = req.params;
|
||||
const { clubId } = req.query;
|
||||
console.log('[getMemberNotes]', userToken, memberId, clubId);
|
||||
devLog('[getMemberNotes]', userToken, memberId, clubId);
|
||||
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
|
||||
res.status(200).json(notes);
|
||||
} catch (error) {
|
||||
console.log('[getMemberNotes] - Error: ', error);
|
||||
devLog('[getMemberNotes] - Error: ', error);
|
||||
res.status(500).json({ error: 'systemerror' });
|
||||
}
|
||||
};
|
||||
@@ -18,12 +19,12 @@ const addMemberNote = async (req, res) => {
|
||||
try {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { memberId, content, clubId } = req.body;
|
||||
console.log('[addMemberNote]', userToken, memberId, content, clubId);
|
||||
devLog('[addMemberNote]', userToken, memberId, content, clubId);
|
||||
await MemberNoteService.addNoteToMember(userToken, clubId, memberId, content);
|
||||
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
|
||||
res.status(201).json(notes);
|
||||
} catch (error) {
|
||||
console.log('[addMemberNote] - Error: ', error);
|
||||
devLog('[addMemberNote] - Error: ', error);
|
||||
res.status(500).json({ error: 'systemerror' });
|
||||
}
|
||||
};
|
||||
@@ -33,13 +34,13 @@ const deleteMemberNote = async (req, res) => {
|
||||
const { authcode: userToken } = req.headers;
|
||||
const { noteId } = req.params;
|
||||
const { clubId } = req.body;
|
||||
console.log('[deleteMemberNote]', userToken, noteId, clubId);
|
||||
devLog('[deleteMemberNote]', userToken, noteId, clubId);
|
||||
const memberId = await MemberNoteService.getMemberIdForNote(noteId); // Member ID ermitteln
|
||||
await MemberNoteService.deleteNoteForMember(userToken, clubId, noteId);
|
||||
const notes = await MemberNoteService.getNotesForMember(userToken, clubId, memberId);
|
||||
res.status(200).json(notes);
|
||||
} catch (error) {
|
||||
console.log('[deleteMemberNote] - Error: ', error);
|
||||
devLog('[deleteMemberNote] - Error: ', error);
|
||||
res.status(500).json({ error: 'systemerror' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
import myTischtennisService from '../services/myTischtennisService.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
class MyTischtennisController {
|
||||
/**
|
||||
* GET /api/mytischtennis/account
|
||||
* Get current user's myTischtennis account
|
||||
*/
|
||||
async getAccount(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const account = await myTischtennisService.getAccount(userId);
|
||||
|
||||
if (!account) {
|
||||
return res.status(200).json({ account: null });
|
||||
}
|
||||
|
||||
res.status(200).json({ account });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mytischtennis/status
|
||||
* Check account configuration status
|
||||
*/
|
||||
async getStatus(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const status = await myTischtennisService.checkAccountStatus(userId);
|
||||
res.status(200).json(status);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/account
|
||||
* Create or update myTischtennis account
|
||||
*/
|
||||
async upsertAccount(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { email, password, savePassword, userPassword } = req.body;
|
||||
|
||||
if (!email) {
|
||||
throw new HttpError(400, 'E-Mail-Adresse erforderlich');
|
||||
}
|
||||
|
||||
// Wenn ein Passwort gesetzt wird, muss das App-Passwort angegeben werden
|
||||
if (password && !userPassword) {
|
||||
throw new HttpError(400, 'App-Passwort erforderlich zum Setzen des myTischtennis-Passworts');
|
||||
}
|
||||
|
||||
const account = await myTischtennisService.upsertAccount(
|
||||
userId,
|
||||
email,
|
||||
password,
|
||||
savePassword || false,
|
||||
userPassword
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'myTischtennis-Account erfolgreich gespeichert',
|
||||
account
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/mytischtennis/account
|
||||
* Delete myTischtennis account
|
||||
*/
|
||||
async deleteAccount(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const deleted = await myTischtennisService.deleteAccount(userId);
|
||||
|
||||
if (!deleted) {
|
||||
throw new HttpError(404, 'Kein myTischtennis-Account gefunden');
|
||||
}
|
||||
|
||||
res.status(200).json({ message: 'myTischtennis-Account gelöscht' });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mytischtennis/verify
|
||||
* Verify login credentials
|
||||
*/
|
||||
async verifyLogin(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { password } = req.body;
|
||||
|
||||
const result = await myTischtennisService.verifyLogin(userId, password);
|
||||
|
||||
res.status(200).json({
|
||||
message: 'Login erfolgreich',
|
||||
success: true,
|
||||
accessToken: result.accessToken,
|
||||
expiresAt: result.expiresAt,
|
||||
clubId: result.clubId,
|
||||
clubName: result.clubName
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mytischtennis/session
|
||||
* Get stored session data for authenticated requests
|
||||
*/
|
||||
async getSession(req, res, next) {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const session = await myTischtennisService.getSession(userId);
|
||||
|
||||
res.status(200).json({ session });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MyTischtennisController();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Participant from '../models/Participant.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const getParticipants = async (req, res) => {
|
||||
try {
|
||||
const { dateId } = req.params;
|
||||
const participants = await Participant.findAll({ where: { diaryDateId: dateId } });
|
||||
res.status(200).json(participants);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Fehler beim Abrufen der Teilnehmer' });
|
||||
}
|
||||
};
|
||||
@@ -17,7 +18,7 @@ export const addParticipant = async (req, res) => {
|
||||
const participant = await Participant.create({ diaryDateId, memberId });
|
||||
res.status(201).json(participant);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Fehler beim Hinzufügen des Teilnehmers' });
|
||||
}
|
||||
};
|
||||
@@ -28,7 +29,7 @@ export const removeParticipant = async (req, res) => {
|
||||
await Participant.destroy({ where: { diaryDateId, memberId } });
|
||||
res.status(200).json({ message: 'Teilnehmer entfernt' });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
res.status(500).json({ error: 'Fehler beim Entfernen des Teilnehmers' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
export const uploadPredefinedActivityImage = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params; // predefinedActivityId
|
||||
@@ -35,7 +36,7 @@ export const uploadPredefinedActivityImage = async (req, res) => {
|
||||
|
||||
// Extrahiere Zeichnungsdaten aus dem Request
|
||||
const drawingData = req.body.drawingData ? JSON.parse(req.body.drawingData) : null;
|
||||
console.log('[uploadPredefinedActivityImage] - drawingData:', drawingData);
|
||||
devLog('[uploadPredefinedActivityImage] - drawingData:', drawingData);
|
||||
|
||||
const imageRecord = await PredefinedActivityImage.create({
|
||||
predefinedActivityId: id,
|
||||
|
||||
@@ -19,6 +19,25 @@ class TrainingStatsController {
|
||||
}
|
||||
});
|
||||
|
||||
// Anzahl der Trainings im jeweiligen Zeitraum berechnen
|
||||
const trainingsCount12Months = await DiaryDate.count({
|
||||
where: {
|
||||
clubId: parseInt(clubId),
|
||||
date: {
|
||||
[Op.gte]: twelveMonthsAgo
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const trainingsCount3Months = await DiaryDate.count({
|
||||
where: {
|
||||
clubId: parseInt(clubId),
|
||||
date: {
|
||||
[Op.gte]: threeMonthsAgo
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const stats = [];
|
||||
|
||||
for (const member of members) {
|
||||
@@ -116,7 +135,36 @@ class TrainingStatsController {
|
||||
// Nach Gesamtteilnahme absteigend sortieren
|
||||
stats.sort((a, b) => b.participationTotal - a.participationTotal);
|
||||
|
||||
res.json(stats);
|
||||
// Trainingstage mit Teilnehmerzahlen abrufen (letzte 12 Monate, absteigend sortiert)
|
||||
const trainingDays = await DiaryDate.findAll({
|
||||
where: {
|
||||
clubId: parseInt(clubId),
|
||||
date: {
|
||||
[Op.gte]: twelveMonthsAgo
|
||||
}
|
||||
},
|
||||
include: [{
|
||||
model: Participant,
|
||||
as: 'participantList',
|
||||
attributes: ['id']
|
||||
}],
|
||||
order: [['date', 'DESC']]
|
||||
});
|
||||
|
||||
// Formatiere Trainingstage mit Teilnehmerzahl
|
||||
const formattedTrainingDays = trainingDays.map(day => ({
|
||||
id: day.id,
|
||||
date: day.date,
|
||||
participantCount: day.participantList ? day.participantList.length : 0
|
||||
}));
|
||||
|
||||
// Zusätzliche Metadaten mit Trainingsanzahl zurückgeben
|
||||
res.json({
|
||||
members: stats,
|
||||
trainingsCount12Months,
|
||||
trainingsCount3Months,
|
||||
trainingDays: formattedTrainingDays
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Trainings-Statistik:', error);
|
||||
|
||||
132
backend/models/ExternalServiceAccount.js
Normal file
132
backend/models/ExternalServiceAccount.js
Normal file
@@ -0,0 +1,132 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import { encryptData, decryptData } from '../utils/encrypt.js';
|
||||
|
||||
const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'user',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
service: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: 'mytischtennis'
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
encryptedPassword: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'encrypted_password'
|
||||
},
|
||||
savePassword: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false,
|
||||
field: 'save_password'
|
||||
},
|
||||
accessToken: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'access_token'
|
||||
},
|
||||
refreshToken: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
field: 'refresh_token'
|
||||
},
|
||||
expiresAt: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: true,
|
||||
field: 'expires_at'
|
||||
},
|
||||
cookie: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true
|
||||
},
|
||||
userData: {
|
||||
type: DataTypes.JSON,
|
||||
allowNull: true,
|
||||
field: 'user_data'
|
||||
},
|
||||
clubId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'club_id'
|
||||
},
|
||||
clubName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'club_name'
|
||||
},
|
||||
fedNickname: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
field: 'fed_nickname'
|
||||
},
|
||||
lastLoginAttempt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'last_login_attempt'
|
||||
},
|
||||
lastLoginSuccess: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
field: 'last_login_success'
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'external_service_account',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['user_id', 'service']
|
||||
}
|
||||
],
|
||||
hooks: {
|
||||
beforeSave: async (instance) => {
|
||||
// Wenn savePassword false ist, password auf null setzen
|
||||
if (!instance.savePassword) {
|
||||
instance.encryptedPassword = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Virtuelle Felder für password handling
|
||||
ExternalServiceAccount.prototype.setPassword = function(password) {
|
||||
if (password && this.savePassword) {
|
||||
this.encryptedPassword = encryptData(password);
|
||||
} else {
|
||||
this.encryptedPassword = null;
|
||||
}
|
||||
};
|
||||
|
||||
ExternalServiceAccount.prototype.getPassword = function() {
|
||||
if (this.encryptedPassword) {
|
||||
try {
|
||||
return decryptData(this.encryptedPassword);
|
||||
} catch (error) {
|
||||
console.error('Error decrypting password:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ExternalServiceAccount;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DataTypes } from 'sequelize';
|
||||
import sequelize from '../database.js';
|
||||
import { encryptData, decryptData } from '../utils/encrypt.js';
|
||||
|
||||
const MyTischtennis = sequelize.define('MyTischtennis', {
|
||||
const ExternalServiceAccount = sequelize.define('ExternalServiceAccount', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
@@ -12,13 +12,17 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
|
||||
userId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
references: {
|
||||
model: 'user',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
service: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
defaultValue: 'mytischtennis'
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
@@ -85,8 +89,14 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
|
||||
}
|
||||
}, {
|
||||
underscored: true,
|
||||
tableName: 'my_tischtennis',
|
||||
tableName: 'external_service_account',
|
||||
timestamps: true,
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['user_id', 'service']
|
||||
}
|
||||
],
|
||||
hooks: {
|
||||
beforeSave: async (instance) => {
|
||||
// Wenn savePassword false ist, password auf null setzen
|
||||
@@ -98,7 +108,7 @@ const MyTischtennis = sequelize.define('MyTischtennis', {
|
||||
});
|
||||
|
||||
// Virtuelle Felder für password handling
|
||||
MyTischtennis.prototype.setPassword = function(password) {
|
||||
ExternalServiceAccount.prototype.setPassword = function(password) {
|
||||
if (password && this.savePassword) {
|
||||
this.encryptedPassword = encryptData(password);
|
||||
} else {
|
||||
@@ -106,17 +116,17 @@ MyTischtennis.prototype.setPassword = function(password) {
|
||||
}
|
||||
};
|
||||
|
||||
MyTischtennis.prototype.getPassword = function() {
|
||||
ExternalServiceAccount.prototype.getPassword = function() {
|
||||
if (this.encryptedPassword) {
|
||||
try {
|
||||
return decryptData(this.encryptedPassword);
|
||||
} catch (error) {
|
||||
console.error('Error decrypting myTischtennis password:', error);
|
||||
console.error('Error decrypting password:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default MyTischtennis;
|
||||
export default ExternalServiceAccount;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import UserToken from './UserToken.js';
|
||||
import OfficialTournament from './OfficialTournament.js';
|
||||
import OfficialCompetition from './OfficialCompetition.js';
|
||||
import OfficialCompetitionMember from './OfficialCompetitionMember.js';
|
||||
import MyTischtennis from './MyTischtennis.js';
|
||||
import ExternalServiceAccount from './ExternalServiceAccount.js';
|
||||
// Official tournaments relations
|
||||
OfficialTournament.hasMany(OfficialCompetition, { foreignKey: 'tournamentId', as: 'competitions' });
|
||||
OfficialCompetition.belongsTo(OfficialTournament, { foreignKey: 'tournamentId', as: 'tournament' });
|
||||
@@ -205,8 +205,8 @@ Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
|
||||
Accident.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
|
||||
DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' });
|
||||
|
||||
User.hasOne(MyTischtennis, { foreignKey: 'userId', as: 'myTischtennis' });
|
||||
MyTischtennis.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
User.hasMany(ExternalServiceAccount, { foreignKey: 'userId', as: 'externalServiceAccounts' });
|
||||
ExternalServiceAccount.belongsTo(User, { foreignKey: 'userId', as: 'user' });
|
||||
|
||||
export {
|
||||
User,
|
||||
@@ -243,5 +243,5 @@ export {
|
||||
OfficialTournament,
|
||||
OfficialCompetition,
|
||||
OfficialCompetitionMember,
|
||||
MyTischtennis,
|
||||
ExternalServiceAccount,
|
||||
};
|
||||
|
||||
36
backend/routes/externalServiceRoutes.js
Normal file
36
backend/routes/externalServiceRoutes.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import express from 'express';
|
||||
import externalServiceController from '../controllers/externalServiceController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// GET /api/external-service/account?service=mytischtennis - Get account
|
||||
router.get('/account', externalServiceController.getAccount);
|
||||
|
||||
// GET /api/external-service/status?service=mytischtennis - Check status
|
||||
router.get('/status', externalServiceController.getStatus);
|
||||
|
||||
// POST /api/external-service/account - Create or update account
|
||||
router.post('/account', externalServiceController.upsertAccount);
|
||||
|
||||
// DELETE /api/external-service/account?service=mytischtennis - Delete account
|
||||
router.delete('/account', externalServiceController.deleteAccount);
|
||||
|
||||
// POST /api/external-service/verify - Verify login
|
||||
router.post('/verify', externalServiceController.verifyLogin);
|
||||
|
||||
// GET /api/external-service/session?service=mytischtennis - Get stored session
|
||||
router.get('/session', externalServiceController.getSession);
|
||||
|
||||
// HeTTV specific routes
|
||||
// GET /api/external-service/hettv/main-page - Load HeTTV main page and find downloads
|
||||
router.get('/hettv/main-page', externalServiceController.loadHettvMainPage);
|
||||
|
||||
// POST /api/external-service/hettv/download-page - Load specific HeTTV download page
|
||||
router.post('/hettv/download-page', externalServiceController.loadHettvDownloadPage);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import express from 'express';
|
||||
import myTischtennisController from '../controllers/myTischtennisController.js';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(authenticate);
|
||||
|
||||
// GET /api/mytischtennis/account - Get account
|
||||
router.get('/account', myTischtennisController.getAccount);
|
||||
|
||||
// GET /api/mytischtennis/status - Check status
|
||||
router.get('/status', myTischtennisController.getStatus);
|
||||
|
||||
// POST /api/mytischtennis/account - Create or update account
|
||||
router.post('/account', myTischtennisController.upsertAccount);
|
||||
|
||||
// DELETE /api/mytischtennis/account - Delete account
|
||||
router.delete('/account', myTischtennisController.deleteAccount);
|
||||
|
||||
// POST /api/mytischtennis/verify - Verify login
|
||||
router.post('/verify', myTischtennisController.verifyLogin);
|
||||
|
||||
// GET /api/mytischtennis/session - Get stored session
|
||||
router.get('/session', myTischtennisController.getSession);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -3,6 +3,7 @@ import DiaryDate from '../models/DiaryDates.js';
|
||||
import Member from '../models/Member.js';
|
||||
import { checkAccess, getUserByToken} from '../utils/userUtils.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class AccidentService {
|
||||
async createAccident(userToken, clubId, memberId, diaryDateId, accident) {
|
||||
await checkAccess(userToken, clubId);
|
||||
@@ -14,7 +15,7 @@ class AccidentService {
|
||||
if (!member || member.clubId != clubId) {
|
||||
throw new Error('Member not found');
|
||||
}
|
||||
console.log(diaryDateId);
|
||||
devLog(diaryDateId);
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
if (!diaryDate || diaryDate.clubId != clubId) {
|
||||
throw new Error('Diary date not found');
|
||||
|
||||
@@ -4,6 +4,7 @@ import User from '../models/User.js';
|
||||
import UserToken from '../models/UserToken.js';
|
||||
import { sendActivationEmail } from './emailService.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
const register = async (email, password) => {
|
||||
try {
|
||||
const activationCode = Math.random().toString(36).substring(2, 15);
|
||||
@@ -11,7 +12,7 @@ const register = async (email, password) => {
|
||||
await sendActivationEmail(email, activationCode);
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,12 +6,13 @@ import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class DiaryDateActivityService {
|
||||
|
||||
async createActivity(userToken, clubId, data) {
|
||||
console.log('[DiaryDateActivityService::createActivity] - check user access');
|
||||
devLog('[DiaryDateActivityService::createActivity] - check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryDateActivityService::createActivity] - add: ', data);
|
||||
devLog('[DiaryDateActivityService::createActivity] - add: ', data);
|
||||
const { activity, ...restData } = data;
|
||||
// Versuche, die PredefinedActivity robust zu finden:
|
||||
// 1) per übergebener ID
|
||||
@@ -59,23 +60,23 @@ class DiaryDateActivityService {
|
||||
});
|
||||
const newOrderId = maxOrderId !== null ? maxOrderId + 1 : 1;
|
||||
restData.orderId = newOrderId;
|
||||
console.log('[DiaryDateActivityService::createActivity] - create diary date activity');
|
||||
devLog('[DiaryDateActivityService::createActivity] - create diary date activity');
|
||||
return await DiaryDateActivity.create(restData);
|
||||
}
|
||||
|
||||
async updateActivity(userToken, clubId, id, data) {
|
||||
console.log('[DiaryDateActivityService::updateActivity] - check user access');
|
||||
devLog('[DiaryDateActivityService::updateActivity] - check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryDateActivityService::updateActivity] - load activity', id);
|
||||
devLog('[DiaryDateActivityService::updateActivity] - load activity', id);
|
||||
const activity = await DiaryDateActivity.findByPk(id);
|
||||
if (!activity) {
|
||||
console.log('[DiaryDateActivityService::updateActivity] - activity not found');
|
||||
devLog('[DiaryDateActivityService::updateActivity] - activity not found');
|
||||
throw new Error('Activity not found');
|
||||
}
|
||||
|
||||
// Wenn customActivityName gesendet wird, müssen wir die PredefinedActivity behandeln
|
||||
if (data.customActivityName) {
|
||||
console.log('[DiaryDateActivityService::updateActivity] - handling customActivityName:', data.customActivityName);
|
||||
devLog('[DiaryDateActivityService::updateActivity] - handling customActivityName:', data.customActivityName);
|
||||
|
||||
// Suche nach einer existierenden PredefinedActivity mit diesem Namen
|
||||
let predefinedActivity = await PredefinedActivity.findOne({
|
||||
@@ -84,7 +85,7 @@ class DiaryDateActivityService {
|
||||
|
||||
if (!predefinedActivity) {
|
||||
// Erstelle eine neue PredefinedActivity
|
||||
console.log('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity');
|
||||
devLog('[DiaryDateActivityService::updateActivity] - creating new PredefinedActivity');
|
||||
predefinedActivity = await PredefinedActivity.create({
|
||||
name: data.customActivityName,
|
||||
description: data.description || '',
|
||||
@@ -99,7 +100,7 @@ class DiaryDateActivityService {
|
||||
delete data.customActivityName;
|
||||
}
|
||||
|
||||
console.log('[DiaryDateActivityService::updateActivity] - update activity', clubId, id, data, JSON.stringify(data));
|
||||
devLog('[DiaryDateActivityService::updateActivity] - update activity', clubId, id, data, JSON.stringify(data));
|
||||
return await activity.update(data);
|
||||
}
|
||||
|
||||
@@ -113,22 +114,22 @@ class DiaryDateActivityService {
|
||||
}
|
||||
|
||||
async updateActivityOrder(userToken, clubId, id, newOrderId) {
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Start update for activity id: ${id}`);
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - User token: ${userToken}, Club id: ${clubId}, New order id: ${newOrderId}`);
|
||||
console.log('[DiaryDateActivityService::updateActivityOrder] - Checking user access');
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Start update for activity id: ${id}`);
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - User token: ${userToken}, Club id: ${clubId}, New order id: ${newOrderId}`);
|
||||
devLog('[DiaryDateActivityService::updateActivityOrder] - Checking user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryDateActivityService::updateActivityOrder] - User access confirmed');
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Finding activity with id: ${id}`);
|
||||
devLog('[DiaryDateActivityService::updateActivityOrder] - User access confirmed');
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Finding activity with id: ${id}`);
|
||||
const activity = await DiaryDateActivity.findByPk(id);
|
||||
if (!activity) {
|
||||
console.error('[DiaryDateActivityService::updateActivityOrder] - Activity not found, throwing error');
|
||||
throw new Error('Activity not found');
|
||||
}
|
||||
console.log('[DiaryDateActivityService::updateActivityOrder] - Activity found:', activity);
|
||||
devLog('[DiaryDateActivityService::updateActivityOrder] - Activity found:', activity);
|
||||
const currentOrderId = activity.orderId;
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Current order id: ${currentOrderId}`);
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Current order id: ${currentOrderId}`);
|
||||
if (newOrderId < currentOrderId) {
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Shifting items down. Moving activities with orderId between ${newOrderId} and ${currentOrderId - 1}`);
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Shifting items down. Moving activities with orderId between ${newOrderId} and ${currentOrderId - 1}`);
|
||||
await DiaryDateActivity.increment(
|
||||
{ orderId: 1 },
|
||||
{
|
||||
@@ -138,9 +139,9 @@ class DiaryDateActivityService {
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Items shifted down`);
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Items shifted down`);
|
||||
} else if (newOrderId > currentOrderId) {
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Shifting items up. Moving activities with orderId between ${currentOrderId + 1} and ${newOrderId}`);
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Shifting items up. Moving activities with orderId between ${currentOrderId + 1} and ${newOrderId}`);
|
||||
await DiaryDateActivity.decrement(
|
||||
{ orderId: 1 },
|
||||
{
|
||||
@@ -150,16 +151,16 @@ class DiaryDateActivityService {
|
||||
},
|
||||
}
|
||||
);
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Items shifted up`);
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Items shifted up`);
|
||||
} else {
|
||||
console.log('[DiaryDateActivityService::updateActivityOrder] - New order id is the same as the current order id. No shift required.');
|
||||
devLog('[DiaryDateActivityService::updateActivityOrder] - New order id is the same as the current order id. No shift required.');
|
||||
}
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Setting new order id for activity id: ${id}`);
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Setting new order id for activity id: ${id}`);
|
||||
activity.orderId = newOrderId;
|
||||
console.log('[DiaryDateActivityService::updateActivityOrder] - Saving activity with new order id');
|
||||
devLog('[DiaryDateActivityService::updateActivityOrder] - Saving activity with new order id');
|
||||
const savedActivity = await activity.save();
|
||||
console.log('[DiaryDateActivityService::updateActivityOrder] - Activity saved:', savedActivity);
|
||||
console.log(`[DiaryDateActivityService::updateActivityOrder] - Finished update for activity id: ${id}`);
|
||||
devLog('[DiaryDateActivityService::updateActivityOrder] - Activity saved:', savedActivity);
|
||||
devLog(`[DiaryDateActivityService::updateActivityOrder] - Finished update for activity id: ${id}`);
|
||||
return savedActivity;
|
||||
}
|
||||
|
||||
@@ -256,9 +257,9 @@ class DiaryDateActivityService {
|
||||
}
|
||||
|
||||
async addGroupActivity(userToken, clubId, diaryDateId, groupId, activity) {
|
||||
console.log('[DiaryDateActivityService::addGroupActivity] Check user access');
|
||||
devLog('[DiaryDateActivityService::addGroupActivity] Check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryDateActivityService::addGroupActivity] Check diary date');
|
||||
devLog('[DiaryDateActivityService::addGroupActivity] Check diary date');
|
||||
const diaryDateActivity = await DiaryDateActivity.findOne({
|
||||
where: {
|
||||
diaryDateId,
|
||||
@@ -271,26 +272,26 @@ class DiaryDateActivityService {
|
||||
console.error('[DiaryDateActivityService::addGroupActivity] Activity not found');
|
||||
throw new Error('Activity not found');
|
||||
}
|
||||
console.log('[DiaryDateActivityService::addGroupActivity] Check group');
|
||||
devLog('[DiaryDateActivityService::addGroupActivity] Check group');
|
||||
const group = await Group.findByPk(groupId);
|
||||
if (!group || group.diaryDateId !== diaryDateActivity.diaryDateId) {
|
||||
console.error('[DiaryDateActivityService::addGroupActivity] Group and date don\'t fit');
|
||||
throw new Error('Group isn\'t related to date');
|
||||
}
|
||||
console.log('[DiaryDateActivityService::addGroupActivity] Get predefined activity');
|
||||
devLog('[DiaryDateActivityService::addGroupActivity] Get predefined activity');
|
||||
const [predefinedActivity, created] = await PredefinedActivity.findOrCreate({
|
||||
where: {
|
||||
name: activity
|
||||
}
|
||||
});
|
||||
console.log('[DiaryDateActivityService::addGroupActivity] Add group activity');
|
||||
console.log(predefinedActivity);
|
||||
devLog('[DiaryDateActivityService::addGroupActivity] Add group activity');
|
||||
devLog(predefinedActivity);
|
||||
const activityData = {
|
||||
diaryDateActivity: diaryDateActivity.id,
|
||||
groupId: groupId,
|
||||
customActivity: predefinedActivity.id
|
||||
}
|
||||
console.log(activityData);
|
||||
devLog(activityData);
|
||||
return await GroupActivity.create(activityData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Member from "../models/Member.js";
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import { Op, literal } from "sequelize";
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class DiaryDateTagService {
|
||||
async getDiaryDateMemberTags(userToken, clubId, memberId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
@@ -35,7 +36,7 @@ class DiaryDateTagService {
|
||||
}
|
||||
|
||||
async addDiaryDateTag(userToken, clubId, diaryDateId, memberId, tag) {
|
||||
console.log(userToken, clubId, diaryDateId, memberId, tag);
|
||||
devLog(userToken, clubId, diaryDateId, memberId, tag);
|
||||
await checkAccess(userToken, clubId);
|
||||
const tagObject = await DiaryTag.findOne({ where: { id: tag.id } });
|
||||
if (!tagObject) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import DiaryMemberTag from '../models/DiaryMemberTag.js';
|
||||
import { DiaryTag } from '../models/DiaryTag.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class DiaryMemberService {
|
||||
async addNoteToMember(userToken, clubId, diaryDateId, memberId, content) {
|
||||
await checkAccess(userToken, clubId);
|
||||
@@ -14,7 +15,7 @@ class DiaryMemberService {
|
||||
|
||||
async getNotesForMember(userToken, clubId, diaryDateId, memberId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log(clubId, diaryDateId, memberId);
|
||||
devLog(clubId, diaryDateId, memberId);
|
||||
return await DiaryMemberNote.findAll({ where: { diaryDateId, memberId }, order: [['createdAt', 'DESC']] });
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ class DiaryMemberService {
|
||||
if (tagLink) {
|
||||
await tagLink.destroy();
|
||||
} else {
|
||||
console.log(diaryDateId, memberId, tagId);
|
||||
devLog(diaryDateId, memberId, tagId);
|
||||
throw new Error('Das Tag ist nicht verknüpft.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,17 @@ import DiaryDateTag from '../models/DiaryDateTag.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class DiaryService {
|
||||
async getDatesForClub(userToken, clubId) {
|
||||
console.log('[DiaryService::getDatesForClub] - Check user access');
|
||||
devLog('[DiaryService::getDatesForClub] - Check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryService::getDatesForClub] - Validate club existence');
|
||||
devLog('[DiaryService::getDatesForClub] - Validate club existence');
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (!club) {
|
||||
throw new HttpError('Club not found', 404);
|
||||
}
|
||||
console.log('[DiaryService::getDatesForClub] - Load diary dates');
|
||||
devLog('[DiaryService::getDatesForClub] - Load diary dates');
|
||||
const dates = await DiaryDate.findAll({
|
||||
where: { clubId },
|
||||
include: [
|
||||
@@ -29,14 +30,14 @@ class DiaryService {
|
||||
}
|
||||
|
||||
async createDateForClub(userToken, clubId, date, trainingStart, trainingEnd) {
|
||||
console.log('[DiaryService::createDateForClub] - Check user access');
|
||||
devLog('[DiaryService::createDateForClub] - Check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryService::createDateForClub] - Validate club existence');
|
||||
devLog('[DiaryService::createDateForClub] - Validate club existence');
|
||||
const club = await Club.findByPk(clubId);
|
||||
if (!club) {
|
||||
throw new HttpError('Club not found', 404);
|
||||
}
|
||||
console.log('[DiaryService::createDateForClub] - Validate date');
|
||||
devLog('[DiaryService::createDateForClub] - Validate date');
|
||||
const parsedDate = new Date(date);
|
||||
if (isNaN(parsedDate.getTime())) {
|
||||
throw new HttpError('Invalid date format', 400);
|
||||
@@ -44,7 +45,7 @@ class DiaryService {
|
||||
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
|
||||
throw new HttpError('Training start time must be before training end time', 400);
|
||||
}
|
||||
console.log('[DiaryService::createDateForClub] - Create new diary date');
|
||||
devLog('[DiaryService::createDateForClub] - Create new diary date');
|
||||
const newDate = await DiaryDate.create({
|
||||
date: parsedDate,
|
||||
clubId,
|
||||
@@ -56,9 +57,9 @@ class DiaryService {
|
||||
}
|
||||
|
||||
async updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd) {
|
||||
console.log('[DiaryService::updateTrainingTimes] - Check user access');
|
||||
devLog('[DiaryService::updateTrainingTimes] - Check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryService::updateTrainingTimes] - Validate date');
|
||||
devLog('[DiaryService::updateTrainingTimes] - Validate date');
|
||||
const diaryDate = await DiaryDate.findOne({ where: { clubId, id: dateId } });
|
||||
if (!diaryDate) {
|
||||
throw new HttpError('Diary entry not found', 404);
|
||||
@@ -66,7 +67,7 @@ class DiaryService {
|
||||
if (trainingStart && trainingEnd && trainingStart >= trainingEnd) {
|
||||
throw new HttpError('Training start time must be before training end time', 400);
|
||||
}
|
||||
console.log('[DiaryService::updateTrainingTimes] - Update training times');
|
||||
devLog('[DiaryService::updateTrainingTimes] - Update training times');
|
||||
diaryDate.trainingStart = trainingStart || null;
|
||||
diaryDate.trainingEnd = trainingEnd || null;
|
||||
await diaryDate.save();
|
||||
@@ -74,14 +75,14 @@ class DiaryService {
|
||||
}
|
||||
|
||||
async addNoteToDate(userToken, diaryDateId, content) {
|
||||
console.log('[DiaryService::addNoteToDate] - Add note');
|
||||
devLog('[DiaryService::addNoteToDate] - Add note');
|
||||
await checkAccess(userToken, diaryDateId);
|
||||
await DiaryNote.create({ diaryDateId, content });
|
||||
return await DiaryNote.findAll({ where: { diaryDateId }, order: [['createdAt', 'DESC']] });
|
||||
}
|
||||
|
||||
async deleteNoteFromDate(userToken, noteId) {
|
||||
console.log('[DiaryService::deleteNoteFromDate] - Delete note');
|
||||
devLog('[DiaryService::deleteNoteFromDate] - Delete note');
|
||||
const note = await DiaryNote.findByPk(noteId);
|
||||
if (!note) {
|
||||
throw new HttpError('Note not found', 404);
|
||||
@@ -92,7 +93,7 @@ class DiaryService {
|
||||
}
|
||||
|
||||
async addTagToDate(userToken, diaryDateId, tagName) {
|
||||
console.log('[DiaryService::addTagToDate] - Add tag');
|
||||
devLog('[DiaryService::addTagToDate] - Add tag');
|
||||
await checkAccess(userToken, diaryDateId);
|
||||
let tag = await DiaryTag.findOne({ where: { name: tagName } });
|
||||
if (!tag) {
|
||||
@@ -105,29 +106,29 @@ class DiaryService {
|
||||
|
||||
async addTagToDiaryDate(userToken, clubId, diaryDateId, tagId) {
|
||||
checkAccess(userToken, clubId);
|
||||
console.log(`[DiaryService::addTagToDiaryDate] - diaryDateId: ${diaryDateId}, tagId: ${tagId}`);
|
||||
devLog(`[DiaryService::addTagToDiaryDate] - diaryDateId: ${diaryDateId}, tagId: ${tagId}`);
|
||||
const diaryDate = await DiaryDate.findByPk(diaryDateId);
|
||||
if (!diaryDate) {
|
||||
throw new HttpError('DiaryDate not found', 404);
|
||||
}
|
||||
console.log('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
|
||||
devLog('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
|
||||
const existingEntry = await DiaryDateTag.findOne({
|
||||
where: { diaryDateId, tagId }
|
||||
});
|
||||
if (existingEntry) {
|
||||
return;
|
||||
}
|
||||
console.log('[DiaryService::addTagToDiaryDate] - Tag not found, creating new entry');
|
||||
devLog('[DiaryService::addTagToDiaryDate] - Tag not found, creating new entry');
|
||||
const tag = await DiaryTag.findByPk(tagId);
|
||||
if (!tag) {
|
||||
throw new HttpError('Tag not found', 404);
|
||||
}
|
||||
console.log('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
|
||||
devLog('[DiaryService::addTagToDiaryDate] - Add tag to diary date');
|
||||
await DiaryDateTag.create({
|
||||
diaryDateId,
|
||||
tagId
|
||||
});
|
||||
console.log('[DiaryService::addTagToDiaryDate] - Get tags');
|
||||
devLog('[DiaryService::addTagToDiaryDate] - Get tags');
|
||||
const tags = await DiaryDateTag.findAll({ where: {
|
||||
diaryDateId: diaryDateId },
|
||||
include: {
|
||||
@@ -135,12 +136,12 @@ class DiaryService {
|
||||
as: 'tag'
|
||||
}
|
||||
});
|
||||
console.log(tags);
|
||||
devLog(tags);
|
||||
return tags.map(tag => tag.tag);
|
||||
}
|
||||
|
||||
async getDiaryNotesForDateAndMember(diaryDateId, memberId) {
|
||||
console.log('[DiaryService::getDiaryNotesForDateAndMember] - Fetching notes');
|
||||
devLog('[DiaryService::getDiaryNotesForDateAndMember] - Fetching notes');
|
||||
return await DiaryNote.findAll({
|
||||
where: { diaryDateId, memberId },
|
||||
order: [['createdAt', 'DESC']]
|
||||
@@ -153,19 +154,19 @@ class DiaryService {
|
||||
}
|
||||
|
||||
async removeDateForClub(userToken, clubId, dateId) {
|
||||
console.log('[DiaryService::removeDateForClub] - Check user access');
|
||||
devLog('[DiaryService::removeDateForClub] - Check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[DiaryService::removeDateForClub] - Validate date');
|
||||
devLog('[DiaryService::removeDateForClub] - Validate date');
|
||||
const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } });
|
||||
if (!diaryDate) {
|
||||
throw new HttpError('Diary entry not found', 404);
|
||||
}
|
||||
console.log('[DiaryService::removeDateForClub] - Check for activities');
|
||||
devLog('[DiaryService::removeDateForClub] - Check for activities');
|
||||
const activityCount = await DiaryDateActivity.count({ where: { diaryDateId: dateId } });
|
||||
if (activityCount > 0) {
|
||||
throw new HttpError('Cannot delete date with activities', 409);
|
||||
}
|
||||
console.log('[DiaryService::removeDateForClub] - Delete diary date');
|
||||
devLog('[DiaryService::removeDateForClub] - Delete diary date');
|
||||
await diaryDate.destroy();
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
347
backend/services/externalServiceService.js
Normal file
347
backend/services/externalServiceService.js
Normal file
@@ -0,0 +1,347 @@
|
||||
import ExternalServiceAccount from '../models/ExternalServiceAccount.js';
|
||||
import User from '../models/User.js';
|
||||
import myTischtennisClient from '../clients/myTischtennisClient.js';
|
||||
import hettvClient from '../clients/hettvClient.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
class ExternalServiceService {
|
||||
/**
|
||||
* Get the appropriate client for a service
|
||||
*/
|
||||
getClientForService(service) {
|
||||
switch (service) {
|
||||
case 'mytischtennis':
|
||||
return myTischtennisClient;
|
||||
case 'hettv':
|
||||
return hettvClient;
|
||||
default:
|
||||
throw new HttpError(400, `Unbekannter Service: ${service}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account for user and service
|
||||
*/
|
||||
async getAccount(userId, service = 'mytischtennis') {
|
||||
const account = await ExternalServiceAccount.findOne({
|
||||
where: { userId, service },
|
||||
attributes: ['id', 'service', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
|
||||
});
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update external service account
|
||||
*/
|
||||
async upsertAccount(userId, email, password, savePassword, userPassword, service = 'mytischtennis') {
|
||||
// Verify user's app password
|
||||
const user = await User.findByPk(userId);
|
||||
if (!user) {
|
||||
throw new HttpError(404, 'Benutzer nicht gefunden');
|
||||
}
|
||||
|
||||
let loginResult = null;
|
||||
|
||||
// Wenn ein Passwort gesetzt/geändert wird, App-Passwort verifizieren
|
||||
if (password) {
|
||||
const isValidPassword = await user.validatePassword(userPassword);
|
||||
if (!isValidPassword) {
|
||||
throw new HttpError(401, 'Ungültiges Passwort');
|
||||
}
|
||||
|
||||
// Login-Versuch beim entsprechenden Service
|
||||
const client = this.getClientForService(service);
|
||||
loginResult = await client.login(email, password);
|
||||
if (!loginResult.success) {
|
||||
throw new HttpError(401, loginResult.error || `${service}-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create account
|
||||
let account = await ExternalServiceAccount.findOne({ where: { userId, service } });
|
||||
|
||||
const now = new Date();
|
||||
|
||||
if (account) {
|
||||
// Update existing
|
||||
account.email = email;
|
||||
account.savePassword = savePassword;
|
||||
|
||||
if (password && savePassword) {
|
||||
account.setPassword(password);
|
||||
} else if (!savePassword) {
|
||||
account.encryptedPassword = null;
|
||||
}
|
||||
|
||||
if (loginResult && loginResult.success) {
|
||||
account.lastLoginAttempt = now;
|
||||
account.lastLoginSuccess = now;
|
||||
account.accessToken = loginResult.accessToken;
|
||||
account.refreshToken = loginResult.refreshToken;
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
account.userData = loginResult.user;
|
||||
|
||||
// Hole Club-ID und Federation (nur für myTischtennis)
|
||||
if (service === 'mytischtennis') {
|
||||
console.log('[externalServiceService] - Getting user profile...');
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
console.log('[externalServiceService] - Profile result:', {
|
||||
success: profileResult.success,
|
||||
clubId: profileResult.clubId,
|
||||
clubName: profileResult.clubName,
|
||||
fedNickname: profileResult.fedNickname
|
||||
});
|
||||
|
||||
if (profileResult.success) {
|
||||
account.clubId = profileResult.clubId;
|
||||
account.clubName = profileResult.clubName;
|
||||
account.fedNickname = profileResult.fedNickname;
|
||||
console.log('[externalServiceService] - Updated account with club data');
|
||||
} else {
|
||||
console.error('[externalServiceService] - Failed to get profile:', profileResult.error);
|
||||
}
|
||||
}
|
||||
} else if (password) {
|
||||
account.lastLoginAttempt = now;
|
||||
}
|
||||
|
||||
console.log('[externalServiceService] - Speichere Account (update).');
|
||||
try {
|
||||
await account.save();
|
||||
} catch (e) {
|
||||
console.error('[externalServiceService] - Fehler beim Speichern (update):', e.message, e.parent?.sqlMessage);
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
// Create new
|
||||
const accountData = {
|
||||
userId,
|
||||
service,
|
||||
email,
|
||||
savePassword,
|
||||
lastLoginAttempt: password ? now : null,
|
||||
lastLoginSuccess: loginResult?.success ? now : null
|
||||
};
|
||||
|
||||
if (loginResult && loginResult.success) {
|
||||
accountData.accessToken = loginResult.accessToken;
|
||||
accountData.refreshToken = loginResult.refreshToken;
|
||||
accountData.expiresAt = loginResult.expiresAt;
|
||||
accountData.cookie = loginResult.cookie;
|
||||
accountData.userData = loginResult.user;
|
||||
|
||||
// Hole Club-/Verbandsdaten nur für myTischtennis
|
||||
if (service === 'mytischtennis') {
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
if (profileResult.success) {
|
||||
accountData.clubId = profileResult.clubId;
|
||||
accountData.clubName = profileResult.clubName;
|
||||
accountData.fedNickname = profileResult.fedNickname;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[externalServiceService] - Erstelle Account (create).');
|
||||
try {
|
||||
account = await ExternalServiceAccount.create(accountData);
|
||||
} catch (e) {
|
||||
console.error('[externalServiceService] - Fehler beim Erstellen (create):', e.message, e.parent?.sqlMessage);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (password && savePassword) {
|
||||
account.setPassword(password);
|
||||
console.log('[externalServiceService] - Speichere Passwort (nach create).');
|
||||
try {
|
||||
await account.save();
|
||||
} catch (e) {
|
||||
console.error('[externalServiceService] - Fehler beim Speichern (nach create):', e.message, e.parent?.sqlMessage);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
savePassword: account.savePassword,
|
||||
lastLoginAttempt: account.lastLoginAttempt,
|
||||
lastLoginSuccess: account.lastLoginSuccess,
|
||||
expiresAt: account.expiresAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete external service account
|
||||
*/
|
||||
async deleteAccount(userId, service = 'mytischtennis') {
|
||||
const deleted = await ExternalServiceAccount.destroy({
|
||||
where: { userId, service }
|
||||
});
|
||||
return deleted > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify login with stored or provided credentials
|
||||
*/
|
||||
async verifyLogin(userId, providedPassword = null, service = 'mytischtennis') {
|
||||
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
|
||||
|
||||
if (!account) {
|
||||
throw new HttpError(404, `Kein ${service}-Account verknüpft`);
|
||||
}
|
||||
|
||||
let password = providedPassword;
|
||||
|
||||
// Wenn kein Passwort übergeben wurde, versuche gespeichertes Passwort zu verwenden
|
||||
if (!password) {
|
||||
if (!account.savePassword || !account.encryptedPassword) {
|
||||
throw new HttpError(400, 'Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.');
|
||||
}
|
||||
password = account.getPassword();
|
||||
}
|
||||
|
||||
// Login-Versuch
|
||||
const now = new Date();
|
||||
account.lastLoginAttempt = now;
|
||||
const client = this.getClientForService(service);
|
||||
const loginResult = await client.login(account.email, password);
|
||||
|
||||
if (loginResult.success) {
|
||||
account.lastLoginSuccess = now;
|
||||
account.accessToken = loginResult.accessToken;
|
||||
account.refreshToken = loginResult.refreshToken;
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
account.userData = loginResult.user;
|
||||
|
||||
// Hole Club-/Verbandsdaten nur für myTischtennis
|
||||
if (service === 'mytischtennis') {
|
||||
console.log('[externalServiceService] - Getting myTischtennis user profile...');
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
console.log('[externalServiceService] - Profile result:', {
|
||||
success: profileResult.success,
|
||||
clubId: profileResult.clubId,
|
||||
clubName: profileResult.clubName,
|
||||
fedNickname: profileResult.fedNickname
|
||||
});
|
||||
|
||||
if (profileResult.success) {
|
||||
account.clubId = profileResult.clubId;
|
||||
account.clubName = profileResult.clubName;
|
||||
account.fedNickname = profileResult.fedNickname;
|
||||
console.log('[externalServiceService] - Updated account with club data');
|
||||
} else {
|
||||
console.error('[externalServiceService] - Failed to get profile:', profileResult.error);
|
||||
}
|
||||
}
|
||||
|
||||
await account.save();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
accessToken: loginResult.accessToken,
|
||||
refreshToken: loginResult.refreshToken,
|
||||
expiresAt: loginResult.expiresAt,
|
||||
user: loginResult.user,
|
||||
clubId: account.clubId,
|
||||
clubName: account.clubName
|
||||
};
|
||||
} else {
|
||||
await account.save(); // Save lastLoginAttempt
|
||||
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if account is configured and ready
|
||||
*/
|
||||
async checkAccountStatus(userId, service = 'mytischtennis') {
|
||||
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
|
||||
|
||||
return {
|
||||
exists: !!account,
|
||||
hasEmail: !!account?.email,
|
||||
hasPassword: !!(account?.savePassword && account?.encryptedPassword),
|
||||
hasValidSession: !!account?.accessToken && account?.expiresAt > Date.now() / 1000,
|
||||
needsConfiguration: !account || !account.email,
|
||||
needsPassword: !!account && (!account.savePassword || !account.encryptedPassword)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored session for user (for authenticated API requests)
|
||||
*/
|
||||
async getSession(userId, service = 'mytischtennis') {
|
||||
const account = await ExternalServiceAccount.findOne({ where: { userId, service } });
|
||||
|
||||
if (!account) {
|
||||
throw new HttpError(404, `Kein ${service}-Account verknüpft`);
|
||||
}
|
||||
|
||||
// Check if session is valid
|
||||
if (service === 'hettv') {
|
||||
// HeTTV nutzt Cookie-basierte Session, kein expiresAt verfügbar
|
||||
if (!account.cookie) {
|
||||
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
|
||||
}
|
||||
} else {
|
||||
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
|
||||
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: account.accessToken,
|
||||
refreshToken: account.refreshToken,
|
||||
cookie: account.cookie,
|
||||
expiresAt: account.expiresAt,
|
||||
userData: account.userData
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load HeTTV main page and find download links
|
||||
*/
|
||||
async loadHettvMainPage(userId) {
|
||||
const client = this.getClientForService('hettv');
|
||||
let session = await this.getSession(userId, 'hettv');
|
||||
|
||||
const finalUrl = session.userData?.finalUrl || null;
|
||||
let result = await client.getMainPageWithDownloads(session.cookie, finalUrl);
|
||||
|
||||
// Wenn Session abgelaufen: versuche automatischen Re-Login mit gespeichertem Passwort
|
||||
if (!result.success && result.status === 401) {
|
||||
const account = await ExternalServiceAccount.findOne({ where: { userId, service: 'hettv' } });
|
||||
if (account && account.savePassword && account.encryptedPassword) {
|
||||
const password = account.getPassword();
|
||||
const login = await client.login(account.email, password);
|
||||
if (login.success) {
|
||||
// Session aktualisieren
|
||||
account.cookie = login.cookie;
|
||||
account.userData = login.user;
|
||||
await account.save();
|
||||
// Erneut versuchen
|
||||
result = await client.getMainPageWithDownloads(login.cookie, login.user?.finalUrl || null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load specific HeTTV download page
|
||||
*/
|
||||
async loadHettvDownloadPage(userId, downloadUrl) {
|
||||
const session = await this.getSession(userId, 'hettv');
|
||||
const client = this.getClientForService('hettv');
|
||||
|
||||
const finalUrl = session.userData?.finalUrl || null;
|
||||
return await client.loadDownloadPage(downloadUrl, session.cookie, finalUrl);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ExternalServiceService();
|
||||
|
||||
@@ -3,6 +3,7 @@ import DiaryDate from '../models/DiaryDates.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import Group from '../models/Group.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class GroupService {
|
||||
|
||||
async checkDiaryDateToClub(clubId, dateId) {
|
||||
@@ -40,7 +41,7 @@ class GroupService {
|
||||
}
|
||||
|
||||
async changeGroup(userToken, groupId, clubId, dateId, name, lead) {
|
||||
console.log("changeGroup: ", groupId, clubId, dateId, name, lead);
|
||||
devLog("changeGroup: ", groupId, clubId, dateId, name, lead);
|
||||
await checkAccess(userToken, clubId);
|
||||
await this.checkDiaryDateToClub(clubId, dateId);
|
||||
const group = await Group.findOne({
|
||||
|
||||
@@ -10,6 +10,7 @@ import Team from '../models/Team.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class MatchService {
|
||||
|
||||
generateSeasonString(date = new Date()) {
|
||||
@@ -78,7 +79,7 @@ class MatchService {
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.log('Error during CSV import:', error);
|
||||
devLog('Error during CSV import:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import MemberNote from '../models/MemberNote.js';
|
||||
import { checkAccess } from '../utils/userUtils.js';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class MemberNoteService {
|
||||
async addNoteToMember(userToken, clubId, memberId, content) {
|
||||
await checkAccess(userToken, clubId);
|
||||
@@ -8,7 +9,7 @@ class MemberNoteService {
|
||||
}
|
||||
|
||||
async getNotesForMember(userToken, clubId, memberId) {
|
||||
console.log(userToken, clubId);
|
||||
devLog(userToken, clubId);
|
||||
await checkAccess(userToken, clubId);
|
||||
return await MemberNote.findAll({
|
||||
where: { memberId },
|
||||
|
||||
@@ -6,13 +6,14 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class MemberService {
|
||||
async getApprovalRequests(userToken, clubId) {
|
||||
console.log('[MemberService::getApprovalRequest] - Check user access');
|
||||
devLog('[MemberService::getApprovalRequest] - Check user access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[MemberService::getApprovalRequest] - Load user');
|
||||
devLog('[MemberService::getApprovalRequest] - Load user');
|
||||
const user = await getUserByToken(userToken);
|
||||
console.log('[MemberService::getApprovalRequest] - Load userclub');
|
||||
devLog('[MemberService::getApprovalRequest] - Load userclub');
|
||||
return await UserClub.findAll({
|
||||
where: {
|
||||
clubId: clubId,
|
||||
@@ -23,9 +24,9 @@ class MemberService {
|
||||
}
|
||||
|
||||
async getClubMembers(userToken, clubId, showAll) {
|
||||
console.log('[getClubMembers] - Check access');
|
||||
devLog('[getClubMembers] - Check access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[getClubMembers] - Find members');
|
||||
devLog('[getClubMembers] - Find members');
|
||||
const where = {
|
||||
clubId: clubId
|
||||
};
|
||||
@@ -44,7 +45,7 @@ class MemberService {
|
||||
});
|
||||
})
|
||||
.then(membersWithImageStatus => {
|
||||
console.log('[getClubMembers] - return members');
|
||||
devLog('[getClubMembers] - return members');
|
||||
return membersWithImageStatus;
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -56,15 +57,15 @@ class MemberService {
|
||||
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate, phone, email, active = true, testMembership = false,
|
||||
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null) {
|
||||
try {
|
||||
console.log('[setClubMembers] - Check access');
|
||||
devLog('[setClubMembers] - Check access');
|
||||
await checkAccess(userToken, clubId);
|
||||
console.log('[setClubMembers] - set default member');
|
||||
devLog('[setClubMembers] - set default member');
|
||||
let member = null;
|
||||
console.log('[setClubMembers] - load member if possible');
|
||||
devLog('[setClubMembers] - load member if possible');
|
||||
if (memberId) {
|
||||
member = await Member.findOne({ where: { id: memberId } });
|
||||
}
|
||||
console.log('[setClubMembers] - set member');
|
||||
devLog('[setClubMembers] - set member');
|
||||
if (member) {
|
||||
member.firstName = firstName;
|
||||
member.lastName = lastName;
|
||||
@@ -98,13 +99,13 @@ class MemberService {
|
||||
qttr: qttr,
|
||||
});
|
||||
}
|
||||
console.log('[setClubMembers] - return response');
|
||||
devLog('[setClubMembers] - return response');
|
||||
return {
|
||||
status: 200,
|
||||
response: { result: "success" },
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog(error);
|
||||
return {
|
||||
status: error.statusCode || 500,
|
||||
response: { error: "nocreation" }
|
||||
@@ -114,7 +115,7 @@ class MemberService {
|
||||
|
||||
async uploadMemberImage(userToken, clubId, memberId, imageBuffer) {
|
||||
try {
|
||||
console.log('------>', userToken, clubId, memberId, imageBuffer);
|
||||
devLog('------>', userToken, clubId, memberId, imageBuffer);
|
||||
await checkAccess(userToken, clubId);
|
||||
const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
|
||||
if (!member) {
|
||||
@@ -152,27 +153,27 @@ class MemberService {
|
||||
}
|
||||
|
||||
async updateRatingsFromMyTischtennis(userToken, clubId) {
|
||||
console.log('[updateRatingsFromMyTischtennis] - Check access');
|
||||
devLog('[updateRatingsFromMyTischtennis] - Check access');
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const user = await getUserByToken(userToken);
|
||||
console.log('[updateRatingsFromMyTischtennis] - User:', user.id);
|
||||
devLog('[updateRatingsFromMyTischtennis] - User:', user.id);
|
||||
|
||||
const myTischtennisService = (await import('./myTischtennisService.js')).default;
|
||||
const externalServiceService = (await import('./externalServiceService.js')).default;
|
||||
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
|
||||
|
||||
try {
|
||||
// 1. myTischtennis-Session abrufen
|
||||
console.log('[updateRatingsFromMyTischtennis] - Get session for user', user.id);
|
||||
const session = await myTischtennisService.getSession(user.id);
|
||||
console.log('[updateRatingsFromMyTischtennis] - Session retrieved:', {
|
||||
devLog('[updateRatingsFromMyTischtennis] - Get session for user', user.id);
|
||||
const session = await externalServiceService.getSession(user.id, 'mytischtennis');
|
||||
devLog('[updateRatingsFromMyTischtennis] - Session retrieved:', {
|
||||
hasAccessToken: !!session.accessToken,
|
||||
hasCookie: !!session.cookie,
|
||||
expiresAt: session.expiresAt
|
||||
});
|
||||
|
||||
const account = await myTischtennisService.getAccount(user.id);
|
||||
console.log('[updateRatingsFromMyTischtennis] - Account data:', {
|
||||
const account = await externalServiceService.getAccount(user.id, 'mytischtennis');
|
||||
devLog('[updateRatingsFromMyTischtennis] - Account data:', {
|
||||
id: account?.id,
|
||||
email: account?.email,
|
||||
clubId: account?.clubId,
|
||||
@@ -216,7 +217,7 @@ class MemberService {
|
||||
}
|
||||
|
||||
// 2. Rangliste vom Verein abrufen
|
||||
console.log('[updateRatingsFromMyTischtennis] - Get club rankings', {
|
||||
devLog('[updateRatingsFromMyTischtennis] - Get club rankings', {
|
||||
clubId: account.clubId,
|
||||
fedNickname: account.fedNickname,
|
||||
hasCookie: !!session.cookie
|
||||
@@ -228,7 +229,7 @@ class MemberService {
|
||||
account.fedNickname
|
||||
);
|
||||
|
||||
console.log('[updateRatingsFromMyTischtennis] - Rankings result:', {
|
||||
devLog('[updateRatingsFromMyTischtennis] - Rankings result:', {
|
||||
success: rankings.success,
|
||||
entriesCount: rankings.entries?.length || 0,
|
||||
error: rankings.error
|
||||
@@ -251,9 +252,9 @@ class MemberService {
|
||||
}
|
||||
|
||||
// 3. Alle Mitglieder des Clubs laden
|
||||
console.log('[updateRatingsFromMyTischtennis] - Load club members for clubId:', clubId);
|
||||
devLog('[updateRatingsFromMyTischtennis] - Load club members for clubId:', clubId);
|
||||
const members = await Member.findAll({ where: { clubId } });
|
||||
console.log('[updateRatingsFromMyTischtennis] - Found members:', members.length);
|
||||
devLog('[updateRatingsFromMyTischtennis] - Found members:', members.length);
|
||||
|
||||
let updated = 0;
|
||||
const errors = [];
|
||||
@@ -284,7 +285,7 @@ class MemberService {
|
||||
oldTtr: oldTtr,
|
||||
newTtr: rankingEntry.fedRank
|
||||
});
|
||||
console.log(`[updateRatingsFromMyTischtennis] - Updated ${firstName} ${lastName}: TTR ${oldTtr} → ${rankingEntry.fedRank}`);
|
||||
devLog(`[updateRatingsFromMyTischtennis] - Updated ${firstName} ${lastName}: TTR ${oldTtr} → ${rankingEntry.fedRank}`);
|
||||
} catch (error) {
|
||||
console.error(`[updateRatingsFromMyTischtennis] - Error updating ${firstName} ${lastName}:`, error);
|
||||
errors.push({
|
||||
@@ -294,12 +295,12 @@ class MemberService {
|
||||
}
|
||||
} else {
|
||||
notFound.push(`${firstName} ${lastName}`);
|
||||
console.log(`[updateRatingsFromMyTischtennis] - Not found in rankings: ${firstName} ${lastName}`);
|
||||
devLog(`[updateRatingsFromMyTischtennis] - Not found in rankings: ${firstName} ${lastName}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[updateRatingsFromMyTischtennis] - Update complete');
|
||||
console.log(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
|
||||
devLog('[updateRatingsFromMyTischtennis] - Update complete');
|
||||
devLog(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
|
||||
|
||||
let message = `${updated} Mitglied(er) aktualisiert.`;
|
||||
if (notFound.length > 0) {
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
import MyTischtennis from '../models/MyTischtennis.js';
|
||||
import User from '../models/User.js';
|
||||
import myTischtennisClient from '../clients/myTischtennisClient.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
class MyTischtennisService {
|
||||
/**
|
||||
* Get myTischtennis account for user
|
||||
*/
|
||||
async getAccount(userId) {
|
||||
const account = await MyTischtennis.findOne({
|
||||
where: { userId },
|
||||
attributes: ['id', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
|
||||
});
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update myTischtennis account
|
||||
*/
|
||||
async upsertAccount(userId, email, password, savePassword, userPassword) {
|
||||
// Verify user's app password
|
||||
const user = await User.findByPk(userId);
|
||||
if (!user) {
|
||||
throw new HttpError(404, 'Benutzer nicht gefunden');
|
||||
}
|
||||
|
||||
let loginResult = null;
|
||||
|
||||
// Wenn ein Passwort gesetzt/geändert wird, App-Passwort verifizieren
|
||||
if (password) {
|
||||
const isValidPassword = await user.validatePassword(userPassword);
|
||||
if (!isValidPassword) {
|
||||
throw new HttpError(401, 'Ungültiges Passwort');
|
||||
}
|
||||
|
||||
// Login-Versuch bei myTischtennis
|
||||
loginResult = await myTischtennisClient.login(email, password);
|
||||
if (!loginResult.success) {
|
||||
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.');
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create account
|
||||
let account = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
const now = new Date();
|
||||
|
||||
if (account) {
|
||||
// Update existing
|
||||
account.email = email;
|
||||
account.savePassword = savePassword;
|
||||
|
||||
if (password && savePassword) {
|
||||
account.setPassword(password);
|
||||
} else if (!savePassword) {
|
||||
account.encryptedPassword = null;
|
||||
}
|
||||
|
||||
if (loginResult && loginResult.success) {
|
||||
account.lastLoginAttempt = now;
|
||||
account.lastLoginSuccess = now;
|
||||
account.accessToken = loginResult.accessToken;
|
||||
account.refreshToken = loginResult.refreshToken;
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
account.userData = loginResult.user;
|
||||
|
||||
// Hole Club-ID und Federation
|
||||
console.log('[myTischtennisService] - Getting user profile...');
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
console.log('[myTischtennisService] - Profile result:', {
|
||||
success: profileResult.success,
|
||||
clubId: profileResult.clubId,
|
||||
clubName: profileResult.clubName,
|
||||
fedNickname: profileResult.fedNickname
|
||||
});
|
||||
|
||||
if (profileResult.success) {
|
||||
account.clubId = profileResult.clubId;
|
||||
account.clubName = profileResult.clubName;
|
||||
account.fedNickname = profileResult.fedNickname;
|
||||
console.log('[myTischtennisService] - Updated account with club data');
|
||||
} else {
|
||||
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
|
||||
}
|
||||
} else if (password) {
|
||||
account.lastLoginAttempt = now;
|
||||
}
|
||||
|
||||
await account.save();
|
||||
} else {
|
||||
// Create new
|
||||
const accountData = {
|
||||
userId,
|
||||
email,
|
||||
savePassword,
|
||||
lastLoginAttempt: password ? now : null,
|
||||
lastLoginSuccess: loginResult?.success ? now : null
|
||||
};
|
||||
|
||||
if (loginResult && loginResult.success) {
|
||||
accountData.accessToken = loginResult.accessToken;
|
||||
accountData.refreshToken = loginResult.refreshToken;
|
||||
accountData.expiresAt = loginResult.expiresAt;
|
||||
accountData.cookie = loginResult.cookie;
|
||||
accountData.userData = loginResult.user;
|
||||
|
||||
// Hole Club-ID
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
if (profileResult.success) {
|
||||
accountData.clubId = profileResult.clubId;
|
||||
accountData.clubName = profileResult.clubName;
|
||||
}
|
||||
}
|
||||
|
||||
account = await MyTischtennis.create(accountData);
|
||||
|
||||
if (password && savePassword) {
|
||||
account.setPassword(password);
|
||||
await account.save();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
savePassword: account.savePassword,
|
||||
lastLoginAttempt: account.lastLoginAttempt,
|
||||
lastLoginSuccess: account.lastLoginSuccess,
|
||||
expiresAt: account.expiresAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete myTischtennis account
|
||||
*/
|
||||
async deleteAccount(userId) {
|
||||
const deleted = await MyTischtennis.destroy({
|
||||
where: { userId }
|
||||
});
|
||||
return deleted > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify login with stored or provided credentials
|
||||
*/
|
||||
async verifyLogin(userId, providedPassword = null) {
|
||||
const account = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
if (!account) {
|
||||
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
|
||||
}
|
||||
|
||||
let password = providedPassword;
|
||||
|
||||
// Wenn kein Passwort übergeben wurde, versuche gespeichertes Passwort zu verwenden
|
||||
if (!password) {
|
||||
if (!account.savePassword || !account.encryptedPassword) {
|
||||
throw new HttpError(400, 'Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.');
|
||||
}
|
||||
password = account.getPassword();
|
||||
}
|
||||
|
||||
// Login-Versuch
|
||||
const now = new Date();
|
||||
account.lastLoginAttempt = now;
|
||||
const loginResult = await myTischtennisClient.login(account.email, password);
|
||||
|
||||
if (loginResult.success) {
|
||||
account.lastLoginSuccess = now;
|
||||
account.accessToken = loginResult.accessToken;
|
||||
account.refreshToken = loginResult.refreshToken;
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
account.userData = loginResult.user;
|
||||
|
||||
// Hole Club-ID und Federation
|
||||
console.log('[myTischtennisService] - Getting user profile...');
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
console.log('[myTischtennisService] - Profile result:', {
|
||||
success: profileResult.success,
|
||||
clubId: profileResult.clubId,
|
||||
clubName: profileResult.clubName,
|
||||
fedNickname: profileResult.fedNickname
|
||||
});
|
||||
|
||||
if (profileResult.success) {
|
||||
account.clubId = profileResult.clubId;
|
||||
account.clubName = profileResult.clubName;
|
||||
account.fedNickname = profileResult.fedNickname;
|
||||
console.log('[myTischtennisService] - Updated account with club data');
|
||||
} else {
|
||||
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
|
||||
}
|
||||
|
||||
await account.save();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
accessToken: loginResult.accessToken,
|
||||
refreshToken: loginResult.refreshToken,
|
||||
expiresAt: loginResult.expiresAt,
|
||||
user: loginResult.user,
|
||||
clubId: account.clubId,
|
||||
clubName: account.clubName
|
||||
};
|
||||
} else {
|
||||
await account.save(); // Save lastLoginAttempt
|
||||
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if account is configured and ready
|
||||
*/
|
||||
async checkAccountStatus(userId) {
|
||||
const account = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
return {
|
||||
exists: !!account,
|
||||
hasEmail: !!account?.email,
|
||||
hasPassword: !!(account?.savePassword && account?.encryptedPassword),
|
||||
hasValidSession: !!account?.accessToken && account?.expiresAt > Date.now() / 1000,
|
||||
needsConfiguration: !account || !account.email,
|
||||
needsPassword: !!account && (!account.savePassword || !account.encryptedPassword)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored session for user (for authenticated API requests)
|
||||
*/
|
||||
async getSession(userId) {
|
||||
const account = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
if (!account) {
|
||||
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
|
||||
}
|
||||
|
||||
// Check if session is valid
|
||||
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
|
||||
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: account.accessToken,
|
||||
refreshToken: account.refreshToken,
|
||||
cookie: account.cookie,
|
||||
expiresAt: account.expiresAt,
|
||||
userData: account.userData
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new MyTischtennisService();
|
||||
|
||||
@@ -5,9 +5,10 @@ import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
|
||||
import sequelize from '../database.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
class PredefinedActivityService {
|
||||
async createPredefinedActivity(data) {
|
||||
console.log('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity');
|
||||
devLog('[PredefinedActivityService::createPredefinedActivity] - Creating predefined activity');
|
||||
return await PredefinedActivity.create({
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
@@ -20,10 +21,10 @@ class PredefinedActivityService {
|
||||
}
|
||||
|
||||
async updatePredefinedActivity(id, data) {
|
||||
console.log(`[PredefinedActivityService::updatePredefinedActivity] - Updating predefined activity with id: ${id}`);
|
||||
devLog(`[PredefinedActivityService::updatePredefinedActivity] - Updating predefined activity with id: ${id}`);
|
||||
const activity = await PredefinedActivity.findByPk(id);
|
||||
if (!activity) {
|
||||
console.log('[PredefinedActivityService::updatePredefinedActivity] - Activity not found');
|
||||
devLog('[PredefinedActivityService::updatePredefinedActivity] - Activity not found');
|
||||
throw new Error('Predefined activity not found');
|
||||
}
|
||||
return await activity.update({
|
||||
@@ -38,7 +39,7 @@ class PredefinedActivityService {
|
||||
}
|
||||
|
||||
async getAllPredefinedActivities() {
|
||||
console.log('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities');
|
||||
devLog('[PredefinedActivityService::getAllPredefinedActivities] - Fetching all predefined activities');
|
||||
return await PredefinedActivity.findAll({
|
||||
order: [
|
||||
[sequelize.literal('code IS NULL'), 'ASC'], // Non-null codes first
|
||||
@@ -49,10 +50,10 @@ class PredefinedActivityService {
|
||||
}
|
||||
|
||||
async getPredefinedActivityById(id) {
|
||||
console.log(`[PredefinedActivityService::getPredefinedActivityById] - Fetching predefined activity with id: ${id}`);
|
||||
devLog(`[PredefinedActivityService::getPredefinedActivityById] - Fetching predefined activity with id: ${id}`);
|
||||
const activity = await PredefinedActivity.findByPk(id);
|
||||
if (!activity) {
|
||||
console.log('[PredefinedActivityService::getPredefinedActivityById] - Activity not found');
|
||||
devLog('[PredefinedActivityService::getPredefinedActivityById] - Activity not found');
|
||||
throw new Error('Predefined activity not found');
|
||||
}
|
||||
return activity;
|
||||
@@ -80,7 +81,7 @@ class PredefinedActivityService {
|
||||
}
|
||||
|
||||
async mergeActivities(sourceId, targetId) {
|
||||
console.log(`[PredefinedActivityService::mergeActivities] - Merge ${sourceId} -> ${targetId}`);
|
||||
devLog(`[PredefinedActivityService::mergeActivities] - Merge ${sourceId} -> ${targetId}`);
|
||||
if (!sourceId || !targetId) throw new Error('sourceId and targetId are required');
|
||||
if (Number(sourceId) === Number(targetId)) throw new Error('sourceId and targetId must differ');
|
||||
|
||||
@@ -120,7 +121,7 @@ class PredefinedActivityService {
|
||||
}
|
||||
|
||||
async deduplicateActivities() {
|
||||
console.log('[PredefinedActivityService::deduplicateActivities] - Start');
|
||||
devLog('[PredefinedActivityService::deduplicateActivities] - Start');
|
||||
const all = await PredefinedActivity.findAll();
|
||||
const nameToActivities = new Map();
|
||||
for (const activity of all) {
|
||||
@@ -142,7 +143,7 @@ class PredefinedActivityService {
|
||||
mergedCount++;
|
||||
}
|
||||
}
|
||||
console.log('[PredefinedActivityService::deduplicateActivities] - Done', { mergedCount, groupCount });
|
||||
devLog('[PredefinedActivityService::deduplicateActivities] - Done', { mergedCount, groupCount });
|
||||
return { mergedCount, groupCount };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { checkAccess } from '../utils/userUtils.js';
|
||||
import { Op, literal } from 'sequelize';
|
||||
|
||||
|
||||
import { devLog } from '../utils/logger.js';
|
||||
function getRoundName(size) {
|
||||
switch (size) {
|
||||
case 2: return "Finale";
|
||||
@@ -186,7 +187,7 @@ class TournamentService {
|
||||
|
||||
if (alreadyAssigned.length > 0) {
|
||||
// Spieler sind bereits manuell zugeordnet - nicht neu verteilen
|
||||
console.log(`${alreadyAssigned.length} Spieler bereits zugeordnet, ${unassigned.length} noch nicht zugeordnet`);
|
||||
devLog(`${alreadyAssigned.length} Spieler bereits zugeordnet, ${unassigned.length} noch nicht zugeordnet`);
|
||||
} else {
|
||||
// Keine manuellen Zuordnungen - zufällig verteilen
|
||||
const shuffled = members.slice();
|
||||
@@ -202,11 +203,11 @@ class TournamentService {
|
||||
}
|
||||
|
||||
// 4) Round‑Robin anlegen wie gehabt - NUR innerhalb jeder Gruppe
|
||||
console.log(`[fillGroups] Erstelle Matches für ${groups.length} Gruppen`);
|
||||
devLog(`[fillGroups] Erstelle Matches für ${groups.length} Gruppen`);
|
||||
for (const g of groups) {
|
||||
console.log(`[fillGroups] Verarbeite Gruppe ${g.id}`);
|
||||
devLog(`[fillGroups] Verarbeite Gruppe ${g.id}`);
|
||||
const gm = await TournamentMember.findAll({ where: { groupId: g.id } });
|
||||
console.log(`[fillGroups] Gruppe ${g.id} hat ${gm.length} Teilnehmer:`, gm.map(m => ({ id: m.id, name: m.member?.firstName + ' ' + m.member?.lastName })));
|
||||
devLog(`[fillGroups] Gruppe ${g.id} hat ${gm.length} Teilnehmer:`, gm.map(m => ({ id: m.id, name: m.member?.firstName + ' ' + m.member?.lastName })));
|
||||
|
||||
if (gm.length < 2) {
|
||||
console.warn(`Gruppe ${g.id} hat nur ${gm.length} Teilnehmer - keine Matches erstellt`);
|
||||
@@ -214,10 +215,10 @@ class TournamentService {
|
||||
}
|
||||
|
||||
const rounds = this.generateRoundRobinSchedule(gm);
|
||||
console.log(`[fillGroups] Gruppe ${g.id} hat ${rounds.length} Runden`);
|
||||
devLog(`[fillGroups] Gruppe ${g.id} hat ${rounds.length} Runden`);
|
||||
|
||||
for (let roundIndex = 0; roundIndex < rounds.length; roundIndex++) {
|
||||
console.log(`[fillGroups] Runde ${roundIndex + 1} für Gruppe ${g.id}:`, rounds[roundIndex]);
|
||||
devLog(`[fillGroups] Runde ${roundIndex + 1} für Gruppe ${g.id}:`, rounds[roundIndex]);
|
||||
for (const [p1Id, p2Id] of rounds[roundIndex]) {
|
||||
// Prüfe, ob beide Spieler zur gleichen Gruppe gehören
|
||||
const p1 = gm.find(p => p.id === p1Id);
|
||||
@@ -231,7 +232,7 @@ class TournamentService {
|
||||
player2Id: p2Id,
|
||||
groupRound: roundIndex + 1
|
||||
});
|
||||
console.log(`[fillGroups] Match erstellt: ${match.id} - Spieler ${p1Id} vs ${p2Id} in Gruppe ${g.id}`);
|
||||
devLog(`[fillGroups] Match erstellt: ${match.id} - Spieler ${p1Id} vs ${p2Id} in Gruppe ${g.id}`);
|
||||
} else {
|
||||
console.warn(`Spieler gehören nicht zur gleichen Gruppe: ${p1Id} (${p1?.groupId}) vs ${p2Id} (${p2?.groupId}) in Gruppe ${g.id}`);
|
||||
}
|
||||
|
||||
19
backend/utils/logger.js
Normal file
19
backend/utils/logger.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Zentrale Logger-Utility für dev/prod Umgebungen
|
||||
const isDev = process.env.STAGE === 'dev';
|
||||
|
||||
// Debug-Logs nur im dev-Modus
|
||||
export const devLog = (...args) => isDev && console.log(...args);
|
||||
|
||||
// Fehler-Logs immer ausgeben
|
||||
export const errorLog = (...args) => console.error(...args);
|
||||
|
||||
// Info-Logs immer ausgeben (für wichtige Produktions-Events)
|
||||
export const infoLog = (...args) => console.log(...args);
|
||||
|
||||
export default {
|
||||
devLog,
|
||||
errorLog,
|
||||
infoLog,
|
||||
isDev
|
||||
};
|
||||
|
||||
@@ -2,9 +2,10 @@ import jwt from 'jsonwebtoken';
|
||||
import { Op } from 'sequelize';
|
||||
import User from '../models/User.js';
|
||||
import UserToken from '../models/UserToken.js';
|
||||
import UserClub from '../models/UserClub.js'; // <-- hier hinzufügen
|
||||
import UserClub from '../models/UserClub.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
import { config } from 'dotenv';
|
||||
import { devLog } from './logger.js';
|
||||
config(); // sorgt dafür, dass process.env.JWT_SECRET geladen wird
|
||||
|
||||
export const getUserByToken = async (token) => {
|
||||
@@ -41,7 +42,7 @@ export const getUserByToken = async (token) => {
|
||||
|
||||
export const hasUserClubAccess = async (userId, clubId) => {
|
||||
try {
|
||||
console.log('[hasUserClubAccess]');
|
||||
devLog('[hasUserClubAccess]');
|
||||
const userClub = await UserClub.findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
@@ -51,10 +52,9 @@ export const hasUserClubAccess = async (userId, clubId) => {
|
||||
});
|
||||
return userClub !== null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error('[hasUserClubAccess] - error:', error);
|
||||
throw new HttpError('notfound', 500);
|
||||
}
|
||||
console.log('---- no user found');
|
||||
}
|
||||
|
||||
export const checkAccess = async (userToken, clubId) => {
|
||||
@@ -62,11 +62,11 @@ export const checkAccess = async (userToken, clubId) => {
|
||||
const user = await getUserByToken(userToken);
|
||||
const hasAccess = await hasUserClubAccess(user.id, clubId);
|
||||
if (!hasAccess) {
|
||||
console.log('no club access');
|
||||
devLog('[checkAccess] - no club access');
|
||||
throw new HttpError('noaccess', 403);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog('[checkAccess] - error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -76,7 +76,7 @@ export const checkGlobalAccess = async (userToken) => {
|
||||
const user = await getUserByToken(userToken);
|
||||
return user; // Einfach den User zurückgeben, da globale Zugriffe nur Authentifizierung benötigen
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
devLog('[checkGlobalAccess] - error:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -68,13 +68,17 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav class="nav-menu">
|
||||
<nav class="sidebar-footer">
|
||||
<div class="nav-section">
|
||||
<h4 class="nav-title">Einstellungen</h4>
|
||||
<a href="/mytischtennis-account" class="nav-link">
|
||||
<span class="nav-icon">🔗</span>
|
||||
myTischtennis-Account
|
||||
</a>
|
||||
<a href="/hettv-account" class="nav-link">
|
||||
<span class="nav-icon">🔗</span>
|
||||
HeTTV-Account
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -133,7 +137,7 @@ export default {
|
||||
if (newVal === 'new') {
|
||||
this.$router.push('/createclub');
|
||||
} else if (newVal) {
|
||||
this.$router.push(`/showclub/${newVal}`);
|
||||
this.$router.push('/training-stats');
|
||||
}
|
||||
},
|
||||
isAuthenticated(newVal) {
|
||||
@@ -171,7 +175,7 @@ export default {
|
||||
|
||||
loadClub() {
|
||||
this.setCurrentClub(this.currentClub);
|
||||
this.$router.push(`/showclub/${this.currentClub}`);
|
||||
this.$router.push('/training-stats');
|
||||
},
|
||||
|
||||
async checkSession() {
|
||||
@@ -341,6 +345,15 @@ export default {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-menu-no-flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
flex: 0;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
291
frontend/src/components/HettvDialog.vue
Normal file
291
frontend/src/components/HettvDialog.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="modal-overlay" @click.self="$emit('close')">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3>{{ account ? 'HeTTV-Account bearbeiten' : 'HeTTV-Account verknüpfen' }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="hettv-email">HeTTV-E-Mail:</label>
|
||||
<input
|
||||
type="email"
|
||||
id="hettv-email"
|
||||
v-model="formData.email"
|
||||
placeholder="Ihre HeTTV-E-Mail-Adresse"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hettv-password">HeTTV-Passwort:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="hettv-password"
|
||||
v-model="formData.password"
|
||||
:placeholder="account && account.savePassword ? 'Leer lassen um beizubehalten' : 'Ihr HeTTV-Passwort'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="formData.savePassword"
|
||||
/>
|
||||
<span>HeTTV-Passwort speichern</span>
|
||||
</label>
|
||||
<p class="hint">
|
||||
Wenn aktiviert, wird Ihr HeTTV-Passwort verschlüsselt gespeichert,
|
||||
sodass automatische Synchronisationen möglich sind.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="formData.password">
|
||||
<label for="app-password">Ihr App-Passwort zur Bestätigung:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="app-password"
|
||||
v-model="formData.userPassword"
|
||||
placeholder="Ihr Passwort für diese App"
|
||||
required
|
||||
/>
|
||||
<p class="hint">
|
||||
Aus Sicherheitsgründen benötigen wir Ihr App-Passwort,
|
||||
um das HeTTV-Passwort zu speichern.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" @click="$emit('close')" :disabled="saving">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button class="btn-primary" @click="saveAccount" :disabled="!canSave || saving">
|
||||
{{ saving ? 'Speichere...' : 'Speichern' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient.js';
|
||||
|
||||
export default {
|
||||
name: 'HettvDialog',
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
email: this.account?.email || '',
|
||||
password: '',
|
||||
savePassword: this.account?.savePassword || false,
|
||||
userPassword: ''
|
||||
},
|
||||
saving: false,
|
||||
error: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canSave() {
|
||||
if (!this.formData.email.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.formData.password && !this.formData.userPassword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async saveAccount() {
|
||||
if (!this.canSave) return;
|
||||
|
||||
this.error = null;
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
email: this.formData.email,
|
||||
savePassword: this.formData.savePassword,
|
||||
service: 'hettv'
|
||||
};
|
||||
|
||||
if (this.formData.password) {
|
||||
payload.password = this.formData.password;
|
||||
payload.userPassword = this.formData.userPassword;
|
||||
}
|
||||
|
||||
await apiClient.post('/external-service/account', payload);
|
||||
this.$emit('saved');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
this.error = error.response?.data?.message || 'Fehler beim Speichern des Accounts';
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 1rem 1.5rem;
|
||||
border-top: 1px solid #dee2e6;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="email"],
|
||||
.form-group input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-group input[type="text"]:focus,
|
||||
.form-group input[type="email"]:focus,
|
||||
.form-group input[type="password"]:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 0.75rem;
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
color: #721c24;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -121,7 +121,8 @@ export default {
|
||||
try {
|
||||
const payload = {
|
||||
email: this.formData.email,
|
||||
savePassword: this.formData.savePassword
|
||||
savePassword: this.formData.savePassword,
|
||||
service: 'mytischtennis'
|
||||
};
|
||||
|
||||
// Nur password und userPassword hinzufügen, wenn ein Passwort eingegeben wurde
|
||||
@@ -130,7 +131,7 @@ export default {
|
||||
payload.userPassword = this.formData.userPassword;
|
||||
}
|
||||
|
||||
await apiClient.post('/mytischtennis/account', payload);
|
||||
await apiClient.post('/external-service/account', payload);
|
||||
this.$emit('saved');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Speichern:', error);
|
||||
|
||||
@@ -14,6 +14,7 @@ import TrainingStatsView from './views/TrainingStatsView.vue';
|
||||
import PredefinedActivities from './views/PredefinedActivities.vue';
|
||||
import OfficialTournaments from './views/OfficialTournaments.vue';
|
||||
import MyTischtennisAccount from './views/MyTischtennisAccount.vue';
|
||||
import HettvAccount from './views/HettvAccount.vue';
|
||||
import Impressum from './views/Impressum.vue';
|
||||
import Datenschutz from './views/Datenschutz.vue';
|
||||
|
||||
@@ -33,6 +34,7 @@ const routes = [
|
||||
{ path: '/predefined-activities', component: PredefinedActivities },
|
||||
{ path: '/official-tournaments', component: OfficialTournaments },
|
||||
{ path: '/mytischtennis-account', component: MyTischtennisAccount },
|
||||
{ path: '/hettv-account', component: HettvAccount },
|
||||
{ path: '/impressum', component: Impressum },
|
||||
{ path: '/datenschutz', component: Datenschutz },
|
||||
];
|
||||
|
||||
314
frontend/src/views/HettvAccount.vue
Normal file
314
frontend/src/views/HettvAccount.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>HeTTV-Account</h1>
|
||||
|
||||
<div class="account-container">
|
||||
<div v-if="loading" class="loading">Lade...</div>
|
||||
|
||||
<div v-else-if="account" class="account-info">
|
||||
<div class="info-section">
|
||||
<h2>Verknüpfter Account</h2>
|
||||
|
||||
<div class="info-row">
|
||||
<label>E-Mail:</label>
|
||||
<span>{{ account.email }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<label>Passwort gespeichert:</label>
|
||||
<span>{{ account.savePassword ? 'Ja' : 'Nein' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastLoginSuccess">
|
||||
<label>Letzter erfolgreicher Login:</label>
|
||||
<span>{{ formatDate(account.lastLoginSuccess) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row" v-if="account.lastLoginAttempt">
|
||||
<label>Letzter Login-Versuch:</label>
|
||||
<span>{{ formatDate(account.lastLoginAttempt) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn-primary" @click="openEditDialog">Account bearbeiten</button>
|
||||
<button class="btn-secondary" @click="testConnection">Erneut einloggen</button>
|
||||
<button class="btn-danger" @click="deleteAccount">Account trennen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="no-account">
|
||||
<p>Kein HeTTV-Account verknüpft.</p>
|
||||
<button class="btn-primary" @click="openEditDialog">Account verknüpfen</button>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Über HeTTV</h3>
|
||||
<p>Durch die Verknüpfung Ihres HeTTV-Accounts können Sie:</p>
|
||||
<ul>
|
||||
<li>Spielerdaten automatisch synchronisieren</li>
|
||||
<li>Ligainformationen abrufen</li>
|
||||
<li>Wettkampfdaten direkt importieren</li>
|
||||
</ul>
|
||||
<p><strong>Hinweis:</strong> Das Speichern des Passworts ist optional. Wenn Sie es nicht speichern, werden Sie bei jeder Synchronisation nach dem Passwort gefragt.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Dialog -->
|
||||
<HettvDialog
|
||||
v-if="showDialog"
|
||||
:account="account"
|
||||
@close="closeDialog"
|
||||
@saved="onAccountSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient.js';
|
||||
import HettvDialog from '../components/HettvDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'HettvAccount',
|
||||
components: {
|
||||
HettvDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
account: null,
|
||||
showDialog: false
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadAccount();
|
||||
},
|
||||
methods: {
|
||||
async loadAccount() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const response = await apiClient.get('/external-service/account', {
|
||||
params: { service: 'hettv' }
|
||||
});
|
||||
this.account = response.data.account;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden des Accounts:', error);
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'Fehler beim Laden des HeTTV-Accounts',
|
||||
type: 'error'
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
openEditDialog() {
|
||||
this.showDialog = true;
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.showDialog = false;
|
||||
},
|
||||
|
||||
async onAccountSaved() {
|
||||
this.closeDialog();
|
||||
await this.loadAccount();
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'HeTTV-Account erfolgreich gespeichert',
|
||||
type: 'success'
|
||||
});
|
||||
},
|
||||
|
||||
async testConnection() {
|
||||
try {
|
||||
await apiClient.post('/external-service/verify', {
|
||||
service: 'hettv'
|
||||
});
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'Login erfolgreich! Verbindungsdaten aktualisiert.',
|
||||
type: 'success'
|
||||
});
|
||||
await this.loadAccount();
|
||||
} catch (error) {
|
||||
const message = error.response?.data?.message || 'Login fehlgeschlagen';
|
||||
|
||||
if (error.response?.status === 400 && message.includes('Kein Passwort gespeichert')) {
|
||||
this.showDialog = true;
|
||||
}
|
||||
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: message,
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async deleteAccount() {
|
||||
if (!confirm('Möchten Sie die Verknüpfung zum HeTTV-Account wirklich trennen?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await apiClient.delete('/external-service/account', {
|
||||
params: { service: 'hettv' }
|
||||
});
|
||||
this.account = null;
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'HeTTV-Account erfolgreich getrennt',
|
||||
type: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Accounts:', error);
|
||||
this.$store.dispatch('showMessage', {
|
||||
text: 'Fehler beim Trennen des Accounts',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: var(--text-color, #333);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.account-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.account-info, .no-account {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.info-section h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--primary-color, #007bff);
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-row:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.info-row label {
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.info-row span {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.no-account {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.no-account p {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid var(--primary-color, #007bff);
|
||||
padding: 1.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-box h3 {
|
||||
margin-top: 0;
|
||||
color: var(--primary-color, #007bff);
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
margin: 1rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.info-box li {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.btn-primary, .btn-secondary, .btn-danger {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -387,10 +387,6 @@ export default {
|
||||
return '';
|
||||
},
|
||||
async updateRatingsFromMyTischtennis() {
|
||||
if (!confirm('TTR/QTTR-Werte von myTischtennis aktualisieren?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdatingRatings = true;
|
||||
try {
|
||||
const response = await apiClient.post(`/clubmembers/update-ratings/${this.currentClub}`);
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>Spielpläne</h2>
|
||||
<button @click="openImportModal">Spielplanimport</button>
|
||||
<div class="button-group">
|
||||
<button @click="openImportModal">Spielplanimport</button>
|
||||
<button @click="loadHettvData" :disabled="loadingHettv" class="hettv-button">
|
||||
{{ loadingHettv ? 'Lade HeTTV-Daten...' : 'HeTTV-Daten laden' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="hoveredMatch && hoveredMatch.location" class="hover-info">
|
||||
<p><strong>{{ hoveredMatch.location.name || 'N/A' }}</strong></p>
|
||||
<p>{{ hoveredMatch.location.address || 'N/A' }}</p>
|
||||
@@ -79,6 +84,8 @@ export default {
|
||||
matches: [],
|
||||
selectedLeague: '',
|
||||
hoveredMatch: null,
|
||||
loadingHettv: false,
|
||||
hettvData: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
@@ -174,10 +181,15 @@ export default {
|
||||
async loadLeagues() {
|
||||
try {
|
||||
const clubId = this.currentClub;
|
||||
if (!clubId || clubId === 'null') {
|
||||
console.log('Kein Club ausgewählt, überspringe Liga-Laden');
|
||||
return;
|
||||
}
|
||||
const response = await apiClient.get(`/matches/leagues/current/${clubId}`);
|
||||
this.leagues = this.sortLeagues(response.data);
|
||||
} catch (error) {
|
||||
alert('Fehler beim Laden der Ligen');
|
||||
console.log('Fehler beim Laden der Ligen:', error.message);
|
||||
// Keine Alert mehr, da das normal ist wenn kein Club-Zugriff vorhanden
|
||||
}
|
||||
},
|
||||
async loadMatchesForLeague(leagueId, leagueName) {
|
||||
@@ -301,6 +313,39 @@ export default {
|
||||
|
||||
return ''; // Keine besondere Farbe
|
||||
},
|
||||
async loadHettvData() {
|
||||
this.loadingHettv = true;
|
||||
try {
|
||||
console.log('Lade HeTTV-Hauptseite...');
|
||||
const response = await apiClient.get('/external-service/hettv/main-page');
|
||||
|
||||
if (response.data.success) {
|
||||
this.hettvData = response.data.data;
|
||||
console.log('HeTTV-Daten geladen:', {
|
||||
downloadLinks: this.hettvData.downloadLinks,
|
||||
htmlLength: this.hettvData.htmlContent.length,
|
||||
trace: this.hettvData.trace,
|
||||
lastUrl: this.hettvData.lastUrl,
|
||||
lastStatus: this.hettvData.lastStatus,
|
||||
savedFile: this.hettvData.savedFile
|
||||
});
|
||||
|
||||
// Zeige gefundene Download-Links
|
||||
if (this.hettvData.downloadLinks.length > 0) {
|
||||
alert(`HeTTV-Daten erfolgreich geladen!\n\nGefundene Download-Links:\n${this.hettvData.downloadLinks.join('\n')}`);
|
||||
} else {
|
||||
alert('HeTTV-Hauptseite geladen, aber keine Download-Links gefunden.');
|
||||
}
|
||||
} else {
|
||||
alert('Fehler beim Laden der HeTTV-Daten: ' + response.data.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('HeTTV-Fehler:', error);
|
||||
alert('Fehler beim Laden der HeTTV-Daten: ' + (error.response?.data?.error || error.message));
|
||||
} finally {
|
||||
this.loadingHettv = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.loadLeagues();
|
||||
@@ -451,4 +496,37 @@ li {
|
||||
.match-next-week:hover {
|
||||
background-color: #b8daff !important; /* Dunkleres Blau beim Hover */
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
margin-right: 10px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f9fa;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.button-group button:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.hettv-button {
|
||||
background-color: #007bff !important;
|
||||
color: white !important;
|
||||
border-color: #007bff !important;
|
||||
}
|
||||
|
||||
.hettv-button:hover {
|
||||
background-color: #0056b3 !important;
|
||||
}
|
||||
|
||||
.hettv-button:disabled {
|
||||
background-color: #6c757d !important;
|
||||
border-color: #6c757d !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,8 +19,43 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="members-table-container">
|
||||
<table class="members-table">
|
||||
<!-- Trainingstage-Tabelle (standardmäßig aufgeklappt) -->
|
||||
<div class="collapsible-section">
|
||||
<div class="section-header" @click="toggleTrainingDays">
|
||||
<h3>Trainingstage (letzte 12 Monate)</h3>
|
||||
<span class="toggle-icon">{{ showTrainingDays ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div v-if="showTrainingDays" class="section-content">
|
||||
<div class="training-days-container">
|
||||
<table class="training-days-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Datum</th>
|
||||
<th>Wochentag</th>
|
||||
<th>Teilnehmer</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="day in trainingDays" :key="day.id">
|
||||
<td>{{ formatDate(day.date) }}</td>
|
||||
<td>{{ getWeekday(day.date) }}</td>
|
||||
<td>{{ day.participantCount }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mitglieder-Tabelle (standardmäßig eingeklappt) -->
|
||||
<div class="collapsible-section">
|
||||
<div class="section-header" @click="toggleMembers">
|
||||
<h3>Mitglieder-Teilnahmen</h3>
|
||||
<span class="toggle-icon">{{ showMembers ? '▼' : '▶' }}</span>
|
||||
</div>
|
||||
<div v-if="showMembers" class="section-content">
|
||||
<div class="members-table-container">
|
||||
<table class="members-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th @click="sortBy('name')" class="sortable-header">
|
||||
@@ -74,6 +109,8 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal für Mitgliedsdetails -->
|
||||
<div v-if="showDetailsModal" class="modal">
|
||||
@@ -126,15 +163,15 @@ export default {
|
||||
...mapGetters(['isAuthenticated', 'currentClub']),
|
||||
|
||||
averageParticipation12Months() {
|
||||
if (this.activeMembers.length === 0) return 0;
|
||||
if (this.trainingsCount12Months === 0) return 0;
|
||||
const total = this.activeMembers.reduce((sum, member) => sum + member.participation12Months, 0);
|
||||
return total / this.activeMembers.length;
|
||||
return total / this.trainingsCount12Months;
|
||||
},
|
||||
|
||||
averageParticipation3Months() {
|
||||
if (this.activeMembers.length === 0) return 0;
|
||||
if (this.trainingsCount3Months === 0) return 0;
|
||||
const total = this.activeMembers.reduce((sum, member) => sum + member.participation3Months, 0);
|
||||
return total / this.activeMembers.length;
|
||||
return total / this.trainingsCount3Months;
|
||||
},
|
||||
|
||||
sortedMembers() {
|
||||
@@ -167,11 +204,16 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
activeMembers: [],
|
||||
trainingsCount12Months: 0,
|
||||
trainingsCount3Months: 0,
|
||||
trainingDays: [],
|
||||
showDetailsModal: false,
|
||||
selectedMember: {},
|
||||
loading: false,
|
||||
sortField: 'name',
|
||||
sortDirection: 'asc'
|
||||
sortDirection: 'asc',
|
||||
showTrainingDays: true,
|
||||
showMembers: false
|
||||
};
|
||||
},
|
||||
|
||||
@@ -199,7 +241,10 @@ export default {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get(`/training-stats/${this.currentClub}`);
|
||||
this.activeMembers = response.data;
|
||||
this.activeMembers = response.data.members || [];
|
||||
this.trainingsCount12Months = response.data.trainingsCount12Months || 0;
|
||||
this.trainingsCount3Months = response.data.trainingsCount3Months || 0;
|
||||
this.trainingDays = response.data.trainingDays || [];
|
||||
} catch (error) {
|
||||
// Kein Alert - es ist normal, dass nicht alle Daten verfügbar sind
|
||||
} finally {
|
||||
@@ -207,6 +252,20 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
toggleTrainingDays() {
|
||||
this.showTrainingDays = !this.showTrainingDays;
|
||||
},
|
||||
|
||||
toggleMembers() {
|
||||
this.showMembers = !this.showMembers;
|
||||
},
|
||||
|
||||
getWeekday(dateString) {
|
||||
const date = new Date(dateString);
|
||||
const weekdays = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
|
||||
return weekdays[date.getDay()];
|
||||
},
|
||||
|
||||
showMemberDetails(member) {
|
||||
this.selectedMember = member;
|
||||
this.showDetailsModal = true;
|
||||
@@ -296,12 +355,84 @@ export default {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.members-table-container {
|
||||
.collapsible-section {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: var(--shadow-light);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
margin-bottom: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
background: var(--bg-light);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.section-header:hover {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.section-header:hover h3 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
color: var(--text-primary);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.training-days-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.training-days-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.training-days-table th,
|
||||
.training-days-table td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.training-days-table th {
|
||||
background: var(--bg-light);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.training-days-table tbody tr:hover {
|
||||
background: var(--bg-light);
|
||||
}
|
||||
|
||||
.training-days-table tbody tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.members-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.members-table {
|
||||
|
||||
Reference in New Issue
Block a user