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
344 lines
12 KiB
JavaScript
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}`);
|
|
});
|
|
})(); |