SuiteCoffee/app.index.js
msaldain 69f5860b7f Inclusión del dominio suitecoffee.uy al NPM.
Se ajustaron los problemas de renderizado y redirección mayores de https://suitecoffee.uy/
Se re-creó el archivo ~/SuiteCoffee/services/app/src/index.js para mantener un orden adecuado
Las rutas exigen una cookie de seción para cargarse, de o contrario redireccionan a  https://suitecoffee.uy/auth/login para iniciar o crear sesión de usuario, excepto https://suitecoffee.uy/inicio que se mantene de esta manera con motivos de desarrollo
2025-09-09 14:20:05 +00:00

344 lines
12 KiB
JavaScript

// 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(`
<!doctype html><meta charset="utf-8">
<title>SuiteCoffee · Definir contraseña</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<div class="container py-5" style="max-width:520px;">
<h2 class="mb-4">Definir contraseña</h2>
<form method="post" action="/auth/password/set" class="vstack gap-3">
<input class="form-control" type="password" name="password" placeholder="Nueva contraseña" minlength="8" required>
<input class="form-control" type="password" name="password2" placeholder="Repetí la contraseña" minlength="8" required>
<button class="btn btn-primary" type="submit">Guardar y continuar</button>
<small class="text-muted">Luego te redirigiremos a iniciar sesión por SSO.</small>
</form>
</div>
`);
});
// -----------------------------------------------------------------------------
// Montar rutas legacy (render de EJS y APIs de negocio)
// -----------------------------------------------------------------------------
const legacy = await import('./routes.legacy.js');
legacy.default(app, {
requireAuth,
withTenant,
done,
mainPool,
tenantsPool,
express,
});
// ----------------------------------------------------------
// Verificación de conexión
// ----------------------------------------------------------
async function verificarConexion() {
try {
const client = await pool.connect();
const res = await client.query('SELECT NOW() AS hora');
console.log(`Conexión con la base de datos ${process.env.DB_NAME} fue exitosa.`);
console.log('Fecha y hora actual de la base de datos:', res.rows[0].hora);
client.release();
} catch (error) {
console.error('Error al conectar con la base de datos al iniciar:', error.message);
console.error('Revisar credenciales y accesos de red.');
}
}
// -----------------------------------------------------------------------------
// Health + 404 + errores
// -----------------------------------------------------------------------------
app.get('/health', (_req, res) => res.status(200).json({ status: 'ok', service: 'app' }));
app.use((req, res) => res.status(404).json({ error: 'not-found', path: req.originalUrl }));
app.use((err, _req, res, _next) => {
console.error('[APP] Error:', err);
if (res.headersSent) return;
res.status(500).json({ error: 'internal-error', detail: err?.message || String(err) });
});
// -----------------------------------------------------------------------------
// Arranque
// -----------------------------------------------------------------------------
const PORT = Number(process.env.PORT || process.env.APP_LOCAL_PORT || 3030);
// app.listen(PORT, () => {
// console.log(`[APP] SuiteCoffee corriendo en http://localhost:${PORT}`);
// });
(async () => {
await verificarConexion();
app.listen(PORT, () => {
console.log(`[APP] SuiteCoffee corriendo en http://localhost:${PORT}`);
});
})();