- Introduced new endpoints for managing tournament pairings, including creating, updating, and deleting pairings. - Updated the tournament service to handle pairing logic, ensuring validation for participants and preventing duplicate pairings. - Enhanced participant management by adding class-based checks for gender and age restrictions when adding participants. - Updated the tournament controller and routes to support the new pairing features and improved participant handling. - Added localization support for new UI elements related to pairings in the frontend, enhancing user experience.
363 lines
15 KiB
JavaScript
363 lines
15 KiB
JavaScript
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 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, MyTischtennisUpdateHistory, MyTischtennisFetchLog, ApiLog, MemberTransferConfig, MemberContact
|
||
} 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 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 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 memberTransferConfigRoutes from './routes/memberTransferConfigRoutes.js';
|
||
import trainingGroupRoutes from './routes/trainingGroupRoutes.js';
|
||
import trainingTimeRoutes from './routes/trainingTimeRoutes.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 __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
app.use(cors({
|
||
origin: true,
|
||
credentials: true,
|
||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||
allowedHeaders: ['Content-Type', 'Authorization', 'authcode', 'userid']
|
||
}));
|
||
app.use(express.json());
|
||
|
||
// Request Logging Middleware - loggt alle API-Requests
|
||
// Wichtig: userId wird später in authMiddleware gesetzt, aber Middleware funktioniert auch ohne
|
||
app.use(requestLoggingMiddleware);
|
||
|
||
// Globale Fehlerbehandlung, damit der Server bei unerwarteten Fehlern nicht hart abstürzt
|
||
process.on('uncaughtException', (err) => {
|
||
console.error('[uncaughtException]', err);
|
||
});
|
||
process.on('unhandledRejection', (reason, promise) => {
|
||
console.error('[unhandledRejection]', reason);
|
||
});
|
||
|
||
app.use('/api/auth', authRoutes);
|
||
app.use('/api/clubs', clubRoutes);
|
||
app.use('/api/clubmembers', memberRoutes);
|
||
app.use('/api/diary', diaryRoutes);
|
||
app.use('/api/participants', participantRoutes);
|
||
app.use('/api/activities', activityRoutes);
|
||
app.use('/api/membernotes', memberNoteRoutes);
|
||
app.use('/api/diarynotes', diaryNoteRoutes);
|
||
app.use('/api/tags', diaryTagRoutes);
|
||
app.use('/api/diarymember', diaryMemberRoutes);
|
||
app.use('/api/predefined-activities', predefinedActivityRoutes);
|
||
app.use('/api/diary-date-activities', diaryDateActivityRoutes);
|
||
app.use('/api/diary-member-activities', diaryMemberActivityRoutes);
|
||
app.use('/api/matches', matchRoutes);
|
||
app.use('/api/group', groupRoutes);
|
||
app.use('/api/diarydatetags', diaryDateTagRoutes);
|
||
app.use('/api/session', sessionRoutes);
|
||
app.use('/api/tournament', tournamentRoutes);
|
||
app.use('/api/accident', accidentRoutes);
|
||
app.use('/api/training-stats', trainingStatsRoutes);
|
||
app.use('/api/official-tournaments', officialTournamentRoutes);
|
||
app.use('/api/mytischtennis', myTischtennisRoutes);
|
||
app.use('/api/teams', teamRoutes);
|
||
app.use('/api/club-teams', clubTeamRoutes);
|
||
app.use('/api/team-documents', teamDocumentRoutes);
|
||
app.use('/api/seasons', seasonRoutes);
|
||
app.use('/api/proxy/nuscore', nuscoreSimpleProxyRoutes);
|
||
app.use('/api/nuscore', nuscoreApiRoutes);
|
||
app.use('/api/member-activities', memberActivityRoutes);
|
||
app.use('/api/permissions', permissionRoutes);
|
||
app.use('/api/logs', apiLogRoutes);
|
||
app.use('/api/member-transfer-config', memberTransferConfigRoutes);
|
||
app.use('/api/training-groups', trainingGroupRoutes);
|
||
app.use('/api/training-times', trainingTimeRoutes);
|
||
|
||
// Middleware für dynamischen kanonischen Tag (vor express.static)
|
||
const setCanonicalTag = (req, res, next) => {
|
||
// Nur für HTML-Anfragen (nicht für API, Assets, etc.)
|
||
if (req.path.startsWith('/api') || req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|mp3|webmanifest|xml|txt)$/)) {
|
||
return next();
|
||
}
|
||
|
||
// Prüfe, ob die Datei als statische Datei existiert (außer index.html)
|
||
const staticPath = path.join(__dirname, '../frontend/dist', req.path);
|
||
fs.access(staticPath, fs.constants.F_OK, (err) => {
|
||
if (!err && req.path !== '/' && req.path !== '/index.html') {
|
||
// Datei existiert und ist nicht index.html, lasse express.static sie servieren
|
||
return next();
|
||
}
|
||
|
||
// Datei existiert nicht oder ist index.html, serviere index.html mit dynamischem kanonischen Tag
|
||
const indexPath = path.join(__dirname, '../frontend/dist/index.html');
|
||
fs.readFile(indexPath, 'utf8', (err, data) => {
|
||
if (err) {
|
||
return next();
|
||
}
|
||
|
||
// Bestimme die kanonische URL (bevorzuge non-www)
|
||
const protocol = req.protocol || 'https';
|
||
const host = req.get('host') || 'tt-tagebuch.de';
|
||
const canonicalHost = host.replace(/^www\./, ''); // Entferne www falls vorhanden
|
||
const canonicalUrl = `${protocol}://${canonicalHost}${req.path === '/' ? '' : req.path}`;
|
||
|
||
// Ersetze den kanonischen Tag
|
||
const updatedData = data.replace(
|
||
/<link rel="canonical" href="[^"]*" \/>/,
|
||
`<link rel="canonical" href="${canonicalUrl}" />`
|
||
);
|
||
|
||
res.setHeader('Content-Type', 'text/html');
|
||
res.send(updatedData);
|
||
});
|
||
});
|
||
};
|
||
|
||
app.use(setCanonicalTag);
|
||
app.use(express.static(path.join(__dirname, '../frontend/dist')));
|
||
|
||
// Globale Fehlerbehandlung für API-Routen (MUSS nach allen Routes sein!)
|
||
app.use((err, req, res, next) => {
|
||
if (res.headersSent) {
|
||
return next(err);
|
||
}
|
||
|
||
const status = err?.statusCode || err?.status || 500;
|
||
|
||
// Unterstützung für Fehlercodes
|
||
let errorResponse;
|
||
if (err instanceof HttpError && err.errorCode) {
|
||
// Neues Format mit Fehlercode
|
||
errorResponse = err.toJSON();
|
||
} else {
|
||
// Legacy-Format: String-Nachricht
|
||
const message = err?.message || 'Interner Serverfehler';
|
||
errorResponse = {
|
||
message
|
||
};
|
||
}
|
||
|
||
const response = {
|
||
success: false,
|
||
...errorResponse,
|
||
// Für Rückwärtskompatibilität: error-Feld mit Nachricht
|
||
error: errorResponse.message || errorResponse.code || 'Interner Serverfehler'
|
||
};
|
||
|
||
if (process.env.NODE_ENV === 'dev' || process.env.NODE_ENV === 'development') {
|
||
response.debug = {
|
||
stack: err?.stack || null
|
||
};
|
||
}
|
||
|
||
console.error('[ExpressError]', err);
|
||
res.status(status).json(response);
|
||
});
|
||
|
||
(async () => {
|
||
try {
|
||
await sequelize.authenticate();
|
||
|
||
// Einmalige Migration: deutsche Spaltennamen -> englische
|
||
const renameColumnIfExists = async (table, from, to, typeSql) => {
|
||
try {
|
||
const [rows] = await sequelize.query(`SHOW COLUMNS FROM \`${table}\` LIKE :col`, { replacements: { col: from } });
|
||
if (Array.isArray(rows) && rows.length > 0) {
|
||
await sequelize.query(`ALTER TABLE \`${table}\` CHANGE \`${from}\` \`${to}\` ${typeSql}`);
|
||
}
|
||
} catch (e) {
|
||
console.error(`[migration] Failed to rename ${table}.${from} -> ${to}:`, e.message);
|
||
}
|
||
};
|
||
|
||
// official_competitions
|
||
await renameColumnIfExists('official_competitions', 'altersklasse_wettbewerb', 'age_class_competition', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'leistungsklasse', 'performance_class', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'startzeit', 'start_time', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'meldeschluss_datum', 'registration_deadline_date', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'meldeschluss_online', 'registration_deadline_online', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'stichtag', 'cutoff_date', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'offen_fuer', 'open_to', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'vorrunde', 'preliminary_round', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'endrunde', 'final_round', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'max_teilnehmer', 'max_participants', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_competitions', 'startgeld', 'entry_fee', 'VARCHAR(255) NULL');
|
||
|
||
// official_tournaments
|
||
await renameColumnIfExists('official_tournaments', 'termin', 'event_date', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_tournaments', 'veranstalter', 'organizer', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_tournaments', 'ausrichter', 'host', 'VARCHAR(255) NULL');
|
||
await renameColumnIfExists('official_tournaments', 'austragungsorte', 'venues', 'TEXT NULL');
|
||
await renameColumnIfExists('official_tournaments', 'konkurrenztypen', 'competition_types', 'TEXT NULL');
|
||
await renameColumnIfExists('official_tournaments', 'meldeschluesse', 'registration_deadlines', 'TEXT NULL');
|
||
|
||
const isDev = process.env.STAGE === 'dev';
|
||
|
||
// Foreign Keys temporär deaktivieren für MySQL
|
||
try {
|
||
await sequelize.query('SET FOREIGN_KEY_CHECKS = 0');
|
||
} catch (e) {
|
||
console.warn('[sync] Could not disable foreign key checks:', e?.message);
|
||
}
|
||
|
||
const safeSync = async (model) => {
|
||
try {
|
||
if (isDev) {
|
||
await model.sync({ alter: true });
|
||
} else {
|
||
await model.sync();
|
||
}
|
||
} catch (e) {
|
||
try {
|
||
console.error(`[sync] ${model?.name || 'model'} alter failed:`, e?.message || e);
|
||
await model.sync();
|
||
} catch (e2) {
|
||
console.error(`[sync] fallback failed for ${model?.name || 'model'}:`, e2?.message || e2);
|
||
}
|
||
}
|
||
};
|
||
|
||
await safeSync(User);
|
||
await safeSync(Club);
|
||
await safeSync(UserClub);
|
||
await safeSync(Log);
|
||
await safeSync(Member);
|
||
await safeSync(DiaryDate);
|
||
await safeSync(Participant);
|
||
await safeSync(Activity);
|
||
await safeSync(MemberNote);
|
||
await safeSync(DiaryNote);
|
||
await safeSync(DiaryTag);
|
||
await safeSync(MemberDiaryTag);
|
||
await safeSync(DiaryDateTag);
|
||
await safeSync(DiaryMemberTag);
|
||
await safeSync(DiaryMemberNote);
|
||
await safeSync(PredefinedActivity);
|
||
await safeSync(PredefinedActivityImage);
|
||
await safeSync(DiaryDateActivity);
|
||
await safeSync(DiaryMemberActivity);
|
||
await safeSync(OfficialTournament);
|
||
await safeSync(OfficialCompetition);
|
||
await safeSync(OfficialCompetitionMember);
|
||
await safeSync(Season);
|
||
await safeSync(League);
|
||
await safeSync(Team);
|
||
await safeSync(Location);
|
||
await safeSync(Match);
|
||
await safeSync(Group);
|
||
await safeSync(GroupActivity);
|
||
await safeSync(Tournament);
|
||
await safeSync(TournamentGroup);
|
||
await safeSync(TournamentMember);
|
||
await safeSync(TournamentMatch);
|
||
await safeSync(TournamentResult);
|
||
await safeSync(Accident);
|
||
await safeSync(UserToken);
|
||
await safeSync(MyTischtennis);
|
||
await safeSync(MyTischtennisUpdateHistory);
|
||
await safeSync(MyTischtennisFetchLog);
|
||
await safeSync(ApiLog);
|
||
await safeSync(MemberTransferConfig);
|
||
await safeSync(MemberContact);
|
||
await safeSync(ClubTeam);
|
||
await safeSync(TeamDocument);
|
||
|
||
// Foreign Keys wieder aktivieren
|
||
try {
|
||
await sequelize.query('SET FOREIGN_KEY_CHECKS = 1');
|
||
} catch (e) {
|
||
console.warn('[sync] Could not enable foreign key checks:', e?.message);
|
||
}
|
||
|
||
// Start scheduler service
|
||
schedulerService.start();
|
||
|
||
// Erstelle HTTP-Server für API
|
||
const httpServer = createServer(app);
|
||
httpServer.listen(port, () => {
|
||
console.log(`🚀 HTTP-Server läuft auf Port ${port}`);
|
||
});
|
||
|
||
// Erstelle HTTPS-Server für Socket.IO (direkt mit SSL)
|
||
const httpsPort = process.env.HTTPS_PORT || 3051;
|
||
let socketIOInitialized = false;
|
||
|
||
// Prüfe, ob SSL-Zertifikate vorhanden sind
|
||
const sslKeyPath = '/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem';
|
||
const sslCertPath = '/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem';
|
||
|
||
if (fs.existsSync(sslKeyPath) && fs.existsSync(sslCertPath)) {
|
||
try {
|
||
const httpsOptions = {
|
||
key: fs.readFileSync(sslKeyPath),
|
||
cert: fs.readFileSync(sslCertPath)
|
||
};
|
||
|
||
const httpsServer = https.createServer(httpsOptions, app);
|
||
|
||
// Initialisiere Socket.IO auf HTTPS-Server
|
||
initializeSocketIO(httpsServer);
|
||
socketIOInitialized = true;
|
||
|
||
httpsServer.listen(httpsPort, '0.0.0.0', () => {
|
||
console.log(`🚀 HTTPS-Server für Socket.IO läuft auf Port ${httpsPort}`);
|
||
});
|
||
} catch (err) {
|
||
console.error('⚠️ HTTPS-Server konnte nicht gestartet werden:', err.message);
|
||
console.log(' → Socket.IO läuft auf HTTP-Server (nur für Entwicklung)');
|
||
}
|
||
} else {
|
||
console.log('ℹ️ SSL-Zertifikate nicht gefunden - Socket.IO läuft auf HTTP-Server (nur für Entwicklung)');
|
||
}
|
||
|
||
// Fallback: Socket.IO auf HTTP-Server (wenn noch nicht initialisiert)
|
||
if (!socketIOInitialized) {
|
||
initializeSocketIO(httpServer);
|
||
console.log(' ✅ Socket.IO erfolgreich auf HTTP-Server initialisiert');
|
||
}
|
||
} catch (err) {
|
||
console.error('Unable to synchronize the database:', err);
|
||
}
|
||
})(); |