Carga completa
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# ===== Runtime =====
|
||||
NODE_ENV=development
|
||||
PORT=3030
|
||||
APP_LOCAL_PORT=3030
|
||||
|
||||
# ===== Session (usa el Redis del stack) =====
|
||||
# Para DEV podemos reutilizar el Redis de Authentik. En prod conviene uno separado.
|
||||
SESSION_SECRET=pon-una-clave-larga-y-unica
|
||||
REDIS_URL=redis://authentik-redis:6379
|
||||
|
||||
# ===== DB principal (metadatos de SuiteCoffee) =====
|
||||
DB_HOST=dev-tenants
|
||||
DB_PORT=5432
|
||||
DB_NAME=dev-postgres
|
||||
DB_USER=dev-user-postgres
|
||||
DB_PASS=dev-pass-postgres
|
||||
|
||||
# ===== DB tenants (Tenants de SuiteCoffee) =====
|
||||
TENANTS_HOST=dev-tenants
|
||||
TENANTS_DB=dev-postgres
|
||||
TENANTS_USER=dev-user-postgres
|
||||
TENANTS_PASS=dev-pass-postgres
|
||||
TENANTS_PORT=5432
|
||||
|
||||
# ===== (Opcional) Colores UI, si alguna vista los lee =====
|
||||
COL_PRI=452D19 # Marrón oscuro
|
||||
COL_SEC=D7A666 # Crema / Café
|
||||
COL_BG=FFA500 # Naranja
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
NODE_ENV=production # Entorno de desarrollo
|
||||
|
||||
PORT=3000 # Variables del servicio -> suitecoffee-app
|
||||
|
||||
# Variables del servicio -> suitecoffee-db de suitecoffee-app
|
||||
|
||||
DB_HOST=prod-tenants
|
||||
# Nombre de la base de datos
|
||||
DB_NAME=postgres
|
||||
|
||||
# Usuario y contraseña
|
||||
DB_USER=postgres
|
||||
DB_PASS=postgres
|
||||
|
||||
# Puertos del servicio de db
|
||||
DB_LOCAL_PORT=5432
|
||||
DB_DOCKER_PORT=5432
|
||||
|
||||
# Colores personalizados
|
||||
COL_PRI=452D19 # Marrón oscuro
|
||||
COL_SEC=D7A666 # Crema / Café
|
||||
COL_BG=FFA500 # Naranja
|
||||
Generated
+62
@@ -19,6 +19,7 @@
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"express-session": "^1.18.2",
|
||||
"ioredis": "^5.7.0",
|
||||
"morgan": "^1.10.1",
|
||||
"pg": "^8.16.3",
|
||||
"pg-format": "^1.0.4",
|
||||
"redis": "^5.8.2",
|
||||
@@ -131,6 +132,24 @@
|
||||
"version": "1.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/basic-auth": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
|
||||
"integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-auth/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
@@ -902,6 +921,49 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
"integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"basic-auth": "~2.0.1",
|
||||
"debug": "2.6.9",
|
||||
"depd": "~2.0.0",
|
||||
"on-finished": "~2.3.0",
|
||||
"on-headers": "~1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/morgan/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/morgan/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/morgan/node_modules/on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
"integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"license": "MIT"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"express-ejs-layouts": "^2.5.1",
|
||||
"express-session": "^1.18.2",
|
||||
"ioredis": "^5.7.0",
|
||||
"morgan": "^1.10.1",
|
||||
"pg": "^8.16.3",
|
||||
"pg-format": "^1.0.4",
|
||||
"redis": "^5.8.2",
|
||||
|
||||
+197
-154
@@ -1,216 +1,259 @@
|
||||
// services/app/src/index.js
|
||||
// -----------------------------------------------------------------------------
|
||||
// SuiteCoffee — Servicio APP (Express)
|
||||
// - Carga de entorno robusta (compatible con Docker Compose env_file)
|
||||
// - Sesiones compartidas via Redis (mismo cookie que AUTH)
|
||||
// - Middlewares: CORS, JSON, estáticos, EJS opcional
|
||||
// - Multitenant por esquema: requireAuth + withTenant + done
|
||||
// - Montaje automático de ENDPOINTS LEGACY sin perder nada
|
||||
// - Healthcheck, 404 y manejador de errores
|
||||
// - Conserva y expone pools para que tus endpoints los usen
|
||||
// -----------------------------------------------------------------------------
|
||||
// ------------------------------------------------------------
|
||||
// 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
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// === 0) CARGA DE ENTORNO ROBUSTA (no pisa variables ya definidas por Compose)
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import dotenv from 'dotenv';
|
||||
import chalk from 'chalk';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const ENV = (process.env.NODE_ENV || 'development').toLowerCase();
|
||||
const envMap = { development: '.env.development', stage: '.env.test', production: '.env.production' };
|
||||
const envFile = envMap[ENV] || '.env.development';
|
||||
const candidates = [
|
||||
path.resolve(process.cwd(), envFile), // /app/.env.development (dentro del contenedor)
|
||||
path.resolve(__dirname, '..', envFile), // por si queda un nivel arriba
|
||||
path.resolve(__dirname, envFile), // por si lo ponen junto al src
|
||||
];
|
||||
const found = candidates.find((p) => fs.existsSync(p));
|
||||
if (found) {
|
||||
dotenv.config({ path: found, override: false });
|
||||
console.log(`Activando entorno de -> ${ENV.toUpperCase()} ${chalk.gray(`(${found})`)}`);
|
||||
} else {
|
||||
console.log(`Activando entorno de -> ${ENV.toUpperCase()} (sin archivo .env; usando variables del proceso)`);
|
||||
}
|
||||
|
||||
// === 1) IMPORTS PRINCIPALES
|
||||
import 'dotenv/config';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import expressLayouts from 'express-ejs-layouts';
|
||||
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 { createClient as createRedisClient } from 'redis';
|
||||
import * as connectRedis from 'connect-redis';
|
||||
import { Pool } from 'pg';
|
||||
import bcrypt from 'bcrypt'; // <- lo conservamos si ya lo usabas
|
||||
import crypto from 'node:crypto'; // <- idem
|
||||
|
||||
// Tolerante a cambios de export en connect-redis
|
||||
// -----------------------------------------------------------------------------
|
||||
// Utilidades base
|
||||
// -----------------------------------------------------------------------------
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const RedisStore = connectRedis.default || connectRedis.RedisStore;
|
||||
|
||||
// === 2) APP y CONFIG BASICA
|
||||
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', 1);
|
||||
app.use(cors({ origin: true, credentials: true }));
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Vistas EJS (si no usás vistas, puedes dejarlo; no rompe)
|
||||
// Views EJS en ./views
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
app.use(expressLayouts);
|
||||
app.set('layout', 'layout');
|
||||
app.set("layout", "layouts/main");
|
||||
|
||||
// Estáticos opcionales (ajusta si tu estructura difiere)
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
// Estáticos (si tenés carpeta public/, assets, etc.)
|
||||
app.use('/public', express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// === 3) SESIONES COMPARTIDAS (mismo cookie que AUTH)
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET || 'change-me-in-dev';
|
||||
const REDIS_URL = process.env.REDIS_URL || 'redis://authentik-redis:6379';
|
||||
// Middlewares básicos
|
||||
app.use(morgan('dev'));
|
||||
app.use(cors({ origin: true, credentials: true }));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
const redis = createRedisClient({ url: REDIS_URL });
|
||||
await redis.connect().catch((e) => {
|
||||
console.warn('⚠ No se pudo conectar a Redis de sesiones:', e?.message || e);
|
||||
// ----------------------------------------------------------
|
||||
// Middleware para datos globales
|
||||
// ----------------------------------------------------------
|
||||
app.use((req, res, next) => {
|
||||
res.locals.currentPath = req.path;
|
||||
res.locals.pageTitle = "SuiteCoffee";
|
||||
res.locals.pageId = "";
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Sesión (Redis) — misma cookie que AUTH
|
||||
// -----------------------------------------------------------------------------
|
||||
const SESSION_COOKIE_NAME = 'sc.sid';
|
||||
const redis = createRedisClient({ url: process.env.REDIS_URL });
|
||||
await redis.connect().catch((e) => console.error('[APP] Redis session error:', e.message));
|
||||
|
||||
app.use(
|
||||
session({
|
||||
name: 'sc.sid', // <- igual que en AUTH
|
||||
name: SESSION_COOKIE_NAME,
|
||||
store: new RedisStore({ client: redis, prefix: 'sess:' }),
|
||||
secret: SESSION_SECRET,
|
||||
secret: process.env.SESSION_SECRET || 'change-me',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
// domain: 'suitecoffee.mateosaldain.uy', // (opcional) si lo necesitás
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Exponer el usuario a las vistas (no tocar req.session)
|
||||
// Exponer usuario a las vistas
|
||||
app.use((req, res, next) => {
|
||||
res.locals.user = req.session?.user || null;
|
||||
next();
|
||||
});
|
||||
|
||||
// === 4) POOLS A BASES DE DATOS ===
|
||||
// 4.1) Base principal (si la usás en APP). Conservamos variables usadas en el repo.
|
||||
const DB = {
|
||||
host: process.env.DB_HOST || 'dev-db',
|
||||
port: Number(process.env.DB_PORT || 5432),
|
||||
user: process.env.DB_USER || 'dev-user-suitecoffee',
|
||||
password: process.env.DB_PASS || 'dev-pass-suitecoffee',
|
||||
database: process.env.DB_NAME || 'dev-suitecoffee',
|
||||
};
|
||||
export const mainPool = new Pool({ ...DB, max: 10, idleTimeoutMillis: 30_000 });
|
||||
|
||||
async function verificarConexion() {
|
||||
try {
|
||||
const c = await mainPool.connect();
|
||||
const { rows } = await c.query('SELECT NOW() AS now');
|
||||
console.log(`DB principal OK @ ${rows[0].now}`);
|
||||
c.release();
|
||||
} catch (e) {
|
||||
console.warn('⚠ No se pudo verificar DB principal:', e?.message || e);
|
||||
// -----------------------------------------------------------------------------
|
||||
// Middlewares de Auth/Tenant para routes.legacy.js
|
||||
// -----------------------------------------------------------------------------
|
||||
function requireAuth(req, res, next) {
|
||||
if (!req.session?.user) {
|
||||
// Si querés devolver 401 en lugar de redirigir, cambia esta línea
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// 4.2) Base multi-tenant (un solo DB con esquemas por tenant)
|
||||
const TENANTS = {
|
||||
host: process.env.TENANTS_HOST || 'dev-tenants',
|
||||
port: Number(process.env.TENANTS_PORT || 5432),
|
||||
user: process.env.TENANTS_USER || 'postgres',
|
||||
password: process.env.TENANTS_PASS || 'postgres',
|
||||
database: process.env.TENANTS_DB || 'dev-postgres',
|
||||
};
|
||||
export const tenantsPool = new Pool({ ...TENANTS, max: 20, idleTimeoutMillis: 30_000 });
|
||||
|
||||
// === 5) MIDDLEWARES DE AUTENTICACIÓN Y TENANT ===
|
||||
export function requireAuth(req, res, next) {
|
||||
if (req.session?.user) return next();
|
||||
// Fallback DEV: permitir si el front envía explícitamente el tenant (para pruebas)
|
||||
if (req.get('x-tenant-uuid')) return next();
|
||||
return res.status(401).json({ error: 'no-auth' });
|
||||
}
|
||||
|
||||
function getTenantUuid(req) {
|
||||
const h = req.get('x-tenant-uuid');
|
||||
if (h) return String(h).replace(/-/g, '');
|
||||
const s = req.session?.user?.tenant_uuid;
|
||||
if (s) return String(s).replace(/-/g, '');
|
||||
throw new Error('Tenant no especificado');
|
||||
}
|
||||
|
||||
export async function withTenant(req, res, next) {
|
||||
const client = await tenantsPool.connect();
|
||||
// Abre un client al DB de tenants y fija search_path al esquema del usuario
|
||||
async function withTenant(req, res, next) {
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const uuid = getTenantUuid(req);
|
||||
const schema = `schema_tenant_${uuid}`;
|
||||
const hex = CLEAN_HEX(req.session?.user?.tenant_uuid);
|
||||
if (!hex) return res.status(400).json({ error: 'tenant-missing' });
|
||||
|
||||
// Si creaste la función en DB: SELECT public.f_set_search_path($1)
|
||||
// await client.query('SELECT public.f_set_search_path($1)', [schema]);
|
||||
await client.query(`SET LOCAL search_path TO ${schema.replace(/"/g, '')}`);
|
||||
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;
|
||||
req.pgSchema = schema;
|
||||
|
||||
// Liberar el client al finalizar la respuesta
|
||||
const release = () => {
|
||||
try { client.release(); } catch {}
|
||||
};
|
||||
res.on('finish', release);
|
||||
res.on('close', release);
|
||||
|
||||
next();
|
||||
} catch (e) {
|
||||
try { await client.query('ROLLBACK'); } catch {}
|
||||
client.release();
|
||||
return res.status(400).json({ error: e.message });
|
||||
next(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function done(req, res, next) {
|
||||
try {
|
||||
if (req.pg) await req.pg.query('COMMIT');
|
||||
} catch (e) {
|
||||
try { if (req.pg) await req.pg.query('ROLLBACK'); } catch {}
|
||||
} finally {
|
||||
if (req.pg) req.pg.release();
|
||||
// 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('/comandas'); // ya logueado → dashboard
|
||||
}
|
||||
next?.();
|
||||
}
|
||||
return res.render('login', { pageTitle: 'Iniciar sesión' });
|
||||
});
|
||||
|
||||
// === 6) RUTAS BÁSICAS / HEALTH ===
|
||||
app.get('/health', (_req, res) => res.status(200).json({ status: 'ok' }));
|
||||
app.get('/api/health', (_req, res) => res.status(200).json({ status: 'ok' }));
|
||||
// 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');
|
||||
|
||||
// === 7) MONTAJE AUTOMÁTICO DE ENDPOINTS LEGACY ===
|
||||
// Para NO PERDER NADA de tu archivo original:
|
||||
// 1) Crea services/app/src/routes.legacy.js y exporta por defecto una función:
|
||||
// export default function mount(app, ctx) { /* pega aquí TODOS tus app.get/post/... */ }
|
||||
// // ctx trae: { requireAuth, withTenant, done, mainPool, tenantsPool, express }
|
||||
// 2) O exporta un Router en routes.legacy.js como: export const router = Router();
|
||||
// 3) Este bloque intentará montarlo si existe.
|
||||
try {
|
||||
const legacy = await import('./routes.legacy.js');
|
||||
if (legacy?.default) {
|
||||
legacy.default(app, { requireAuth, withTenant, done, mainPool, tenantsPool, express });
|
||||
console.log('✔ Endpoints legacy montados (función default)');
|
||||
} else if (legacy?.router) {
|
||||
app.use(legacy.router);
|
||||
console.log('✔ Endpoints legacy montados (router)');
|
||||
}
|
||||
} catch {
|
||||
console.log('ℹ No se encontró routes.legacy.js; continúa sólo con las rutas nuevas');
|
||||
}
|
||||
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,
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Health + 404 + errores
|
||||
// -----------------------------------------------------------------------------
|
||||
app.get('/health', (_req, res) => res.status(200).json({ status: 'ok', service: 'app' }));
|
||||
|
||||
// === 8) 404 + MANEJO DE ERRORES ===
|
||||
app.use((req, res) => res.status(404).json({ error: 'not-found', path: req.originalUrl }));
|
||||
|
||||
app.use((err, _req, res, _next) => {
|
||||
console.error('❌ Error APP:', err);
|
||||
console.error('[APP] Error:', err);
|
||||
if (res.headersSent) return;
|
||||
res.status(500).json({ error: 'internal-error', detail: err?.message || String(err) });
|
||||
});
|
||||
|
||||
// === 9) ARRANQUE ===
|
||||
const PORT = Number(process.env.APP_LOCAL_PORT || process.env.PORT || 4000);
|
||||
(async () => {
|
||||
console.log(`Entorno -> ${ENV.toUpperCase()} | Puerto -> ${PORT}`);
|
||||
await verificarConexion();
|
||||
app.listen(PORT, () => console.log(`SuiteCoffee APP escuchando en ${chalk.yellow(`http://localhost:${PORT}`)}`));
|
||||
})();
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title><%= typeof pageTitle !== 'undefined' ? pageTitle : 'Iniciar sesión · SuiteCoffee' %></title>
|
||||
|
||||
<!-- Bootstrap 5 (minimal) -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--col-pri: #<%= (process.env.COL_PRI || '452D19') %>;
|
||||
--col-sec: #<%= (process.env.COL_SEC || 'D7A666') %>;
|
||||
--col-bg: #<%= (process.env.COL_BG || 'FFA500') %>33; /* con alpha */
|
||||
}
|
||||
body { background: radial-gradient(1200px 600px at 10% -10%, var(--col-bg), transparent), #f8f9fa; }
|
||||
.brand { color: var(--col-pri); }
|
||||
.btn-sso { background: var(--col-pri); color: #fff; border-color: var(--col-pri); }
|
||||
.btn-sso:hover { filter: brightness(1.05); color: #fff; }
|
||||
.card { border-radius: 14px; }
|
||||
.form-hint { font-size: .875rem; color: #6c757d; }
|
||||
.divider { display:flex; align-items:center; text-transform:uppercase; font-size:.8rem; color:#6c757d; }
|
||||
.divider::before, .divider::after { content:""; height:1px; background:#dee2e6; flex:1; }
|
||||
.divider:not(:empty)::before { margin-right:.75rem; }
|
||||
.divider:not(:empty)::after { margin-left:.75rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-sm-10 col-md-8 col-lg-6 col-xl-5">
|
||||
<div class="text-center mb-4">
|
||||
<h1 class="brand fw-bold">SuiteCoffee</h1>
|
||||
<p class="text-secondary mb-0">Accedé a tu cuenta</p>
|
||||
</div>
|
||||
|
||||
<!-- Mensajes (query ?msg= / ?error=) -->
|
||||
<div id="flash" class="mb-3" style="display:none"></div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
|
||||
<!-- SSO con Authentik -->
|
||||
<div class="d-grid gap-2 mb-3">
|
||||
<a href="/auth/login" class="btn btn-sso btn-lg" id="btn-sso">
|
||||
Ingresar con SSO (Authentik)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="divider my-3">o</div>
|
||||
|
||||
<!-- Registro mínimo (usa POST /api/users/register) -->
|
||||
<form id="form-register" class="needs-validation" novalidate>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" id="email" name="email" placeholder="tu@correo.com" required>
|
||||
<div class="invalid-feedback">Ingresá un email válido.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="display_name" class="form-label">Nombre a mostrar</label>
|
||||
<input type="text" class="form-control" id="display_name" name="display_name" placeholder="Ej.: Juan Pérez" required>
|
||||
<div class="invalid-feedback">Ingresá tu nombre.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="tenant_uuid" class="form-label">Código de organización (tenant UUID)</label>
|
||||
<input type="text" class="form-control" id="tenant_uuid" name="tenant_uuid" placeholder="Ej.: 4b8d0f6a-...">
|
||||
<div class="form-hint">Si te invitaron a una organización existente, pegá aquí su UUID. Si sos el primero de tu empresa, dejalo vacío y el equipo te asignará uno.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="role" class="form-label">Rol</label>
|
||||
<select id="role" name="role" class="form-select">
|
||||
<option value="owner">Owner</option>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="staff">Staff</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<button type="submit" class="btn btn-outline-dark">Crear cuenta</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="text-center text-muted mt-3 mb-0" style="font-size:.9rem;">
|
||||
Al continuar aceptás nuestros términos y políticas.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-secondary mt-3" style="font-size:.9rem;">
|
||||
¿Ya tenés cuenta? <a href="/auth/login" class="link-dark">Iniciá sesión con SSO</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Mostrar mensajes por querystring (?msg=... / ?error=...)
|
||||
(function() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const el = document.getElementById('flash');
|
||||
const msg = params.get('msg');
|
||||
const err = params.get('error');
|
||||
if (msg) {
|
||||
el.innerHTML = `<div class="alert alert-success mb-0" role="alert">${decodeURIComponent(msg)}</div>`;
|
||||
el.style.display = '';
|
||||
} else if (err) {
|
||||
el.innerHTML = `<div class="alert alert-danger mb-0" role="alert">${decodeURIComponent(err)}</div>`;
|
||||
el.style.display = '';
|
||||
}
|
||||
})();
|
||||
|
||||
// Validación Bootstrap + envío del registro contra /api/users/register
|
||||
(function() {
|
||||
const form = document.getElementById('form-register');
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
form.classList.add('was-validated');
|
||||
if (!form.checkValidity()) return;
|
||||
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
btn.disabled = true; btn.innerText = 'Creando...';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
email: document.getElementById('email').value.trim(),
|
||||
display_name: document.getElementById('display_name').value.trim(),
|
||||
tenant_uuid: document.getElementById('tenant_uuid').value.trim() || undefined,
|
||||
role: document.getElementById('role').value
|
||||
};
|
||||
|
||||
const res = await fetch('/api/users/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error(data?.error || data?.message || 'No se pudo registrar');
|
||||
}
|
||||
|
||||
// Registro OK → redirigimos a login SSO
|
||||
const redir = '/auth/login';
|
||||
location.href = redir + '?msg=' + encodeURIComponent('Registro exitoso. Iniciá sesión con SSO.');
|
||||
} catch (err) {
|
||||
alert(err.message || String(err));
|
||||
} finally {
|
||||
btn.disabled = false; btn.innerText = 'Crear cuenta';
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user