302 lines
9.1 KiB
JavaScript
302 lines
9.1 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');
|
|
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(`
|
|
<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/`);
|
|
});
|
|
|
|
// ============================================================================
|
|
// 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);
|
|
}); |