Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | /** * Point d'entrée du backend IliaCloud. * * Configure Express avec toutes les couches de sécurité : * - Helmet (headers de sécurité + CSP) * - CORS (origines autorisées) * - Rate limiting (global + auth) * - Cookie parser (pour les tokens httpOnly) * - CSRF (double submit cookie) * - Gestion centralisée des erreurs * * Monte les routes et démarre le serveur HTTP + WebSocket. * * @module index */ require('dotenv').config(); // ─── Validation des variables d'environnement requises ────────────────────── const REQUIRED_ENV = ['DATABASE_URL', 'JWT_SECRET', 'JWT_REFRESH_SECRET', 'ENCRYPTION_KEY']; for (const key of REQUIRED_ENV) { if (!process.env[key]) { console.error(`[FATAL] Variable d'environnement manquante : ${key}`); process.exit(1); } } if (process.env.JWT_SECRET.length < 32) { console.error('[FATAL] JWT_SECRET doit faire au moins 32 caracteres.'); process.exit(1); } const http = require('node:http'); const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const cookieParser = require('cookie-parser'); const rateLimit = require('express-rate-limit'); const { errorHandler } = require('./middleware/errorHandler'); const { createWebSocketServer } = require('./websocket'); const { startCollector } = require('./services/metricsCollector'); const { startUptimeChecker } = require('./services/uptimeChecker'); const { startBackupScheduler } = require('./services/backupScheduler'); const { startSslChecker } = require('./services/sslChecker'); const { startHttpErrorWatcher } = require('./services/httpErrorWatcher'); const { startDigestScheduler } = require('./services/digestScheduler'); const { startReportScheduler } = require('./services/reportScheduler'); const { runMigrations } = require('./db/autoMigrate'); // ─── Import des routes ─────────────────────────────────────────────────────── const authRoutes = require('./routes/auth'); const serversRoutes = require('./routes/servers'); const sshKeysRoutes = require('./routes/sshKeys'); const apiKeysRoutes = require('./routes/apiKeys'); const dashboardRoutes = require('./routes/dashboard'); const dockerRoutes = require('./routes/docker'); const logsRoutes = require('./routes/logs'); const quickActionsRoutes = require('./routes/quickActions'); const chatRoutes = require('./routes/chat'); const metricsRoutes = require('./routes/metrics'); const alertsRoutes = require('./routes/alerts'); const uptimeRoutes = require('./routes/uptime'); const webhooksRoutes = require('./routes/webhooks'); const backupsRoutes = require('./routes/backups'); const filesRoutes = require('./routes/files'); const cronRoutes = require('./routes/cron'); const sslRoutes = require('./routes/ssl'); const statusPageRoutes = require('./routes/statusPage'); const auditRoutes = require('./routes/audit'); const configExportRoutes = require('./routes/configExport'); const billingRoutes = require('./routes/billing'); const twofaRoutes = require('./routes/twofa'); const annotationsRoutes = require('./routes/annotations'); const digestRoutes = require('./routes/digest'); const reportRoutes = require('./routes/report'); const apiV1Routes = require('./routes/api/v1'); const userApiKeysRoutes = require('./routes/userApiKeys'); const contactRoutes = require('./routes/contact'); const groupsRoutes = require('./routes/groups'); const { auditLog } = require('./middleware/audit'); const app = express(); // ─── Trust proxy ───────────────────────────────────────────────────────────── // Derrière Traefik, Express reçoit X-Forwarded-For. Il faut lui dire de faire // confiance au premier proxy pour que le rate limiter identifie les vraies IPs. app.set('trust proxy', 1); // ─── Sécurité : Headers HTTP ───────────────────────────────────────────────── // Helmet configure automatiquement les headers de sécurité (X-Frame-Options, // X-Content-Type-Options, Strict-Transport-Security, etc.) app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], fontSrc: ["'self'", 'https://fonts.gstatic.com'], imgSrc: ["'self'", 'data:'], connectSrc: ["'self'", 'wss:', 'ws:'], }, }, })); // ─── Sécurité : CORS ───────────────────────────────────────────────────────── // Routes publiques (/contact, /status-pages/public) : CORS ouvert (origin *) // Autres routes : CORS strict (frontend uniquement, credentials) const CORS_OPEN_ROUTES = ['/contact', '/status-pages/public']; const corsOrigin = process.env.CORS_ORIGIN || 'http://localhost:5173'; app.use(cors({ origin: (origin, callback) => { // Pas d'origin = requete server-to-server (curl, etc.) — autoriser if (!origin) return callback(null, true); callback(null, true); }, methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token', 'X-Group-Id'], credentials: true, })); // Pour les routes publiques, overrider le header pour ne pas envoyer credentials // (credentials: true + origin: * est interdit par les navigateurs) app.use((req, res, next) => { if (CORS_OPEN_ROUTES.some(r => req.path.startsWith(r))) { res.setHeader('Access-Control-Allow-Origin', '*'); res.removeHeader('Access-Control-Allow-Credentials'); } else { res.setHeader('Access-Control-Allow-Origin', corsOrigin); } next(); }); // ─── Rate limiting ─────────────────────────────────────────────────────────── // Limite globale : 300 requetes par minute par IP (usage normal d'une SPA) app.use(rateLimit({ windowMs: 60 * 1000, max: Number.parseInt(process.env.RATE_LIMIT_MAX) || 300, standardHeaders: true, legacyHeaders: false, message: { error: 'Trop de requetes. Reessayez dans une minute.' }, })); // Limite sur le login : 10 tentatives par 15 min par IP // (separe de /auth/me et /auth/refresh qui sont des appels normaux) app.use('/auth/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 10, message: { error: 'Trop de tentatives de connexion. Reessayez dans 15 minutes.' }, })); // Limite sur l'inscription : 10 par IP par jour app.use('/auth/register', rateLimit({ windowMs: 24 * 60 * 60 * 1000, max: 10, message: { error: 'Trop de comptes crees depuis cette adresse. Reessayez demain.' }, })); // Limite sur forgot-password : 3 par IP par heure (anti-spam email) app.use('/auth/forgot-password', rateLimit({ windowMs: 60 * 60 * 1000, max: 3, message: { error: 'Trop de demandes de reinitialisation. Reessayez dans une heure.' }, })); // Limite sur reset-password : 5 par IP par heure (anti-bruteforce token) app.use('/auth/reset-password', rateLimit({ windowMs: 60 * 60 * 1000, max: 5, message: { error: 'Trop de tentatives. Reessayez dans une heure.' }, })); // Limite sur change-password : 5 par IP par heure app.use('/auth/change-password', rateLimit({ windowMs: 60 * 60 * 1000, max: 5, message: { error: 'Trop de tentatives de changement de mot de passe. Reessayez dans une heure.' }, })); // Limite sur change-email : 3 par IP par heure app.use('/auth/change-email', rateLimit({ windowMs: 60 * 60 * 1000, max: 3, message: { error: 'Trop de demandes de changement d\'email. Reessayez dans une heure.' }, })); // 2FA validation : 5 tentatives par 15 min (brute-force protection sur les codes TOTP) app.use('/auth/2fa/validate', rateLimit({ windowMs: 15 * 60 * 1000, max: 5, message: { error: 'Trop de tentatives 2FA. Reessayez dans 15 minutes.' }, })); app.use('/auth/2fa/backup', rateLimit({ windowMs: 15 * 60 * 1000, max: 5, message: { error: 'Trop de tentatives. Reessayez dans 15 minutes.' }, })); // ─── Stripe webhook (doit etre AVANT express.json pour recevoir le body brut) app.use('/billing/webhook', express.raw({ type: 'application/json' })); // ─── Parsers ───────────────────────────────────────────────────────────────── app.use(express.json({ limit: '1mb' })); app.use(cookieParser()); // ─── CSRF ──────────────────────────────────────────────────────────────────── // En cross-domain (frontend et API sur des sous-domaines differents), les cookies // CSRF ne sont pas partageables. La protection est assuree par : // 1. Cookies httpOnly avec SameSite=Lax → bloque les POST cross-origin // 2. CORS strict → seul le domaine du frontend peut appeler l'API // 3. Auth JWT requise sur toutes les routes sensibles // Le CSRF token reste disponible pour une utilisation future (same-domain). // ─── Audit log (intercepte les actions mutantes POST/PATCH/PUT/DELETE) ─────── app.use(auditLog); // ─── Routes ────────────────────────────────────────────────────────────────── app.use('/auth', authRoutes); app.use('/auth/2fa', twofaRoutes); app.use('/ssh-keys', sshKeysRoutes); app.use('/servers', serversRoutes); app.use('/api-keys', apiKeysRoutes); app.use('/user-api-keys', userApiKeysRoutes); app.use('/chat', chatRoutes); // Routes imbriquées sous /servers/:serverId app.use('/servers/:serverId/dashboard', dashboardRoutes); app.use('/servers/:serverId/docker', dockerRoutes); app.use('/servers/:serverId/logs', logsRoutes); app.use('/servers/:serverId/quick-actions', quickActionsRoutes); app.use('/servers/:serverId/metrics', metricsRoutes); app.use('/servers/:serverId/annotations', annotationsRoutes); app.use('/servers/:serverId/backups', backupsRoutes); app.use('/servers/:serverId/files', filesRoutes); app.use('/servers/:serverId/cron', cronRoutes); app.use('/alerts', alertsRoutes); app.use('/uptime', uptimeRoutes); app.use('/webhooks', webhooksRoutes); app.use('/ssl', sslRoutes); app.use('/status-pages', statusPageRoutes); app.use('/audit', auditRoutes); app.use('/config', configExportRoutes); app.use('/billing', billingRoutes); app.use('/contact', contactRoutes); app.use('/groups', groupsRoutes); app.use('/digest', digestRoutes); app.use('/report', reportRoutes); // ─── API publique REST v1 ──────────────────────────────────────────────────── app.use('/api/v1', apiV1Routes); // ─── Documentation API (Swagger UI via CDN) ───────────────────────────────── const swaggerSpec = require('./swagger.json'); app.get('/api/docs/json', (_req, res) => { res.json(swaggerSpec); }); app.get('/api/docs', (_req, res) => { res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data: https:; font-src https://cdn.jsdelivr.net"); res.send(`<!DOCTYPE html> <html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <title>IliaCloud API — Documentation</title> <link rel="icon" href="https://iliacloud.com/favicon.ico"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css"> <style> *{margin:0;box-sizing:border-box} body{background:#f8fafc;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif} .api-header{background:#fff;border-bottom:1px solid #e2e8f0;padding:14px 24px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:10} .api-header h1{font-size:18px;font-weight:700;color:#0f172a} .api-header h1 span{color:#6366f1} .api-header nav{display:flex;gap:16px} .api-header a{color:#6366f1;text-decoration:none;font-size:13px;font-weight:500} .api-header a:hover{text-decoration:underline} .swagger-ui .topbar{display:none} .swagger-ui{background:#f8fafc} .swagger-ui .wrapper{max-width:960px;padding:24px} .swagger-ui .info{margin:16px 0 24px} .swagger-ui .info .title{font-size:22px;color:#0f172a} .swagger-ui .info a{color:#6366f1} .swagger-ui .scheme-container{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:12px;box-shadow:none} .swagger-ui .opblock-tag{color:#0f172a !important;border-bottom:1px solid #e2e8f0;font-size:15px} .swagger-ui .opblock{border-radius:8px;margin-bottom:8px;box-shadow:0 1px 3px rgba(0,0,0,0.04)} .swagger-ui .opblock .opblock-summary{padding:10px 16px} .swagger-ui .opblock .opblock-summary-method{border-radius:6px;font-size:12px;font-weight:700;min-width:70px;padding:6px 0;text-align:center} .swagger-ui .opblock.opblock-get .opblock-summary-method{background:#22c55e} .swagger-ui .opblock.opblock-post .opblock-summary-method{background:#3b82f6} .swagger-ui .opblock.opblock-patch .opblock-summary-method{background:#f59e0b} .swagger-ui .opblock.opblock-delete .opblock-summary-method{background:#ef4444} .swagger-ui .opblock-body pre{border-radius:6px;font-size:13px} .swagger-ui .btn{border-radius:6px;font-size:13px;font-weight:600} .swagger-ui .btn.execute{background:#6366f1;border:none;color:#fff} .swagger-ui .btn.execute:hover{background:#4f46e5} .swagger-ui .dialog-ux .modal-ux{border-radius:12px} .swagger-ui section.models{border:1px solid #e2e8f0;border-radius:8px} </style> </head><body> <div class="api-header"> <h1><span>Ilia</span>Cloud API</h1> <nav> <a href="https://iliacloud.com">Site</a> <a href="https://app.iliacloud.com">Dashboard</a> <a href="https://app.iliacloud.com/register">Creer un compte</a> </nav> </div> <div id="swagger-ui"></div> <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script> <script>SwaggerUIBundle({url:'/api/docs/json',dom_id:'#swagger-ui',deepLinking:true,presets:[SwaggerUIBundle.presets.apis,SwaggerUIBundle.SwaggerUIStandalonePreset],layout:'BaseLayout',defaultModelsExpandDepth:0,docExpansion:'list'})</script> </body></html>`); }); // ─── Health check ──────────────────────────────────────────────────────────── // Utilisé par Docker et les load balancers pour vérifier que le service est up. app.get('/health', (_req, res) => { res.json({ status: 'ok', ts: new Date().toISOString() }); }); // ─── 404 ───────────────────────────────────────────────────────────────────── app.use((_req, res) => { res.status(404).json({ error: 'Route introuvable.' }); }); // ─── Gestion centralisée des erreurs ───────────────────────────────────────── app.use(errorHandler); // ─── Démarrage du serveur ──────────────────────────────────────────────────── const PORT = Number.parseInt(process.env.PORT) || 3000; const httpServer = http.createServer(app); // WebSocket pour les métriques dashboard en temps réel createWebSocketServer(httpServer); // Collecteur de métriques (sauvegarde toutes les 5 min en base) startCollector(); // Monitoring uptime (ping des URLs toutes les 60s) startUptimeChecker(); // Backups automatiques planifiés (vérification toutes les 15 min) startBackupScheduler(); // Monitoring SSL (vérification toutes les heures) startSslChecker(); // Surveillance des erreurs HTTP dans les access logs (toutes les 60s) startHttpErrorWatcher(); // Digest email periodique (verification toutes les minutes) startDigestScheduler(); // Rapport mensuel PDF (verification toutes les heures) startReportScheduler(); // Migration automatique puis demarrage du serveur (async () => { // NOSONAR — top-level await indisponible en CommonJS await runMigrations(); httpServer.listen(PORT, () => { console.log(`[server] IliaCloud backend démarré sur le port ${PORT}`); console.log(`[server] Environnement : ${process.env.NODE_ENV || 'development'}`); }); })(); // ─── Arrêt propre ──────────────────────────────────────────────────────────── // Ferme les connexions en cours avant d'arrêter le processus process.on('SIGTERM', () => { console.log('[server] SIGTERM reçu — arrêt propre...'); httpServer.close(() => process.exit(0)); }); process.on('SIGINT', () => { httpServer.close(() => process.exit(0)); }); |