diff --git a/package-lock.json b/package-lock.json index 6400254..906ce69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,14 @@ "dependencies": { "@hisma/server-puppeteer": "^0.6.5", "bcrypt": "^5.1.1", + "discord-oauth2": "^2.12.1", "dotenv": "^16.3.1", "enhanced-postgres-mcp-server": "^1.0.1", "express": "^4.18.2", "express-session": "^1.17.3", "node-cron": "^4.2.1", + "passport": "^0.7.0", + "passport-discord": "^0.1.4", "pg": "^8.11.3", "socket.io": "^4.8.1", "swagger-jsdoc": "^6.2.8", @@ -832,6 +835,15 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -1243,6 +1255,12 @@ "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==", "license": "BSD-3-Clause" }, + "node_modules/discord-oauth2": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.12.1.tgz", + "integrity": "sha512-/Um39bRxVjcGHUu1YaTLangZvZveXjsX4BNsa1Iyd6OQG0jL972IBQGKD0mYqQswxC3bT+hqWSouabfI2RdaZA==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2648,6 +2666,12 @@ "set-blocking": "^2.0.0" } }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -2819,6 +2843,61 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-discord": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/passport-discord/-/passport-discord-0.1.4.tgz", + "integrity": "sha512-VJWPYqSOmh7SaCLw/C+k1ZqCzJnn2frrmQRx1YrcPJ3MQ+Oa31XclbbmqFICSvl8xv3Fqd6YWQ4H4p1MpIN9rA==", + "license": "ISC", + "dependencies": { + "passport-oauth2": "^1.5.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "license": "MIT", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2842,6 +2921,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -4010,6 +4094,12 @@ "node": ">= 0.8" } }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==", + "license": "MIT" + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -4909,6 +4999,11 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, + "base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" + }, "basic-ftp": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", @@ -5188,6 +5283,11 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1475386.tgz", "integrity": "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==" }, + "discord-oauth2": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.12.1.tgz", + "integrity": "sha512-/Um39bRxVjcGHUu1YaTLangZvZveXjsX4BNsa1Iyd6OQG0jL972IBQGKD0mYqQswxC3bT+hqWSouabfI2RdaZA==" + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -6134,6 +6234,11 @@ "set-blocking": "^2.0.0" } }, + "oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==" + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -6248,6 +6353,41 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-discord": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/passport-discord/-/passport-discord-0.1.4.tgz", + "integrity": "sha512-VJWPYqSOmh7SaCLw/C+k1ZqCzJnn2frrmQRx1YrcPJ3MQ+Oa31XclbbmqFICSvl8xv3Fqd6YWQ4H4p1MpIN9rA==", + "requires": { + "passport-oauth2": "^1.5.0" + } + }, + "passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "requires": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -6263,6 +6403,11 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -7062,6 +7207,11 @@ "random-bytes": "~1.0.0" } }, + "uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", diff --git a/package.json b/package.json index b03a2c2..e67b8f9 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,14 @@ "dependencies": { "@hisma/server-puppeteer": "^0.6.5", "bcrypt": "^5.1.1", + "discord-oauth2": "^2.12.1", "dotenv": "^16.3.1", "enhanced-postgres-mcp-server": "^1.0.1", "express": "^4.18.2", "express-session": "^1.17.3", "node-cron": "^4.2.1", + "passport": "^0.7.0", + "passport-discord": "^0.1.4", "pg": "^8.11.3", "socket.io": "^4.8.1", "swagger-jsdoc": "^6.2.8", diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..8de5af0 --- /dev/null +++ b/public/404.html @@ -0,0 +1,336 @@ + + +
+ + +
+ Die Seite, die du suchst, hat sich wie ein echter Ninja versteckt.
+ Vielleicht ist sie auf einer geheimen Mission oder hat sich in der Dunkelheit versteckt.
+
Sie benötigen Level 2 Zugriff für den Lizenzgenerator.
- Zurück zum Dashboard - `); - } - res.sendFile(path.join(__dirname, 'public', 'generator.html')); -}); - -/** - * Login Page - * Authentifizierungsseite für Admin-Benutzer - */ -app.get('/login', (req, res) => { - // Redirect to main page if already authenticated - if (req.session.userId) { - return res.redirect('/'); - } - res.sendFile(path.join(__dirname, 'public', 'login.html')); -}); - -/** - * Admin Dashboard Page - * Dashboard-Seite für eingeloggte Administratoren - * Authentifizierung wird client-side über Supabase gehandhabt - */ -app.get('/dashboard', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); -}); - -/** - * Reset Password Page - * Seite für das Zurücksetzen von Passwörtern über Supabase - * Wird von Supabase E-Mail-Links aufgerufen - */ -app.get('/reset-password.html', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'reset-password.html')); -}); - -/** - * Admin Login Page - * Lizenzgenerator Login-Seite für Admin-Benutzer - */ -app.get('/adminlogin.html', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'adminlogin.html')); -}); - -/** - * OAuth Callback Route - * Handles OAuth redirects from Supabase (Google, etc.) - */ -app.get('/auth/callback', (req, res) => { - // Redirect to the main page after OAuth callback - // Supabase handles the OAuth flow and redirects here - res.redirect('/'); -}); - -// ============================================================================ -// STATIC FILE SERVING -// ============================================================================ - -// Serve static files directly from public directory -app.use(express.static('public')); - -// Serve static files for public pages (CSS, JS, images) - legacy route -app.use('/public', express.static('public')); - -// Serve static files for login page -app.use('/login', express.static('public')); - -// ============================================================================ -// WEBSOCKET CONFIGURATION -// ============================================================================ - -/** - * WebSocket Connection Handler - * Verwaltet Real-time Verbindungen für Live-Updates - */ -io.on('connection', (socket) => { - // Client connected - connection is established - - socket.on('disconnect', () => { - // Client disconnected - cleanup if needed - }); -}); - -// Make Socket.IO instance available to other modules -app.set('io', io); - -// ============================================================================ -// ERROR HANDLING -// ============================================================================ - -// 404 Handler -app.use('*', (req, res) => { - res.status(404).json({ - success: false, - message: 'Route not found', - path: req.originalUrl - }); -}); - -// Global Error Handler -app.use((err, req, res, next) => { - console.error('Server Error:', err); - res.status(500).json({ - success: false, - message: 'Internal server error' - }); -}); - -// ============================================================================ -// SERVER STARTUP -// ============================================================================ - -/** - * Start the server and initialize all services - */ -server.listen(port, () => { - console.log(`🚀 Server läuft auf http://ninja.reptilfpv.de:${port}`); - console.log(`📊 Datenbank: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`); - console.log(`🔐 API-Key Authentifizierung aktiviert`); - console.log(`🔌 WebSocket-Server aktiviert`); - console.log(`📁 Static files: /public`); - console.log(`🌐 Unified API: /api/v1/`); - console.log(` 📖 Public: /api/v1/public/`); - console.log(` 🔒 Private: /api/v1/private/`); - console.log(` 🔐 Web: /api/v1/web/`); - console.log(` 👑 Admin: /api/v1/admin/`); -}); - -// ============================================================================ -// GRACEFUL SHUTDOWN -// ============================================================================ - -/** - * Handle graceful shutdown on SIGINT (Ctrl+C) - */ -process.on('SIGINT', async () => { - console.log('\n🛑 Server wird heruntergefahren...'); - - // Close server gracefully - server.close(() => { - console.log('✅ Server erfolgreich heruntergefahren'); - process.exit(0); - }); - - // Force exit after 5 seconds if graceful shutdown fails - setTimeout(() => { - console.log('⚠️ Forced shutdown after timeout'); - process.exit(1); - }, 5000); -}); - -/** - * Handle uncaught exceptions - */ -process.on('uncaughtException', (err) => { - console.error('Uncaught Exception:', err); - process.exit(1); -}); - -/** - * Handle unhandled promise rejections - */ -process.on('unhandledRejection', (reason, promise) => { - console.error('Unhandled Rejection at:', promise, 'reason:', reason); - process.exit(1); +/** + * NinjaCross Leaderboard Server + * + * Hauptserver für das NinjaCross Timer-System mit: + * - Express.js Web-Server + * - Socket.IO für Real-time Updates + * - PostgreSQL Datenbankanbindung + * - API-Key Authentifizierung + * - Session-basierte Web-Authentifizierung + * + * @author NinjaCross Team + * @version 1.0.0 + */ + +// ============================================================================ +// DEPENDENCIES & IMPORTS +// ============================================================================ + +const express = require('express'); +const path = require('path'); +const session = require('express-session'); +const { createServer } = require('http'); +const { Server } = require('socket.io'); +const swaggerUi = require('swagger-ui-express'); +const swaggerSpecs = require('./swagger'); +require('dotenv').config(); + +// Route Imports +const { router: apiRoutes, requireApiKey } = require('./routes/api'); + +// ============================================================================ +// SERVER CONFIGURATION +// ============================================================================ + +const app = express(); +const server = createServer(app); +const port = process.env.PORT || 3000; + +// Socket.IO Configuration +const io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"] + } +}); + +// ============================================================================ +// MIDDLEWARE SETUP +// ============================================================================ + +// Body Parser Middleware +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); + +// Session Configuration +app.use(session({ + secret: process.env.SESSION_SECRET || 'kjhdizr3lhwho8fpjslgf825ß0hsd', + resave: false, + saveUninitialized: false, + cookie: { + secure: false, // Set to true when using HTTPS + maxAge: 24 * 60 * 60 * 1000, // 24 hours + httpOnly: true // Security: prevent XSS attacks + } +})); + + +// ============================================================================ +// AUTHENTICATION MIDDLEWARE +// ============================================================================ + +/** + * Web Interface Authentication Middleware + * Überprüft ob der Benutzer für das Web-Interface authentifiziert ist + */ +function requireWebAuth(req, res, next) { + if (req.session.userId) { + next(); + } else { + res.redirect('/login'); + } +} + +// ============================================================================ +// ROUTE SETUP +// ============================================================================ + +// Swagger API Documentation +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'Ninja Cross Parkour API Documentation' +})); + +// Unified API Routes (all under /api/v1/) +// - /api/v1/public/* - Public routes (no authentication) +// - /api/v1/private/* - API-Key protected routes +// - /api/v1/web/* - Session protected routes +// - /api/v1/admin/* - Admin protected routes +app.use('/api', apiRoutes); + +// ============================================================================ +// WEB INTERFACE ROUTES +// ============================================================================ + +/** + * Public Landing Page - NinjaCross Leaderboard + * Hauptseite mit dem öffentlichen Leaderboard + */ +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +/** + * Admin Dashboard Page + * Hauptdashboard für Admin-Benutzer + */ +app.get('/admin-dashboard', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'admin-dashboard.html')); +}); + +/** + * Admin Generator Page + * Geschützte Seite für die Lizenz-Generierung (Level 2 Zugriff erforderlich) + */ +app.get('/generator', requireWebAuth, (req, res) => { + // Prüfe Zugriffslevel für Generator + if (req.session.accessLevel < 2) { + return res.status(403).send(` +Sie benötigen Level 2 Zugriff für den Lizenzgenerator.
+ Zurück zum Dashboard + `); + } + res.sendFile(path.join(__dirname, 'public', 'generator.html')); +}); + +/** + * Login Page + * Authentifizierungsseite für Admin-Benutzer + */ +app.get('/login', (req, res) => { + // Redirect to main page if already authenticated + if (req.session.userId) { + return res.redirect('/'); + } + res.sendFile(path.join(__dirname, 'public', 'login.html')); +}); + +/** + * Admin Dashboard Page + * Dashboard-Seite für eingeloggte Administratoren + * Authentifizierung wird client-side über Supabase gehandhabt + */ +app.get('/dashboard', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'dashboard.html')); +}); + +/** + * Reset Password Page + * Seite für das Zurücksetzen von Passwörtern über Supabase + * Wird von Supabase E-Mail-Links aufgerufen + */ +app.get('/reset-password.html', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'reset-password.html')); +}); + +/** + * Admin Login Page + * Lizenzgenerator Login-Seite für Admin-Benutzer + */ +app.get('/adminlogin.html', (req, res) => { + res.sendFile(path.join(__dirname, 'public', 'adminlogin.html')); +}); + +/** + * OAuth Callback Route + * Handles OAuth redirects from Supabase (Google, etc.) + */ +app.get('/auth/callback', (req, res) => { + // Redirect to the main page after OAuth callback + // Supabase handles the OAuth flow and redirects here + res.redirect('/'); +}); + +// ============================================================================ +// STATIC FILE SERVING +// ============================================================================ + +// Serve static files directly from public directory +app.use(express.static('public')); + +// Serve static files for public pages (CSS, JS, images) - legacy route +app.use('/public', express.static('public')); + +// Serve static files for login page +app.use('/login', express.static('public')); + +// ============================================================================ +// WEBSOCKET CONFIGURATION +// ============================================================================ + +/** + * WebSocket Connection Handler + * Verwaltet Real-time Verbindungen für Live-Updates + */ +io.on('connection', (socket) => { + // Client connected - connection is established + + socket.on('disconnect', () => { + // Client disconnected - cleanup if needed + }); +}); + +// Make Socket.IO instance available to other modules +app.set('io', io); + +// ============================================================================ +// ERROR HANDLING +// ============================================================================ + +// 404 Handler +app.use('*', (req, res) => { + // Check if it's an API request + if (req.originalUrl.startsWith('/api/')) { + res.status(404).json({ + success: false, + message: 'Route not found', + path: req.originalUrl + }); + } else { + // Serve custom 404 page for non-API requests + res.status(404).sendFile(path.join(__dirname, 'public', '404.html')); + } +}); + +// Global Error Handler +app.use((err, req, res, next) => { + console.error('Server Error:', err); + res.status(500).json({ + success: false, + message: 'Internal server error' + }); +}); + +// ============================================================================ +// SERVER STARTUP +// ============================================================================ + +/** + * Start the server and initialize all services + */ +server.listen(port, () => { + console.log(`🚀 Server läuft auf http://ninja.reptilfpv.de:${port}`); + console.log(`📊 Datenbank: ${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}`); + console.log(`🔐 API-Key Authentifizierung aktiviert`); + console.log(`🔌 WebSocket-Server aktiviert`); + console.log(`📁 Static files: /public`); + console.log(`🌐 Unified API: /api/v1/`); + console.log(` 📖 Public: /api/v1/public/`); + console.log(` 🔒 Private: /api/v1/private/`); + console.log(` 🔐 Web: /api/v1/web/`); + console.log(` 👑 Admin: /api/v1/admin/`); +}); + +// ============================================================================ +// GRACEFUL SHUTDOWN +// ============================================================================ + +/** + * Handle graceful shutdown on SIGINT (Ctrl+C) + */ +process.on('SIGINT', async () => { + console.log('\n🛑 Server wird heruntergefahren...'); + + // Close server gracefully + server.close(() => { + console.log('✅ Server erfolgreich heruntergefahren'); + process.exit(0); + }); + + // Force exit after 5 seconds if graceful shutdown fails + setTimeout(() => { + console.log('⚠️ Forced shutdown after timeout'); + process.exit(1); + }, 5000); +}); + +/** + * Handle uncaught exceptions + */ +process.on('uncaughtException', (err) => { + console.error('Uncaught Exception:', err); + process.exit(1); +}); + +/** + * Handle unhandled promise rejections + */ +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); + process.exit(1); }); \ No newline at end of file