// services/app/src/index.js // ------------------------------------------------------------ // SuiteCoffee — Servicio APP (UI + APIs negocio) // - ESM (Node >=18) // - Vistas EJS en ./views (dashboard.ejs, comandas.ejs, etc.) // - Sesión compartida con AUTH (cookie: sc.sid, Redis) // - Monta routes.legacy.js con requireAuth + withTenant // ------------------------------------------------------------ import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import morgan from 'morgan'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import session from 'express-session'; import expressLayouts from 'express-ejs-layouts'; // import RedisStore from "connect-redis"; import { createClient } from 'redis'; import { Pool } from 'pg'; // ----------------------------------------------------------------------------- // Utilidades base // ----------------------------------------------------------------------------- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const qi = (ident) => `"${String(ident).replace(/"/g, '""')}"`; const CLEAN_HEX = (s) => (String(s || '').toLowerCase().replace(/[^0-9a-f]/g, '') || null); const REQUIRED = (...keys) => { const miss = keys.filter((k) => !process.env[k]); if (miss.length) { console.warn(`⚠ Faltan variables de entorno: ${miss.join(', ')}`); } }; // ----------------------------------------------------------------------------- // Validación de entorno mínimo (ajusta nombres si difieren) // ----------------------------------------------------------------------------- REQUIRED( // Sesión 'SESSION_SECRET', 'REDIS_URL', // DB principal 'DB_HOST', 'DB_NAME', 'DB_USER', 'DB_PASS', // DB de tenants 'TENANTS_HOST', 'TENANTS_DB', 'TENANTS_USER', 'TENANTS_PASS' ); // ----------------------------------------------------------------------------- // Pools de PostgreSQL // ----------------------------------------------------------------------------- const mainPool = new Pool({ host: process.env.DB_HOST, port: Number(process.env.DB_PORT || 5432), database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASS, max: 10, idleTimeoutMillis: 30_000, }); const tenantsPool = new Pool({ host: process.env.TENANTS_HOST, port: Number(process.env.TENANTS_PORT || 5432), database: process.env.TENANTS_DB, user: process.env.TENANTS_USER, password: process.env.TENANTS_PASS, max: 10, idleTimeoutMillis: 30_000, }); // Autotest (no rompe si falla; sólo loguea) (async () => { try { const c = await mainPool.connect(); const r = await c.query('SELECT NOW() now'); console.log('[APP] DB principal OK. Hora:', r.rows[0].now); c.release(); } catch (e) { console.error('[APP] Error al conectar DB principal:', e.message); } try { const c = await tenantsPool.connect(); const r = await c.query('SELECT NOW() now'); console.log('[APP] DB tenants OK. Hora:', r.rows[0].now); c.release(); } catch (e) { console.error('[APP] Error al conectar DB tenants:', e.message); } })(); // ----------------------------------------------------------------------------- // Express + EJS // ----------------------------------------------------------------------------- const app = express(); app.set('trust proxy', Number(process.env.TRUST_PROXY_HOPS || 2)); app.use(cors({ origin: true, credentials: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, 'public'))); // ---------------------------------------------------------- // Motor de vistas EJS // ---------------------------------------------------------- // Views EJS en ./views app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.use(expressLayouts); app.set("layout", "layouts/main"); // Estáticos (si tenés carpeta public/, assets, etc.) app.use('/public', express.static(path.join(__dirname, 'public'))); // Middlewares básicos app.use(morgan('dev')); // ---------------------------------------------------------- // Middleware para datos globales // ---------------------------------------------------------- app.use((req, res, next) => { res.locals.currentPath = req.path; res.locals.pageTitle = "SuiteCoffee"; res.locals.pageId = ""; next(); }); // ---------------------------------------------------------- // Rutas de UI // ---------------------------------------------------------- app.get("/", (req, res) => { res.locals.pageTitle = "Inicio"; res.locals.pageId = "inicio"; res.render("inicio"); }); // ----------------------------------------------------------------------------- // Sesión (Redis) — misma cookie que AUTH // ----------------------------------------------------------------------------- const SESSION_COOKIE_NAME = process.env.SESSION_COOKIE_NAME || "sc.sid"; const SESSION_SECRET = process.env.SESSION_SECRET || "pon-una-clave-larga-y-unica"; const REDIS_URL = process.env.REDIS_URL || "redis://authentik-redis:6379"; // 1) Redis client const redis = createClient({ url: REDIS_URL /*, legacyMode: true */ }); redis.on("error", (err) => console.error("[Redis] Client Error:", err)); await redis.connect(); console.log("[Redis] connected"); // 2) Resolver RedisStore (soporta: // - v5: factory CJS -> connectRedis(session) // - v6/v7: export { RedisStore } ó export default class RedisStore) async function resolveRedisStore(session) { const mod = await import("connect-redis"); // ESM/CJS agnóstico // named export (v6/v7) if (typeof mod.RedisStore === "function") return mod.RedisStore; // default export (class ó factory) if (typeof mod.default === "function") { // ¿es clase neweable? if (mod.default.prototype && (mod.default.prototype.get || mod.default.prototype.set)) { return mod.default; // class RedisStore } // si no, asumimos factory antigua const Store = mod.default(session); // connectRedis(session) if (typeof Store === "function") return Store; // class devuelta por factory } // algunos builds CJS exponen la factory bajo mod (poco común) if (typeof mod === "function") { const Store = mod(session); if (typeof Store === "function") return Store; } throw new Error("connect-redis: no se pudo resolver RedisStore (API desconocida)."); } const RedisStore = await resolveRedisStore(session); // 3) Session middleware app.use(session({ name: SESSION_COOKIE_NAME, secret: SESSION_SECRET, resave: false, saveUninitialized: false, store: new RedisStore({ client: redis, prefix: "sc:", // opcional }), proxy: true, cookie: { secure: "auto", httpOnly: true, sameSite: "lax", path: "/", // ¡crítico! visible en / y /auth/* }, })); // ----------------------------------------------------------------------------- // Middlewares de Auth/Tenant para routes.legacy.js // ----------------------------------------------------------------------------- function requireAuth(req, res, next) { if (!req.session?.user) return res.redirect(303, "/auth/login"); next(); } // Abre un client al DB de tenants y fija search_path al esquema del usuario async function withTenant(req, res, next) { try { const hex = CLEAN_HEX(req.session?.user?.tenant_uuid); if (!hex) return res.status(400).json({ error: 'tenant-missing' }); const schema = `schema_tenant_${hex}`; const client = await tenantsPool.connect(); // Fijar search_path para que las consultas apunten al esquema del tenant await client.query(`SET SESSION search_path TO ${qi(schema)}, public`); // Hacemos el client accesible para los handlers de routes.legacy.js req.pg = client; // Liberar el client al finalizar la respuesta const release = () => { try { client.release(); } catch {} }; res.on('finish', release); res.on('close', release); next(); } catch (e) { next(e); } } // No-op (compatibilidad con el archivo legacy si lo pasa al final) function done(_req, _res, next) { return next && next(); } // ----------------------------------------------------------------------------- // Home / Landing // ----------------------------------------------------------------------------- // app.get('/', (req, res) => { // if (req.session?.user) return res.redirect(303, "/inicio"); // return res.redirect(303, "/auth/login"); // }); // Página de login app.get("/auth/login", (_req, res) => { return res.render("login", { pageTitle: "Iniciar sesión" }); }); app.get('/', (_req, res) => { return res.render("inicio", { pageTitle: "Bienvenido" }); }); app.get("/", (_req, res) => res.redirect(303, "/auth/login")); app.use([ "/dashboard", "/comandas", "/estadoComandas", "/productos", "/usuarios", "/reportes", "/compras", ], requireAuth); // Página para definir contraseña (el form envía al servicio AUTH) app.get('/set-password', (req, res) => { const pp = req.session?.pendingPassword; if (!pp) return req.session?.user ? res.redirect('/comandas') : res.redirect('/auth/login'); res.type('html').send(`