8 Commits

Author SHA1 Message Date
Torsten Schulz (local)
7be98ffeeb Entfernt die myTischtennis-Integration aus dem Backend und Frontend. Löscht Controller, Routen und Service für myTischtennis. Aktualisiert die Datenmodelle, um die neue ExternalServiceAccount-Integration zu unterstützen. Ändert die API-Routen und Frontend-Komponenten, um die neuen Endpunkte zu verwenden. 2025-10-01 21:01:09 +02:00
Torsten Schulz (local)
1ef1711eea Merge branch 'main' into httv 2025-10-01 13:49:26 +02:00
Torsten Schulz (local)
85981a880d Ändert die Navigationsstruktur in App.vue, indem die Klasse der Fußzeile von "nav-menu" zu "sidebar-footer" geändert wird. Fügt eine neue CSS-Klasse "nav-menu-no-flex" hinzu, um das Layout der Navigation zu optimieren und die Flexbox-Eigenschaften anzupassen. 2025-10-01 13:49:13 +02:00
Torsten Schulz (local)
84503b6404 Merge branch 'httv' 2025-10-01 13:31:41 +02:00
Torsten Schulz (local)
bcc3ce036d Ersetzt Konsolenausgaben durch eine bedingte Entwicklungsprotokollierungsfunktion in mehreren Controllern und Services. Dies verbessert die Protokollierung und Fehlerverfolgung im gesamten Code. Aktualisiert die Benutzer-Utils, um die neue Protokollierungsfunktion zu verwenden. 2025-10-01 13:29:49 +02:00
Torsten Schulz (local)
0fe0514660 Verbessert die Protokollierung in den Club-Controller- und Benutzer-Utils-Dateien, indem die Konsolenausgaben durch eine bedingte Entwicklungsprotokollierungsfunktion ersetzt werden. Aktualisiert die Fehlerbehandlung, um detailliertere Fehlermeldungen auszugeben. 2025-10-01 13:24:16 +02:00
Torsten Schulz (local)
431ec861ba Erweitert die Trainingsstatistik-Funktionalität im TrainingStatsController um die Abfrage und Formatierung von Trainingstagen der letzten 12 Monate. Aktualisiert die Benutzeroberfläche in TrainingStatsView.vue zur Anzeige dieser Trainingstage in einer aufklappbaren Tabelle. Fügt Funktionen zum Umschalten der Sichtbarkeit von Trainingstagen und Mitgliedern hinzu. 2025-10-01 13:20:36 +02:00
Torsten Schulz (local)
648b608036 Erweitert die Trainingsstatistik-Funktionalität im TrainingStatsController, um die Anzahl der Trainings in den letzten 12 und 3 Monaten zu berechnen und zurückzugeben. Aktualisiert die Benutzeroberfläche in TrainingStatsView.vue zur Anzeige dieser neuen Daten. Ändert die Navigation in App.vue, um direkt zu den Trainingsstatistiken zu führen. 2025-10-01 13:01:54 +02:00
45 changed files with 2422 additions and 624 deletions

View 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();

View File

@@ -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" });
}
}

View File

@@ -1,6 +1,7 @@
import diaryService from '../services/diaryService.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
const getDatesForClub = async (req, res) => {
try {
const { clubId } = req.params;
@@ -38,7 +39,7 @@ const updateTrainingTimes = async (req, res) => {
const { authcode: userToken } = req.headers;
const { dateId, trainingStart, trainingEnd } = req.body;
if (!dateId || !trainingStart) {
console.log(dateId, trainingStart, trainingEnd);
devLog(dateId, trainingStart, trainingEnd);
throw new HttpError('notallfieldsfilled', 400);
}
const updatedDate = await diaryService.updateTrainingTimes(userToken, clubId, dateId, trainingStart, trainingEnd);

View File

@@ -1,5 +1,6 @@
import diaryDateActivityService from '../services/diaryDateActivityService.js';
import { devLog } from '../utils/logger.js';
export const createDiaryDateActivity = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
@@ -15,7 +16,7 @@ export const createDiaryDateActivity = async (req, res) => {
});
res.status(201).json(activityItem);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error creating activity' });
}
};
@@ -58,7 +59,7 @@ export const updateDiaryDateActivityOrder = async (req, res) => {
const updatedActivity = await diaryDateActivityService.updateActivityOrder(userToken, clubId, id, orderId);
res.status(200).json(updatedActivity);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error updating activity order' });
}
};
@@ -70,7 +71,7 @@ export const getDiaryDateActivities = async (req, res) => {
const activities = await diaryDateActivityService.getActivities(userToken, clubId, diaryDateId);
res.status(200).json(activities);
} catch (error) {
console.log(error);
devLog(error);
res.status(500).json({ error: 'Error getting activities' });
}
}
@@ -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' });
}
}

View File

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

View File

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

View File

@@ -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' });
}
};

View 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();

View File

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

View File

@@ -1,6 +1,7 @@
import MatchService from '../services/matchService.js';
import fs from 'fs';
import { devLog } from '../utils/logger.js';
export const uploadCSV = async (req, res) => {
try {
const { clubId } = req.body;
@@ -21,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);

View File

@@ -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;

View File

@@ -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' });
}
};

View File

@@ -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();

View File

@@ -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' });
}
};

View File

@@ -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,

View File

@@ -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);

View 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;

View File

@@ -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;

View File

@@ -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,
};

View 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;

View File

@@ -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;

View File

@@ -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');

View File

@@ -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;
}
};

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -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.');
}
}

View File

@@ -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 };
}

View 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();

View File

@@ -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({

View File

@@ -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;
}
}

View File

@@ -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 },

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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 };
}
}

View File

@@ -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) RoundRobin 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
View 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
};

View File

@@ -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;
}
};

View File

@@ -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;

View 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>

View File

@@ -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);

View File

@@ -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 },
];

View 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>

View File

@@ -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}`);

View File

@@ -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>

View File

@@ -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 {