import express from 'express'; import path from 'path'; import { fileURLToPath } from 'url'; import { createServer } from 'http'; import https from 'https'; import fs from 'fs'; import { exec } from 'child_process'; import sequelize from './database.js'; import cors from 'cors'; import { initializeSocketIO } from './services/socketService.js'; import { User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote, DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag, PredefinedActivity, PredefinedActivityImage, DiaryDateActivity, DiaryMemberActivity, Match, League, Team, ClubTeam, TeamDocument, Group, GroupActivity, Tournament, TournamentGroup, TournamentMatch, TournamentResult, TournamentMember, Accident, UserToken, OfficialTournament, OfficialCompetition, OfficialCompetitionMember, MyTischtennis, ClickTtAccount, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog, MemberTransferConfig, MemberContact, MemberTtrHistory , MemberOrder, MemberOrderHistory } from './models/index.js'; import authRoutes from './routes/authRoutes.js'; import clubRoutes from './routes/clubRoutes.js'; import diaryRoutes from './routes/diaryRoutes.js'; import memberRoutes from './routes/memberRoutes.js'; import participantRoutes from './routes/participantRoutes.js'; import activityRoutes from './routes/activityRoutes.js'; import memberNoteRoutes from './routes/memberNoteRoutes.js'; import diaryTagRoutes from './routes/diaryTagRoutes.js'; import diaryNoteRoutes from './routes/diaryNoteRoutes.js'; import diaryMemberRoutes from './routes/diaryMemberRoutes.js'; import predefinedActivityRoutes from './routes/predefinedActivityRoutes.js'; import diaryDateActivityRoutes from './routes/diaryDateActivityRoutes.js'; import diaryMemberActivityRoutes from './routes/diaryMemberActivityRoutes.js'; import matchRoutes from './routes/matchRoutes.js'; import Season from './models/Season.js'; import Location from './models/Location.js'; import groupRoutes from './routes/groupRoutes.js'; import diaryDateTagRoutes from './routes/diaryDateTagRoutes.js'; import sessionRoutes from './routes/sessionRoutes.js'; import schedulerRoutes from './routes/schedulerRoutes.js'; import tournamentRoutes from './routes/tournamentRoutes.js'; import accidentRoutes from './routes/accidentRoutes.js'; import trainingStatsRoutes from './routes/trainingStatsRoutes.js'; import officialTournamentRoutes from './routes/officialTournamentRoutes.js'; import myTischtennisRoutes from './routes/myTischtennisRoutes.js'; import clickTtAccountRoutes from './routes/clickTtAccountRoutes.js'; import teamRoutes from './routes/teamRoutes.js'; import clubTeamRoutes from './routes/clubTeamRoutes.js'; import teamDocumentRoutes from './routes/teamDocumentRoutes.js'; import seasonRoutes from './routes/seasonRoutes.js'; import nuscoreSimpleProxyRoutes from './routes/nuscoreSimpleProxyRoutes.js'; import nuscoreApiRoutes from './routes/nuscoreApiRoutes.js'; import memberActivityRoutes from './routes/memberActivityRoutes.js'; import permissionRoutes from './routes/permissionRoutes.js'; import apiLogRoutes from './routes/apiLogRoutes.js'; import clickTtHttpPageRoutes from './routes/clickTtHttpPageRoutes.js'; import memberTransferConfigRoutes from './routes/memberTransferConfigRoutes.js'; import trainingGroupRoutes from './routes/trainingGroupRoutes.js'; import trainingTimeRoutes from './routes/trainingTimeRoutes.js'; import memberOrderRoutes from './routes/memberOrderRoutes.js'; import schedulerService from './services/schedulerService.js'; import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.js'; import HttpError from './exceptions/HttpError.js'; const app = express(); const port = process.env.PORT || 3005; const PUBLIC_SITE_URL = process.env.PUBLIC_SITE_URL || 'https://tt-tagebuch.de'; const publicSiteOrigin = new URL(PUBLIC_SITE_URL).origin; const publicSiteHost = new URL(PUBLIC_SITE_URL).host; const publicSiteProtocol = new URL(PUBLIC_SITE_URL).protocol.replace(':', ''); function captureRawBody(req, res, buf, encoding) { if (!buf || buf.length === 0) return; req.rawBody = buf.toString(encoding || 'utf8'); } const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const SEO_DEFAULTS = { title: 'Trainingstagebuch – Vereinsverwaltung für Tischtennis, Trainingsplanung & Turniere', description: 'Trainingstagebuch ist die Software für Tischtennisvereine: Mitgliederverwaltung, Trainingsplanung, Trainingstagebuch, Gruppen, Turniere, Team-Management, Statistiken und MyTischtennis-Integration.', robots: 'index,follow', }; const SEO_ROUTE_CONFIG = { '/': { title: SEO_DEFAULTS.title, description: SEO_DEFAULTS.description, robots: 'index,follow', }, '/vereinssoftware-tischtennis': { title: 'Vereinssoftware für Tischtennisvereine | Trainingstagebuch', description: 'Webbasierte Vereinssoftware für Tischtennisvereine mit Mitgliederverwaltung, Trainingsplanung, Mannschaftsorganisation, Turnieren und Auswertungen.', robots: 'index,follow', }, '/mitgliederverwaltung-verein': { title: 'Mitgliederverwaltung für Vereine | Trainingstagebuch', description: 'Mitgliederverwaltung für Vereine mit Stammdaten, Rollen, Gruppenbezug und organisatorischer Verbindung zu Training und Vereinsabläufen.', robots: 'index,follow', }, '/trainingsplanung-tischtennis': { title: 'Trainingsplanung für Tischtennisvereine | Trainingstagebuch', description: 'Trainingsplanung für Tischtennisvereine mit Gruppen, Anwesenheiten, Trainingstagebuch und digitaler Organisation von Trainingstagen.', robots: 'index,follow', }, '/turniersoftware-tischtennis': { title: 'Turniersoftware für Tischtennis | Trainingstagebuch', description: 'Turniersoftware für Tischtennisvereine mit Teilnehmerverwaltung, Gruppen, Paarungen, Ergebnissen und Organisation interner oder offizieller Turniere.', robots: 'index,follow', }, '/login': { title: 'Login | Trainingstagebuch', description: 'Im Trainingstagebuch einloggen und Vereinsdaten, Trainingsplanung, Mitglieder und Turniere verwalten.', robots: 'noindex,follow', }, '/register': { title: 'Registrieren | Trainingstagebuch', description: 'Kostenlos im Trainingstagebuch registrieren und die Vereinsverwaltung für Tischtennisvereine kennenlernen.', robots: 'noindex,follow', }, '/activate': { title: 'Konto aktivieren | Trainingstagebuch', description: 'Aktivierung des Benutzerkontos im Trainingstagebuch.', robots: 'noindex,follow', }, '/forgot-password': { title: 'Passwort vergessen | Trainingstagebuch', description: 'Zugang zum Trainingstagebuch wiederherstellen.', robots: 'noindex,follow', }, '/reset-password': { title: 'Passwort zurücksetzen | Trainingstagebuch', description: 'Passwort im Trainingstagebuch sicher zurücksetzen.', robots: 'noindex,follow', }, '/impressum': { title: 'Impressum | Trainingstagebuch', description: 'Impressum von Trainingstagebuch.', robots: 'index,follow', }, '/datenschutz': { title: 'Datenschutzerklärung | Trainingstagebuch', description: 'Datenschutzerklärung von Trainingstagebuch.', robots: 'index,follow', }, }; const SEO_NOINDEX_PREFIXES = [ '/createclub', '/showclub', '/members', '/diary', '/pending-approvals', '/schedule', '/tournaments', '/tournament-participations', '/training-stats', '/club-settings', '/predefined-activities', '/mytischtennis-account', '/clicktt-account', '/team-management', '/permissions', '/logs', '/clicktt', '/member-transfer-settings', '/personal-settings', '/orders', ]; function normalizeSeoPath(pathname = '/') { if (!pathname || pathname === '') return '/'; if (pathname === '/') return '/'; return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; } function getSeoConfigForPath(pathname = '/') { const normalizedPath = normalizeSeoPath(pathname); const matchedPrefix = Object.keys(SEO_ROUTE_CONFIG) .filter((routePath) => routePath !== '/' && normalizedPath.startsWith(routePath)) .sort((a, b) => b.length - a.length)[0]; const configuredSeo = (matchedPrefix && SEO_ROUTE_CONFIG[matchedPrefix]) || SEO_ROUTE_CONFIG[normalizedPath]; if (configuredSeo) { return configuredSeo; } const shouldNoindex = SEO_NOINDEX_PREFIXES.some((routePath) => normalizedPath.startsWith(routePath)); return { ...SEO_DEFAULTS, robots: shouldNoindex ? 'noindex,follow' : SEO_DEFAULTS.robots }; } function escapeHtmlAttribute(value = '') { return String(value) .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } function isPublicFacingHost(host = '') { const normalizedHost = String(host).toLowerCase().replace(/:\d+$/, ''); return normalizedHost === 'tt-tagebuch.de' || normalizedHost === 'www.tt-tagebuch.de'; } function getCanonicalRequestContext(req) { const forwardedProtoHeader = req.get('x-forwarded-proto'); const forwardedHostHeader = req.get('x-forwarded-host'); const forwardedProto = forwardedProtoHeader?.split(',')[0]?.trim(); const forwardedHost = forwardedHostHeader?.split(',')[0]?.trim(); const requestHost = forwardedHost || req.get('host') || publicSiteHost; const requestProtocol = forwardedProto || req.protocol || publicSiteProtocol; const canonicalHost = isPublicFacingHost(requestHost) ? requestHost.replace(/^www\./, '') : publicSiteHost; const canonicalProtocol = isPublicFacingHost(requestHost) ? requestProtocol : publicSiteProtocol; return { requestHost, requestProtocol, canonicalHost, canonicalProtocol, canonicalOrigin: `${canonicalProtocol}://${canonicalHost}`, }; } function stripJsonLdByType(html, schemaType) { const escapedType = schemaType.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const pattern = new RegExp( `