Files
Ninjaserver/server.js

372 lines
12 KiB
JavaScript

/**
* 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 all origins
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
}
}));
// ============================================================================
// 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(`
<h1>Zugriff verweigert</h1>
<p>Sie benötigen Level 2 Zugriff für den Lizenzgenerator.</p>
<a href="/admin-dashboard">Zurück zum Dashboard</a>
`);
}
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);
});