- Updated .env.example to consolidate SQL Server credentials under PANEL_MSSQL_* variables. - Removed deprecated docker-compose.postgres.yml file. - Adjusted docker-compose.yml to utilize new SQL Server credential structure. - Enhanced README.md with Docker build and push instructions. - Refined database schema in schema.sql to align with new user and permission structures. - Updated init-database.js to reflect changes in user and session table names. - Modified user management functions in users.ts to accommodate new database schema. - Streamlined API routes to utilize PostgreSQL for user and database management. - Improved error handling and logging in various server routes.
336 lines
9.6 KiB
TypeScript
336 lines
9.6 KiB
TypeScript
/**
|
|
* Catálogo ControlDesk en PostgreSQL (esquema a24c; DDL lo aprovisiona otra aplicación).
|
|
* Devuelve columnas con alias en español para compatibilidad con la UI existente.
|
|
*/
|
|
import { env } from '$env/dynamic/private';
|
|
import { pgPool } from './db';
|
|
|
|
function schemaName(): string {
|
|
const s = 'a24c';
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s)) return 'a24c';
|
|
return s;
|
|
}
|
|
|
|
function qNodes(): string {
|
|
const s = schemaName();
|
|
return `"${s.replace(/"/g, '""')}"."database_nodes"`;
|
|
}
|
|
|
|
function qUsers(): string {
|
|
const s = schemaName();
|
|
return `"${s.replace(/"/g, '""')}"."portal_users"`;
|
|
}
|
|
|
|
const ROW_DATABASE_NODE = `
|
|
id AS "ID",
|
|
node_subnode_key AS "NodoSubNodo",
|
|
is_active AS "Activo",
|
|
rfc AS "RFC",
|
|
legal_name AS "Nombre",
|
|
branch_name AS "Sucursal",
|
|
notification_email AS "CorreoNotificacion",
|
|
server_name AS "ServerName",
|
|
database_name AS "BDName"
|
|
`;
|
|
|
|
const ROW_PORTAL_USER = `
|
|
id AS "ID",
|
|
database_node_id AS "IDNodoSubNodo",
|
|
is_authority_client AS "ClienteAutoridad",
|
|
full_name AS "Nombre",
|
|
username AS "Usuario",
|
|
bd_shelter AS "BD_Shelter"
|
|
`;
|
|
|
|
export async function listDatabaseNodes(): Promise<any[]> {
|
|
const sql = `SELECT ${ROW_DATABASE_NODE} FROM ${qNodes()}`;
|
|
const r = await pgPool.query(sql);
|
|
return r.rows;
|
|
}
|
|
|
|
export async function listDatabaseNodesActive(): Promise<any[]> {
|
|
const sql = `
|
|
SELECT ${ROW_DATABASE_NODE}
|
|
FROM ${qNodes()}
|
|
WHERE is_active = 1
|
|
AND database_name IS NOT NULL
|
|
AND TRIM(database_name) <> ''
|
|
ORDER BY node_subnode_key
|
|
`;
|
|
const r = await pgPool.query(sql);
|
|
return r.rows;
|
|
}
|
|
|
|
/** Nodos activos con `sql_password` para conectar a SQL Server (no exponer al cliente). */
|
|
export async function listDatabaseNodesForMssql(): Promise<any[]> {
|
|
const where = `
|
|
WHERE is_active = 1
|
|
AND database_name IS NOT NULL
|
|
AND TRIM(database_name) <> ''
|
|
ORDER BY node_subnode_key
|
|
`;
|
|
try {
|
|
const r = await pgPool.query(
|
|
`SELECT ${ROW_DATABASE_NODE}, sql_password AS "sql_password" FROM ${qNodes()} ${where}`
|
|
);
|
|
return r.rows;
|
|
} catch (e: any) {
|
|
if (e?.code === '42703') {
|
|
const r = await pgPool.query(`SELECT ${ROW_DATABASE_NODE} FROM ${qNodes()} ${where}`);
|
|
return r.rows.map((row: any) => ({ ...row, sql_password: null }));
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export async function listClientsCatalog(): Promise<any[]> {
|
|
const sql = `
|
|
SELECT
|
|
id AS "ID",
|
|
legal_name AS "Nombre",
|
|
node_subnode_key AS "NodoSubNodo",
|
|
notification_email AS "CorreoNotificacion",
|
|
is_active AS "Activo",
|
|
database_name AS "BDName"
|
|
FROM ${qNodes()}
|
|
`;
|
|
const r = await pgPool.query(sql);
|
|
return r.rows;
|
|
}
|
|
|
|
export async function listPortalUsers(): Promise<any[]> {
|
|
const sql = `SELECT ${ROW_PORTAL_USER} FROM ${qUsers()} ORDER BY id`;
|
|
const r = await pgPool.query(sql);
|
|
return r.rows;
|
|
}
|
|
|
|
export async function lookupNodeByNodoOrBdName(nodoName: string): Promise<any | null> {
|
|
const sql = `
|
|
SELECT
|
|
legal_name AS "Nombre",
|
|
node_subnode_key AS "NodoSubNodo",
|
|
rfc AS "RFC"
|
|
FROM ${qNodes()}
|
|
WHERE LOWER(TRIM(node_subnode_key)) = LOWER(TRIM($1::text))
|
|
OR LOWER(TRIM(database_name)) = LOWER(TRIM($1::text))
|
|
LIMIT 1
|
|
`;
|
|
const r = await pgPool.query(sql, [nodoName]);
|
|
return r.rows[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Asocia el nombre de archivo de respaldo (sin extensión) a una fila de `database_nodes`.
|
|
* Cubre: igualdad sin mayúsculas y prefijos tipo `NODO001_full_20240414` → nodo `NODO001`.
|
|
*/
|
|
export function matchNodeRowFromBackupStem(stem: string, nodes: any[]): any | null {
|
|
const raw = String(stem ?? '').trim();
|
|
if (!raw || !nodes?.length) return null;
|
|
const key = raw.toLowerCase();
|
|
|
|
const nodoOf = (row: any) => String(row.NodoSubNodo ?? '').trim().toLowerCase();
|
|
const bdOf = (row: any) => String(row.BDName ?? '').trim().toLowerCase();
|
|
|
|
for (const row of nodes) {
|
|
const n = nodoOf(row);
|
|
const b = bdOf(row);
|
|
if (n && n === key) return row;
|
|
if (b && b === key) return row;
|
|
}
|
|
|
|
type Cand = { row: any; len: number };
|
|
const cands: Cand[] = [];
|
|
for (const row of nodes) {
|
|
const n = nodoOf(row);
|
|
const b = bdOf(row);
|
|
if (n && (key === n || key.startsWith(`${n}_`) || key.startsWith(`${n}-`))) {
|
|
cands.push({ row, len: n.length });
|
|
}
|
|
if (b && b !== n && (key === b || key.startsWith(`${b}_`) || key.startsWith(`${b}-`))) {
|
|
cands.push({ row, len: b.length });
|
|
}
|
|
}
|
|
if (!cands.length) return null;
|
|
cands.sort((a, b) => b.len - a.len);
|
|
return cands[0].row;
|
|
}
|
|
|
|
/** Datos de contacto para alertas (equivalente a la consulta previa sobre Usuarios/BasesDeDatos). */
|
|
export async function lookupAlertClientData(nodoName: string): Promise<any | null> {
|
|
const sql = `
|
|
SELECT
|
|
pu.is_authority_client AS "ClienteAutoridad",
|
|
pu.full_name AS "Nombre",
|
|
pu.username AS "Usuario",
|
|
dn.notification_email AS "CorreoNotificacion"
|
|
FROM ${qUsers()} pu
|
|
LEFT JOIN ${qNodes()} dn ON pu.database_node_id = dn.id
|
|
WHERE pu.username = $1
|
|
OR dn.database_name = $1
|
|
OR dn.node_subnode_key = $1
|
|
LIMIT 1
|
|
`;
|
|
const r = await pgPool.query(sql, [nodoName]);
|
|
return r.rows[0] ?? null;
|
|
}
|
|
|
|
export async function updateNodeActive(id: number, activo: boolean): Promise<void> {
|
|
await pgPool.query(`UPDATE ${qNodes()} SET is_active = $1 WHERE id = $2`, [activo ? 1 : 0, id]);
|
|
}
|
|
|
|
export async function updateNodeLegalName(id: number, nombre: string): Promise<void> {
|
|
await pgPool.query(`UPDATE ${qNodes()} SET legal_name = $1 WHERE id = $2`, [nombre, id]);
|
|
}
|
|
|
|
export async function insertDatabaseNode(row: {
|
|
nodoSubNodo: string;
|
|
rfc: string;
|
|
nombre: string;
|
|
sucursal: string;
|
|
correo: string;
|
|
serverName: string;
|
|
bdName: string;
|
|
activo: number;
|
|
}): Promise<void> {
|
|
await pgPool.query(
|
|
`
|
|
INSERT INTO ${qNodes()} (
|
|
node_subnode_key, rfc, legal_name, branch_name,
|
|
notification_email, server_name, database_name, is_active
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
`,
|
|
[
|
|
row.nodoSubNodo,
|
|
row.rfc,
|
|
row.nombre,
|
|
row.sucursal,
|
|
row.correo,
|
|
row.serverName,
|
|
row.bdName,
|
|
row.activo
|
|
]
|
|
);
|
|
}
|
|
|
|
export async function updateDatabaseNode(
|
|
id: number,
|
|
row: {
|
|
nodoSubNodo: string;
|
|
rfc: string;
|
|
nombre: string;
|
|
sucursal: string;
|
|
correo: string;
|
|
serverName: string;
|
|
bdName: string;
|
|
activo: number;
|
|
}
|
|
): Promise<void> {
|
|
await pgPool.query(
|
|
`
|
|
UPDATE ${qNodes()} SET
|
|
node_subnode_key = $1,
|
|
rfc = $2,
|
|
legal_name = $3,
|
|
branch_name = $4,
|
|
notification_email = $5,
|
|
server_name = $6,
|
|
database_name = $7,
|
|
is_active = $8
|
|
WHERE id = $9
|
|
`,
|
|
[
|
|
row.nodoSubNodo,
|
|
row.rfc,
|
|
row.nombre,
|
|
row.sucursal,
|
|
row.correo,
|
|
row.serverName,
|
|
row.bdName,
|
|
row.activo,
|
|
id
|
|
]
|
|
);
|
|
}
|
|
|
|
export async function deleteDatabaseNode(id: number): Promise<void> {
|
|
await pgPool.query(`DELETE FROM ${qNodes()} WHERE id = $1`, [id]);
|
|
}
|
|
|
|
export async function insertPortalUser(row: {
|
|
databaseNodeId: number;
|
|
isAuthorityClient: number;
|
|
fullName: string;
|
|
username: string;
|
|
passwordHash: string;
|
|
bdShelter: string | null;
|
|
}): Promise<void> {
|
|
await pgPool.query(
|
|
`
|
|
INSERT INTO ${qUsers()} (
|
|
database_node_id, is_authority_client, full_name, username, password_hash, bd_shelter
|
|
) VALUES ($1, $2, $3, $4, $5, $6)
|
|
`,
|
|
[
|
|
row.databaseNodeId,
|
|
row.isAuthorityClient,
|
|
row.fullName,
|
|
row.username,
|
|
row.passwordHash,
|
|
row.bdShelter
|
|
]
|
|
);
|
|
}
|
|
|
|
export async function updatePortalUser(
|
|
id: number,
|
|
row: {
|
|
databaseNodeId: number;
|
|
isAuthorityClient: number;
|
|
fullName: string;
|
|
username: string;
|
|
bdShelter: string | null;
|
|
passwordHash?: string;
|
|
}
|
|
): Promise<void> {
|
|
if (row.passwordHash !== undefined) {
|
|
await pgPool.query(
|
|
`
|
|
UPDATE ${qUsers()} SET
|
|
database_node_id = $1,
|
|
is_authority_client = $2,
|
|
full_name = $3,
|
|
username = $4,
|
|
password_hash = $5,
|
|
bd_shelter = $6
|
|
WHERE id = $7
|
|
`,
|
|
[
|
|
row.databaseNodeId,
|
|
row.isAuthorityClient,
|
|
row.fullName,
|
|
row.username,
|
|
row.passwordHash,
|
|
row.bdShelter,
|
|
id
|
|
]
|
|
);
|
|
} else {
|
|
await pgPool.query(
|
|
`
|
|
UPDATE ${qUsers()} SET
|
|
database_node_id = $1,
|
|
is_authority_client = $2,
|
|
full_name = $3,
|
|
username = $4,
|
|
bd_shelter = $5
|
|
WHERE id = $6
|
|
`,
|
|
[row.databaseNodeId, row.isAuthorityClient, row.fullName, row.username, row.bdShelter, id]
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function deletePortalUser(id: number): Promise<void> {
|
|
await pgPool.query(`DELETE FROM ${qUsers()} WHERE id = $1`, [id]);
|
|
}
|