Files
trainingstagebuch/backend/server.js
Torsten Schulz (local) d10b663dc1 Refactor error handling and localization in frontend components
This commit enhances the error handling and user interface of various frontend components by integrating localization support. It updates error messages and titles across multiple views and dialogs to utilize the translation function, ensuring a consistent user experience in different languages. Additionally, it refines the handling of error messages in the MyTischtennis account and member transfer settings, improving clarity and user feedback during operations.
2025-11-16 20:48:31 +01:00

348 lines
14 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);
});
// Globale Fehlerbehandlung für API-Routen
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);
});
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')));
(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;
try {
const httpsOptions = {
key: fs.readFileSync('/etc/letsencrypt/live/tt-tagebuch.de/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/tt-tagebuch.de/fullchain.pem')
};
const httpsServer = https.createServer(httpsOptions, app);
// Initialisiere Socket.IO auf HTTPS-Server
initializeSocketIO(httpsServer);
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)');
// Fallback: Socket.IO auf HTTP-Server
initializeSocketIO(httpServer);
}
} catch (err) {
console.error('Unable to synchronize the database:', err);
}
})();