This commit revises the deployment documentation for Socket.IO, emphasizing the need to restart the backend server for HTTPS support on port 3051. It introduces a new diagnostic script to check SSL certificate existence, server accessibility, and port status. Additionally, the backend server configuration is updated to ensure it listens on all interfaces (0.0.0.0), enhancing accessibility. These changes improve clarity and troubleshooting guidance for deploying Socket.IO over HTTPS.
298 lines
13 KiB
JavaScript
298 lines
13 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';
|
|
|
|
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;
|
|
const message = err?.message || 'Interner Serverfehler';
|
|
|
|
const response = {
|
|
success: false,
|
|
message,
|
|
error: message
|
|
};
|
|
|
|
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);
|
|
|
|
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';
|
|
|
|
// 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);
|
|
}
|
|
})(); |