From 09610df995dfbd10c53e897e8e98d86a3899fd84 Mon Sep 17 00:00:00 2001 From: msaldain Date: Mon, 25 Aug 2025 18:41:51 +0000 Subject: [PATCH] =?UTF-8?q?Conexi=C3=B3n=20satisfactoria=20con=20la=20base?= =?UTF-8?q?=20de=20datos=20creada=20para=20el=20workarround,=20las=20tabla?= =?UTF-8?q?s,=20columnas=20y=20filas=20se=20muestran=20en=20el=20bashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/manso/src/index.js | 589 +++++++++++------------- services/manso/src/pages/dashboard.html | 293 ++++++++++++ services/manso/src/pages/index.html | 154 ------- 3 files changed, 570 insertions(+), 466 deletions(-) create mode 100644 services/manso/src/pages/dashboard.html delete mode 100644 services/manso/src/pages/index.html diff --git a/services/manso/src/index.js b/services/manso/src/index.js index 280eef2..d428d8c 100644 --- a/services/manso/src/index.js +++ b/services/manso/src/index.js @@ -12,349 +12,314 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Variables de Entorno -import dotenv, { config } from 'dotenv'; +import dotenv from 'dotenv'; -// Obtención de la ruta de la variable de entorno correspondiente a NODE_ENV -try { - if (process.env.NODE_ENV === 'development') { - dotenv.config({ path: path.resolve(__dirname, '../.env.development' )}); - console.log(`Activando entorno de ->${chalk.green(` DEVELOPMENT `)}`); - } else if (process.env.NODE_ENV === 'stage') { - dotenv.config({ path: path.resolve(__dirname, '../.env.test' )}); - console.log(`Activando entorno de ->${chalk.yellow(` TESTING `)}`); - } else if (process.env.NODE_ENV === 'production') { - dotenv.config({ path: path.resolve(__dirname, '../.env.production' )}); - console.log(`Activando entorno de ->${chalk.red(` PRODUCTION `)}`); - } -} catch (error) { - console.log("A ocurrido un error al seleccionar el entorno. \nError: " + error); +// Cargar .env según entorno +if (process.env.NODE_ENV === 'development') { + dotenv.config({ path: path.resolve(__dirname, '../.env.development') }); +} else if (process.env.NODE_ENV === 'test') { + dotenv.config({ path: path.resolve(__dirname, '../.env.test') }); +} else if (process.env.NODE_ENV === 'production') { + dotenv.config({ path: path.resolve(__dirname, '../.env.production') }); +} else { + dotenv.config(); // .env por defecto } -// Renderiado +// ---------------------------------------------------------- +// App +// ---------------------------------------------------------- const app = express(); +app.set('trust proxy', true); app.use(cors()); app.use(express.json()); +app.use(express.json({ limit: '1mb' })); app.use(express.static(path.join(__dirname, 'pages'))); +// ---------------------------------------------------------- // Configuración de conexión PostgreSQL - +// ---------------------------------------------------------- const dbConfig = { host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_NAME, - port: process.env.DB_LOCAL_PORT + port: process.env.DB_LOCAL_PORT ? Number(process.env.DB_LOCAL_PORT) : undefined, + ssl: process.env.PGSSL === 'true' ? { rejectUnauthorized: false } : undefined, + max: 10 }; const pool = new Pool(dbConfig); +// Helper de consulta con acquire/release explícito (del código original, referencial) +// async function q(text, params) { +// const client = await pool.connect(); +// try { +// return await client.query(text, params); +// } finally { +// client.release(); +// } +// } +// ---------------------------------------------------------- +// Seguridad: Tablas permitidas +// ---------------------------------------------------------- +const ALLOWED_TABLES = [ + 'roles','usuarios','usua_roles', + 'categorias','productos', + 'clientes','mesas', + 'comandas','deta_comandas', + 'proveedores','compras','deta_comp_producto', + 'mate_primas','deta_comp_materias', + 'prov_producto','prov_mate_prima', + 'receta_producto' +]; + +const VALID_IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + +// Identificadores SQL -> comillas dobles y escape correcto +const q = (s) => `"${String(s).replace(/"/g, '""')}"`; + +function ensureTable(name) { + const t = String(name || '').toLowerCase(); + if (!ALLOWED_TABLES.includes(t)) throw new Error('Tabla no permitida'); + return t; +} + +async function getClient() { + const client = await pool.connect(); + return client; +} + +// ---------------------------------------------------------- +// Introspección de esquema +// ---------------------------------------------------------- +async function loadColumns(client, table) { + const sql = ` + SELECT + c.column_name, + c.data_type, + c.is_nullable = 'YES' AS is_nullable, + c.column_default, + (SELECT EXISTS ( + SELECT 1 FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + JOIN pg_index i ON i.indrelid = t.oid AND a.attnum = ANY(i.indkey) + WHERE t.relname = $1 AND i.indisprimary AND a.attname = c.column_name + )) AS is_primary, + (SELECT a.attgenerated = 's' OR a.attidentity IN ('a','d') + FROM pg_attribute a + JOIN pg_class t ON t.oid = a.attrelid + WHERE t.relname = $1 AND a.attname = c.column_name + ) AS is_identity + FROM information_schema.columns c + WHERE c.table_schema='public' AND c.table_name=$1 + ORDER BY c.ordinal_position + `; + const { rows } = await client.query(sql, [table]); + return rows; +} + +async function loadForeignKeys(client, table) { + const sql = ` + SELECT + kcu.column_name, + ccu.table_name AS foreign_table, + ccu.column_name AS foreign_column + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema + WHERE tc.table_schema='public' AND tc.table_name=$1 AND tc.constraint_type='FOREIGN KEY' + `; + const { rows } = await client.query(sql, [table]); + const map = {}; + for (const r of rows) map[r.column_name] = { foreign_table: r.foreign_table, foreign_column: r.foreign_column }; + return map; +} + +async function loadPrimaryKey(client, table) { + const sql = ` + SELECT a.attname AS column_name + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + JOIN pg_class t ON t.oid = i.indrelid + WHERE t.relname = $1 AND i.indisprimary + `; + const { rows } = await client.query(sql, [table]); + return rows.map(r => r.column_name); +} + +// label column for FK options +async function pickLabelColumn(client, refTable) { + const preferred = ['nombre','raz_social','apodo','documento','correo','telefono']; + const { rows } = await client.query( + `SELECT column_name, data_type + FROM information_schema.columns + WHERE table_schema='public' AND table_name=$1 + ORDER BY ordinal_position`, [refTable] + ); + for (const cand of preferred) { + if (rows.find(r => r.column_name === cand)) return cand; + } + const textish = rows.find(r => /text|character varying|varchar/i.test(r.data_type)); + if (textish) return textish.column_name; + return rows[0]?.column_name || 'id'; +} + +// ---------------------------------------------------------- +// Rutas de UI +// ---------------------------------------------------------- +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'pages', 'dashboard.html')); +}); + +// ---------------------------------------------------------- +// API +// ---------------------------------------------------------- +app.get('/api/tables', async (_req, res) => { + res.json(ALLOWED_TABLES); +}); + +app.get('/api/schema/:table', async (req, res) => { + try { + const table = ensureTable(req.params.table); + const client = await getClient(); + try { + const columns = await loadColumns(client, table); + const fks = await loadForeignKeys(client, table); + const enriched = columns.map(c => ({ ...c, foreign: fks[c.column_name] || null })); + res.json({ table, columns: enriched }); + } finally { client.release(); } + } catch (e) { + res.status(400).json({ error: e.message }); + } +}); + +app.get('/api/options/:table/:column', async (req, res) => { + try { + const table = ensureTable(req.params.table); + const column = req.params.column; + if (!VALID_IDENT.test(column)) throw new Error('Columna inválida'); + + const client = await getClient(); + try { + const fks = await loadForeignKeys(client, table); + const fk = fks[column]; + if (!fk) return res.json([]); + + const refTable = fk.foreign_table; + const refId = fk.foreign_column; + const labelCol = await pickLabelColumn(client, refTable); + + const sql = `SELECT ${q(refId)} AS id, ${q(labelCol)} AS label FROM ${q(refTable)} ORDER BY ${q(labelCol)} LIMIT 1000`; + const result = await client.query(sql); + res.json(result.rows); + } finally { client.release(); } + } catch (e) { + res.status(400).json({ error: e.message }); + } +}); + +app.get('/api/table/:table', async (req, res) => { + try { + const table = ensureTable(req.params.table); + const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000); + const client = await getClient(); + try { + const pks = await loadPrimaryKey(client, table); + const orderBy = pks.length ? `ORDER BY ${pks.map(q).join(', ')} DESC` : ''; + const sql = `SELECT * FROM ${q(table)} ${orderBy} LIMIT ${limit}`; + const result = await client.query(sql); + + // Normalizar: siempre devolver objetos {col: valor} + const colNames = result.fields.map(f => f.name); + let rows = result.rows; + if (rows.length && Array.isArray(rows[0])) { + rows = rows.map(r => Object.fromEntries(r.map((v, i) => [colNames[i], v]))); + } + res.json(rows); + } finally { client.release(); } + } catch (e) { + res.status(400).json({ error: e.message, code: e.code, detail: e.detail }); + } +}); + +app.post('/api/table/:table', async (req, res) => { + const table = ensureTable(req.params.table); + const payload = req.body || {}; + try { + const client = await getClient(); + try { + const columns = await loadColumns(client, table); + const insertable = columns.filter(c => + !c.is_primary && !c.is_identity && !(c.column_default || '').startsWith('nextval(') + ); + const allowedCols = new Set(insertable.map(c => c.column_name)); + + const cols = []; + const vals = []; + const params = []; + let idx = 1; + for (const [k, v] of Object.entries(payload)) { + if (!allowedCols.has(k)) continue; + if (!VALID_IDENT.test(k)) continue; + cols.push(q(k)); + vals.push(`$${idx++}`); + params.push(v); + } + + if (!cols.length) { + const { rows } = await client.query(`INSERT INTO ${q(table)} DEFAULT VALUES RETURNING *`); + res.status(201).json({ inserted: rows[0] }); + } else { + const { rows } = await client.query( + `INSERT INTO ${q(table)} (${cols.join(', ')}) VALUES (${vals.join(', ')}) RETURNING *`, + params + ); + res.status(201).json({ inserted: rows[0] }); + } + } catch (e) { + if (e.code === '23503') return res.status(400).json({ error: 'Violación de clave foránea', detail: e.detail }); + if (e.code === '23505') return res.status(400).json({ error: 'Violación de unicidad', detail: e.detail }); + if (e.code === '23514') return res.status(400).json({ error: 'Violación de CHECK', detail: e.detail }); + if (e.code === '23502') return res.status(400).json({ error: 'Campo NOT NULL faltante', detail: e.detail }); + throw e; + } + } catch (e) { + res.status(400).json({ error: e.message }); + } +}); + +// ---------------------------------------------------------- +// 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(`\nConexión con la base de datos ${chalk.green(process.env.DB_NAME)} fue exitosa.`); console.log('Fecha y hora actual de la base de datos:', res.rows[0].hora); - client.release(); // liberar el cliente de nuevo al pool + client.release(); } catch (error) { console.error('Error al conectar con la base de datos al iniciar:', error.message); - console.error(`Troubleshooting:\n1. Compruebe que las bases de datos se iniciaron correctamente.\n2. Verifique las credenciales y puertos de acceso a la base de datos.\n3. Si está conectandose a una base de datos externa a localhost, verifique las reglas del firewal de entrada y salida de ambos dispositivos.`); - + console.error('Revisar credenciales y accesos de red.'); } } - -// === Servir páginas estáticas === - -app.get('/',(req, res) => res.sendFile(path.join(__dirname, 'pages', 'index.html'))); - -app.get('/planes', async (req, res) => { - try { - const { rows: [row] } = await pool.query( - 'SELECT api.get_planes_json($1) AS data;', - [true] - ); - res.type('application/json').send(row.data); - } catch (err) { - console.error(err); - res.status(500).json({ error: 'Error al cargar planes' }); - } -}); - - -app.post('/api/registro', async (req, res) => { - const { - nombre_empresa, - rut, - correo, - telefono, - direccion, - logo, - clave_acceso, - plan_id - } = req.body; - - try { - const client = await pool.connect(); - - // 1. Hashear la contraseña - const hash = await bcrypt.hash(clave_acceso, 10); - - // 2. Insertar el tenant - const result = await client.query(` - INSERT INTO tenant ( - nombre_empresa, rut, correo, telefono, direccion, logo, - clave_acceso, plan_id, nombre_base_datos - ) VALUES ( - $1, $2, $3, $4, $5, $6, - $7, $8, 'TEMPORAL' - ) - RETURNING uuid; - `, [ - nombre_empresa, rut, correo, telefono, direccion, logo, - hash, plan_id - ]); - - const uuid = result.rows[0].uuid; - const nombre_base_datos = `tenantdb_${uuid}`.replace(/-/g, '').substring(0, 24); // ajustamos para longitud segura - - // 3. Actualizar el campo nombre_base_datos - await client.query(` - UPDATE tenant SET nombre_base_datos = $1 WHERE uuid = $2 - `, [nombre_base_datos, uuid]); - - client.release(); - - return res.status(201).json({ - message: 'Tenant registrado correctamente', - uuid, - nombre_base_datos - }); - } catch (err) { - console.error(err); - return res.status(500).json({ error: 'Error al registrar tenant' }); - } -}); - - -app.post('/api/login', async (req, res) => { - const { correo, clave_acceso } = req.body; - - try { - const client = await pool.connect(); - - const result = await client.query(` - SELECT uuid, clave_acceso, nombre_empresa, nombre_base_datos - FROM tenant - WHERE correo = $1 AND estado = true - `, [correo]); - - client.release(); - - if (result.rows.length === 0) { - return res.status(401).json({ error: 'Correo no registrado o inactivo' }); - } - - const tenant = result.rows[0]; - const coincide = await bcrypt.compare(clave_acceso, tenant.clave_acceso); - - if (!coincide) { - return res.status(401).json({ error: 'Clave incorrecta' }); - } - - return res.status(200).json({ - message: 'Login correcto', - uuid: tenant.uuid, - nombre_empresa: tenant.nombre_empresa, - base_datos: tenant.nombre_base_datos - }); - - } catch (err) { - console.error(err); - return res.status(500).json({ error: 'Error al validar login' }); - } -}); - - -app.get('/roles', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'roles.html'))); -app.get('/usuarios', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'usuarios.html'))); -app.get('/categorias',(req, res) => res.sendFile(path.join(__dirname, 'pages', 'categorias.html'))); -app.get('/productos', (req, res) => res.sendFile(path.join(__dirname, 'pages', 'productos.html'))); - - -// Helper de consulta con acquire/release explícito -async function q(text, params) { - const client = await pool.connect(); - try { - return await client.query(text, params); - } finally { - client.release(); - } -} - -// === API Roles === - // GET: listar - app.get('/api/roles', async (req, res) => { - try { - const { rows } = await q('SELECT id_rol, nombre FROM roles ORDER BY id_rol ASC'); - res.json(rows); - } catch (e) { - console.error(e); - res.status(500).json({ error: 'No se pudo listar roles' }); - } - }); - - // POST: crear - app.post('/api/roles', async (req, res) => { - try { - const { nombre } = req.body; - if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' }); - const { rows } = await q( - 'INSERT INTO roles (nombre) VALUES ($1) RETURNING id_rol, nombre', - [nombre.trim()] - ); - res.status(201).json(rows[0]); - } catch (e) { - console.error(e); - // Manejo de único/duplicado - if (e.code === '23505') return res.status(409).json({ error: 'El rol ya existe' }); - res.status(500).json({ error: 'No se pudo crear el rol' }); - } - }); - -// === API Usuarios === - // GET: listar - app.get('/api/usuarios', async (req, res) => { - try { - const { rows } = await q(` - SELECT id_usuario, documento, img_perfil, nombre, apellido, correo, telefono, fec_nacimiento, activo - FROM usuarios - ORDER BY id_usuario ASC - `); - res.json(rows); - } catch (e) { - console.error(e); - res.status(500).json({ error: 'No se pudo listar usuarios' }); - } - }); - - // POST: crear - app.post('/api/usuarios', async (req, res) => { - try { - const { documento, nombre, apellido, correo, telefono, fec_nacimiento } = req.body; - if (!nombre || !apellido) return res.status(400).json({ error: 'Nombre y apellido requeridos' }); - - const { rows } = await q(` - INSERT INTO usuarios (documento, nombre, apellido, correo, telefono, fec_nacimiento) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING id_usuario, documento, nombre, apellido, correo, telefono, fec_nacimiento, activo - `, [ - documento || null, - nombre.trim(), - apellido.trim(), - correo || null, - telefono || null, - fec_nacimiento || null - ]); - - res.status(201).json(rows[0]); - } catch (e) { - console.error(e); - if (e.code === '23505') return res.status(409).json({ error: 'Documento/Correo/Teléfono ya existe' }); - res.status(500).json({ error: 'No se pudo crear el usuario' }); - } - }); - -// === API Categorías === - // GET: listar - app.get('/api/categorias', async (req, res) => { - try { - const { rows } = await q('SELECT id_categoria, nombre, visible FROM categorias ORDER BY id_categoria ASC'); - res.json(rows); - } catch (e) { - console.error(e); - res.status(500).json({ error: 'No se pudo listar categorías' }); - } - }); - - // POST: crear - app.post('/api/categorias', async (req, res) => { - try { - const { nombre, visible } = req.body; - if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' }); - const vis = (typeof visible === 'boolean') ? visible : true; - - const { rows } = await q(` - INSERT INTO categorias (nombre, visible) - VALUES ($1, $2) - RETURNING id_categoria, nombre, visible - `, [nombre.trim(), vis]); - - res.status(201).json(rows[0]); - } catch (e) { - console.error(e); - if (e.code === '23505') return res.status(409).json({ error: 'La categoría ya existe' }); - res.status(500).json({ error: 'No se pudo crear la categoría' }); - } - }); - -// === API Productos === - // GET: listar - app.get('/api/productos', async (req, res) => { - try { - const { rows } = await q(` - SELECT id_producto, nombre, img_producto, precio, activo, id_categoria - FROM productos - ORDER BY id_producto ASC - `); - res.json(rows); - } catch (e) { - console.error(e); - res.status(500).json({ error: 'No se pudo listar productos' }); - } - }); - - // POST: crear - app.post('/api/productos', async (req, res) => { - try { - let { nombre, id_categoria, precio } = req.body; - if (!nombre || !nombre.trim()) return res.status(400).json({ error: 'Nombre requerido' }); - id_categoria = parseInt(id_categoria, 10); - precio = parseFloat(precio); - if (!Number.isInteger(id_categoria)) return res.status(400).json({ error: 'id_categoria inválido' }); - if (!(precio >= 0)) return res.status(400).json({ error: 'precio inválido' }); - - const { rows } = await q(` - INSERT INTO productos (nombre, id_categoria, precio) - VALUES ($1, $2, $3) - RETURNING id_producto, nombre, precio, activo, id_categoria - `, [nombre.trim(), id_categoria, precio]); - - res.status(201).json(rows[0]); - } catch (e) { - console.error(e); - // FK categories / checks - if (e.code === '23503') return res.status(400).json({ error: 'La categoría no existe' }); - res.status(500).json({ error: 'No se pudo crear el producto' }); - } - }); - - -// Colores personalizados -let primaryColor = chalk.hex('#'+`${process.env.COL_PRI}`); -let secondaryColor = chalk.hex('#'+`${process.env.COL_SEC}`); -// let backgroundColor = chalk.hex('#'+`${process.env.COL_BG}`); - - +// ---------------------------------------------------------- +// Inicio del servidor +// ---------------------------------------------------------- app.use(expressLayouts); -// Iniciar servidor -app.listen( process.env.PORT, () => { - console.log(`Servidor de ${chalk.red('aplicación')} de ${secondaryColor('SuiteCoffee')} corriendo en ${chalk.yellow(`http://localhost:${process.env.PORT}\n`)}` ); - console.log(chalk.grey(`Comprobando accesibilidad a la db ${chalk.green(process.env.DB_NAME)} del host ${chalk.white(`${process.env.DB_HOST}`)} ...`)); + +const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; +app.listen(PORT, () => { + console.log(`Servidor de aplicación escuchando en ${chalk.yellow(`http://localhost:${PORT}`)}`); + console.log(chalk.grey(`Comprobando accesibilidad a la db ${chalk.white(process.env.DB_NAME)} del host ${chalk.white(process.env.DB_HOST)} ...`)); verificarConexion(); }); -app.get("/health", async (req, res) => { - // Podés chequear DB aquí. 200 = healthy; 503 = not ready. - res.status(200).json({ status: "ok" }); -}); \ No newline at end of file +// Healthcheck +app.get('/health', async (_req, res) => { + res.status(200).json({ status: 'ok' }); +}); diff --git a/services/manso/src/pages/dashboard.html b/services/manso/src/pages/dashboard.html new file mode 100644 index 0000000..78fbbb6 --- /dev/null +++ b/services/manso/src/pages/dashboard.html @@ -0,0 +1,293 @@ + + + + + + Dashboard + + + +
+

Dashboard

+
+ /api/* +
+ +
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
Mostrando hasta 100 filas.
+
+
+ + + +
+ Endpoints +
GET /api/tables • GET /api/schema/:tabla • GET /api/table/:tabla?limit=100 • POST /api/table/:tabla
+
+
+ + + + diff --git a/services/manso/src/pages/index.html b/services/manso/src/pages/index.html deleted file mode 100644 index 33ae362..0000000 --- a/services/manso/src/pages/index.html +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - SuiteCoffee - Autenticación - - - - - -
-

Iniciar Sesión

- - - - - -
- - -
- -
-
- -
- - -
- -
- -
-
- - - - - - \ No newline at end of file