const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const cookieParser = require('cookie-parser'); const morgan = require('morgan'); const path = require('path'); const cron = require('node-cron'); const http = require('http'); const WebSocket = require('ws'); const config = require('./config'); const { initAuth } = require('./auth'); const routes = require('./routes'); const webhookRoutes = require('./webhook'); const { checkAllApps, getAllStatuses } = require('./healthcheck'); const { getCommits } = require('./gitea'); const { initDynamicApps } = require('./app-creator'); const { initSSHWebSocket } = require('./ssh'); const app = express(); const server = http.createServer(app); // WebSocket server pour les mises à jour en temps réel const wss = new WebSocket.Server({ server, path: '/ws' }); initSSHWebSocket(server); // Middleware app.use(helmet({ contentSecurityPolicy: false, crossOriginEmbedderPolicy: false, })); app.use(cors({ origin: true, credentials: true, })); app.use(express.json({ limit: '50mb' })); app.use(express.urlencoded({ extended: true, limit: '50mb' })); app.use(cookieParser()); app.use(morgan('combined')); // Route webhook montée en premier avec raw body pour la vérification HMAC app.use('/api/webhook', express.raw({ type: 'application/json', limit: '10mb' }), (req, res, next) => { if (req.body && Buffer.isBuffer(req.body)) { req.rawBody = req.body; try { req.body = JSON.parse(req.body.toString()); } catch(e) { req.body = {}; } } next(); }, webhookRoutes); // API routes app.use('/api', routes); // Servir le frontend en production const frontendPath = path.join(__dirname, '../../frontend/dist'); app.use(express.static(frontendPath)); // SPA fallback app.get('*', (req, res) => { if (!req.path.startsWith('/api') && !req.path.startsWith('/ws')) { res.sendFile(path.join(frontendPath, 'index.html')); } }); // Broadcast WebSocket message global.wsBroadcast = broadcast; function broadcast(data) { const message = JSON.stringify(data); wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(message); } }); } // Health check périodique async function runHealthChecks() { try { const results = await checkAllApps(); broadcast({ type: 'health_update', data: results }); // Vérifier les nouveaux commits Gitea for (const appConfig of config.apps) { try { const commits = await getCommits(appConfig.giteaOwner, appConfig.giteaRepo, 5); if (commits.length > 0) { broadcast({ type: 'gitea_commits', data: { appId: appConfig.id, commits }, }); } } catch (e) { // Ignorer les erreurs Gitea } } } catch (err) { console.error('Erreur health check:', err.message); } } // Démarrage async function start() { await initAuth(); // Charger les applications dynamiques initDynamicApps(); // Premier health check setTimeout(runHealthChecks, 2000); // Health check toutes les 30 secondes setInterval(runHealthChecks, config.healthCheckInterval); server.listen(config.port, '0.0.0.0', () => { console.log(`Manus Dashboard Backend démarré sur le port ${config.port}`); console.log(`Frontend servi depuis: ${frontendPath}`); }); } // WebSocket connection handler wss.on('connection', (ws) => { console.log('Nouvelle connexion WebSocket'); // Envoyer les statuts actuels const statuses = getAllStatuses(); if (statuses.length > 0) { ws.send(JSON.stringify({ type: 'health_update', data: statuses })); } ws.on('close', () => { console.log('Connexion WebSocket fermée'); }); }); start().catch((err) => { console.error('Erreur au démarrage:', err); process.exit(1); });