/** * 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'); const cron = require('node-cron'); require('dotenv').config(); // Route Imports const { router: apiRoutes, requireApiKey } = require('./routes/api'); // Achievement System const AchievementSystem = require('./lib/achievementSystem'); // ============================================================================ // 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 // ============================================================================ // CORS Configuration - Allow all origins for development app.use((req, res, next) => { // Allow specific origins when credentials are needed const origin = req.headers.origin; if (origin && (origin.includes('ninja.reptilfpv.de') || origin.includes('localhost') || origin.includes('127.0.0.1'))) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Credentials', 'true'); // Allow cookies } else { res.setHeader('Access-Control-Allow-Origin', '*'); } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH'); res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-API-Key'); res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours // Handle preflight requests if (req.method === 'OPTIONS') { res.status(200).end(); return; } next(); }); // 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 sameSite: 'lax' // Allow cookies in cross-origin requests } })); // ============================================================================ // 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/`); }); // ============================================================================ // SCHEDULED TASKS // ============================================================================ /** * Scheduled Function - Runs daily at 7 PM (19:00) * Führt tägliche Achievement-Prüfung durch */ async function scheduledTaskAt7PM() { const now = new Date(); console.log(`⏰ Geplante Aufgabe ausgeführt um ${now.toLocaleString('de-DE')}`); try { // Initialisiere Achievement-System const achievementSystem = new AchievementSystem(); // Führe tägliche Achievement-Prüfung durch const result = await achievementSystem.runDailyAchievementCheck(); console.log(`🏆 Achievement-Prüfung abgeschlossen:`); console.log(` 📊 ${result.totalNewAchievements} neue Achievements vergeben`); console.log(` 👥 ${result.playerAchievements.length} Spieler haben neue Achievements erhalten`); // Zeige Details der neuen Achievements if (result.playerAchievements.length > 0) { console.log(`\n📋 Neue Achievements im Detail:`); result.playerAchievements.forEach(player => { console.log(` 👤 ${player.player}:`); player.achievements.forEach(achievement => { console.log(` ${achievement.icon} ${achievement.name} (+${achievement.points} Punkte)`); }); }); } console.log('✅ Geplante Aufgabe erfolgreich abgeschlossen'); } catch (error) { console.error('❌ Fehler bei der geplanten Aufgabe:', error); } } // Cron Job: Täglich um 19:00 Uhr (7 PM) // Format: Sekunde Minute Stunde Tag Monat Wochentag cron.schedule('0 0 19 * * *', scheduledTaskAt7PM, { scheduled: true, timezone: "Europe/Berlin" // Deutsche Zeitzone }); console.log('📅 Geplante Aufgabe eingerichtet: Täglich um 19:00 Uhr'); // ============================================================================ // 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); });