import express from 'express'; import path from 'path'; import { fileURLToPath } from 'url'; import sequelize from './database.js'; import cors from 'cors'; 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 memberActivityRoutes from './routes/memberActivityRoutes.js'; import permissionRoutes from './routes/permissionRoutes.js'; import apiLogRoutes from './routes/apiLogRoutes.js'; import memberTransferConfigRoutes from './routes/memberTransferConfigRoutes.js'; import schedulerService from './services/schedulerService.js'; import { requestLoggingMiddleware } from './middleware/requestLoggingMiddleware.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/member-activities', memberActivityRoutes); app.use('/api/permissions', permissionRoutes); app.use('/api/logs', apiLogRoutes); app.use('/api/member-transfer-config', memberTransferConfigRoutes); app.use(express.static(path.join(__dirname, '../frontend/dist'))); // Catch-All Handler für Frontend-Routen (muss nach den API-Routen stehen) app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../frontend/dist/index.html')); }); (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'; 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); // Start scheduler service schedulerService.start(); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); console.log('Scheduler service started:'); console.log(' - Rating updates: 6:00 AM daily'); console.log(' - Match results fetch: 6:30 AM daily'); }); } catch (err) { console.error('Unable to synchronize the database:', err); } })();