const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const morgan = require('morgan'); const env = require('./config/env'); const routes = require('./routes'); const { errorHandler, notFoundHandler } = require('./middleware/errorHandler'); const { generalLimiter } = require('./middleware/rateLimiter'); const { scheduleFonturReminders } = require('./jobs/fonturReminders.job'); const { scheduleCheckinReminders } = require('./jobs/checkinReminders.job'); const { scheduleRenewalReminders } = require('./jobs/renewalReminders.job'); const logger = require('./utils/logger'); const app = express(); // Confiamos en el primer proxy (Netlify/Render/Railway/etc suelen // poner uno delante) para que req.ip funcione bien con el rate limiter. app.set('trust proxy', 1); // --- Seguridad de cabeceras HTTP --- app.use(helmet()); // --- CORS: solo dominios permitidos, no "*" --- app.use( cors({ origin(origin, callback) { // Allow requests with no Origin header (curl, Postman, server-to-server). if (!origin) return callback(null, true); // Allow file:// pages (Origin: "null") ONLY in development, so opening // an HTML file directly by double-clicking it can still reach the API // while testing locally. In production this branch never runs, since // NODE_ENV will be "production" and the real site will have a real // https:// origin anyway. if (env.nodeEnv === 'development' && origin === 'null') return callback(null, true); if (env.allowedOrigins.includes(origin)) return callback(null, true); return callback(new Error(`Origen no permitido por CORS: ${origin}`)); }, credentials: true, }) ); app.use(express.json({ limit: '2mb' })); app.use(morgan(env.nodeEnv === 'development' ? 'dev' : 'combined')); app.use(generalLimiter); app.get('/health', (req, res) => { res.json({ status: 'ok', env: env.nodeEnv, timestamp: new Date().toISOString() }); }); app.use('/api', routes); app.use(notFoundHandler); app.use(errorHandler); const server = app.listen(env.port, () => { logger.info(`SyncTRA backend corriendo en http://localhost:${env.port} (${env.nodeEnv})`); // NOTA sobre escalar a mas de una instancia: si en el futuro corres // 2+ instancias del backend al mismo tiempo (para alta disponibilidad), // este cron correria duplicado en cada una. El campo reminder_sent_at // mitiga el duplicado pero no lo elimina 100% (hay una ventana de // condicion de carrera). Cuando llegues a ese punto, mueve este job // a un proceso separado (ej. un "worker" dedicado, o un cron job de // la plataforma de hosting) en vez de dentro del servidor web. scheduleFonturReminders(); scheduleCheckinReminders(); scheduleRenewalReminders(); }); /** * Apagado ordenado ("graceful shutdown"). * * Cuando la plataforma de hosting despliega una version nueva, primero * le manda SIGTERM a la version vieja para avisarle "te vas a apagar". * Sin este manejo, Node mata el proceso de inmediato y cualquier * request en curso (ej. alguien pagando con PSE justo en ese segundo) * recibe un error de conexion cortada. * * Con esto: dejamos de aceptar conexiones NUEVAS, pero terminamos las * que ya estaban en curso antes de cerrar. Esto es lo que hace posible * un despliegue sin downtime cuando se combina con un hosting que * espera a que el health check de la instancia nueva responda antes * de apagar la vieja (Render, Railway, Fly.io lo hacen asi). */ function shutdown(signal) { logger.info(`Recibido ${signal}, cerrando el servidor de forma ordenada...`); server.close(() => { logger.info('Servidor cerrado. No quedan conexiones activas.'); process.exit(0); }); // Salvavidas: si algo se queda colgado, no dejamos el proceso zombie // para siempre. 10 segundos suele ser suficiente para requests normales; // si tienes operaciones largas (ej. generar un reporte grande), sube este numero. setTimeout(() => { logger.error('No se pudo cerrar de forma ordenada en 10s, forzando salida.'); process.exit(1); }, 10_000).unref(); } process.on('SIGTERM', () => shutdown('SIGTERM')); process.on('SIGINT', () => shutdown('SIGINT')); // Red de seguridad: si algo revienta de forma no controlada (un bug real), // preferimos loguearlo y morir limpio para que el hosting reinicie el // proceso automaticamente, en vez de quedar en un estado corrupto. process.on('unhandledRejection', (reason) => { logger.error('Promesa rechazada sin manejar:', reason); }); process.on('uncaughtException', (err) => { logger.error('Excepcion no capturada:', err); process.exit(1); });