From 50bbf7bdcc3594200a753b88a955e50cd6f06bd8 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Wed, 15 Apr 2026 17:54:28 -0500 Subject: [PATCH] Implement streaming file download for backup route - Enhanced the backup server route to support streaming of large backup files using Node.js streams. - Improved error handling for file access and added appropriate HTTP responses for missing parameters and inaccessible files. - Updated response headers to include content length and type for better client handling. --- src/routes/+page.server.ts | 1102 +++++++++++++++++----------------- src/routes/backup/+server.ts | 23 +- 2 files changed, 567 insertions(+), 558 deletions(-) diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index b5467ba..aec1d18 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,551 +1,551 @@ -import { env } from '$env/dynamic/private'; -import fs from 'node:fs/promises'; -import path from 'node:path'; -import crypto from 'node:crypto'; -import { redirect } from '@sveltejs/kit'; -import type { PageServerLoad, Actions } from './$types'; -import { verifyToken } from '$lib/server/auth'; -import { getUserById, filterDatabasesByUserPermissions } from '$lib/server/users'; -import { loadSqlDashboardFromNodes, type CatalogNodeRow } from '$lib/server/mssql-nodes'; -import { - listClientsCatalog, - listDatabaseNodes, - listDatabaseNodesForMssql, - listPortalUsers, - matchNodeRowFromBackupStem, - lookupAlertClientData, - updateNodeActive, - updateNodeLegalName, - insertDatabaseNode, - updateDatabaseNode, - deleteDatabaseNode, - insertPortalUser, - updatePortalUser, - deletePortalUser -} from '$lib/server/controldesk-pg'; - -function parseActivoField(formData: FormData): number { - const v = formData.get('Activo'); - return v === 'true' || v === '1' ? 1 : 0; -} - -// Helper to check disk space (Simple Windows implementation) -// Note: In production, consider a specialized library -async function getDiskSpace(drive: string) { - try { - // Using fs.statfs if available (Node 18.15+) or just mock for now - // Implementing proper disk check via Powershell is safer - return { free: 0, total: 0 }; - } catch { - return { free: 0, total: 0 }; - } -} - - -export const load: PageServerLoad = async ({ cookies }) => { - // 1. Auth Check - Verificar token JWT - const token = cookies.get('session_token'); - if (!token) { - throw redirect(303, '/login'); - } - - const session = verifyToken(token); - if (!session) { - throw redirect(303, '/login'); - } - - const currentUser = await getUserById(session.userId); - if (!currentUser || !currentUser.activo) { - throw redirect(303, '/login'); - } - - // Initialize result containers - let databaseRows: any[] = []; - let summaryMain: any = { total_databases: 0, total_size_gb: 0 }; - let restoredCount = 0; - let notRestoredCount = 0; - - let databaseRowsAZ: any[] = []; - let summaryAZ: any = null; - - let backupFiles: any[] = []; - let clientsData: any[] = []; - let alertsData: any[] = []; - let basesDeDatosList: any[] = []; - let usuariosList: any[] = []; - let restoreHistory: Record = {}; - let effectivenessByDb: Record = {}; - - // Connection errors to be passed to UI - let errors = { - primary: null as string | null, - secondary: null as string | null, - azure: null as string | null, - backups: null as string | null - }; - - // --- 1. SQL Server: una conexión por servidor (master) según a24c.database_nodes --- - let nodesForSql: CatalogNodeRow[] = []; - try { - nodesForSql = (await listDatabaseNodesForMssql()) as CatalogNodeRow[]; - } catch (e: any) { - console.error('Error leyendo database_nodes para SQL Server:', e); - errors.primary = `PostgreSQL / database_nodes: ${e.message}`; - } - - try { - const bundle = await loadSqlDashboardFromNodes(nodesForSql); - databaseRows = bundle.databaseRows; - summaryMain = bundle.summaryMain; - alertsData = bundle.alertsData; - restoreHistory = bundle.restoreHistory; - effectivenessByDb = bundle.effectivenessByDb; - databaseRowsAZ = [...bundle.databaseRows]; - summaryAZ = { ...bundle.summaryMain }; - } catch (e: any) { - console.error('Error métricas SQL Server por nodo:', e); - errors.primary = `${errors.primary ? errors.primary + ' · ' : ''}SQL Server (nodos): ${e.message}`; - } - - // --- 3. Catálogo ControlDesk (PostgreSQL, esquema a24c) --- - let controlDeskOk = false; - try { - clientsData = await listClientsCatalog(); - basesDeDatosList = await listDatabaseNodes(); - usuariosList = await listPortalUsers(); - controlDeskOk = true; - } catch (e: any) { - console.error('Error loading ControlDesk (PostgreSQL):', e); - errors.azure = `Error conectando al catálogo ControlDesk (PostgreSQL): ${e.message}`; - } - - // --- 4. Process Backups, Hydrate Alerts & Enriquecer databaseRows con datos de BasesDeDatos --- - try { - let files: string[] = []; - try { - files = await fs.readdir(env.BACKUP_PATH as string); - } catch (e) { - files = []; - errors.backups = "No se pudo acceder a la carpeta de respaldos."; - } - - for (const file of files) { - if (file === '.' || file === '..') continue; - - const filePath = path.join(env.BACKUP_PATH as string, file); - let stats; - try { - stats = await fs.stat(filePath); - } catch { continue; } - - if (!stats.isFile()) continue; - - const nodoName = path.parse(file).name; - let clientData: any = null; - - if (controlDeskOk && basesDeDatosList.length) { - try { - clientData = matchNodeRowFromBackupStem(nodoName, basesDeDatosList); - } catch { - /* ignore */ - } - } - - backupFiles.push({ - name: file, - nodo_name: (clientData?.NodoSubNodo as string | undefined) || nodoName, - client_name: clientData?.Nombre ?? 'Cliente no identificado', - client_authority: clientData?.RFC ?? 'N/A', - bd_shelter: 'N/A', - date: stats.mtime, - size: (stats.size / 1024 / 1024).toFixed(2) + " MB" - }); - } - - // Ordenar respaldos de más reciente a más antiguo por fecha de modificación - backupFiles.sort((a, b) => { - const da = new Date(a.date).getTime(); - const db = new Date(b.date).getTime(); - return db - da; - }); - - if (controlDeskOk && alertsData.length > 0) { - const hydratedAlerts = []; - for (const alert of alertsData) { - const nodoName = alert.visible_name; - let cData: any = null; - try { - cData = await lookupAlertClientData(String(nodoName)); - } catch { - /* ignore */ - } - - hydratedAlerts.push({ ...alert, clientData: cData }); - } - alertsData = hydratedAlerts; - } - - // Enriquecer databaseRows con catálogo ControlDesk (PostgreSQL) - if (controlDeskOk && databaseRows.length > 0) { - try { - const bases = (await listDatabaseNodes()) as any[]; - - const mapByBdName = new Map(); - const mapByNodo = new Map(); - for (const bd of bases) { - if (bd.BDName) { - mapByBdName.set(String(bd.BDName).toLowerCase(), bd); - } - if (bd.NodoSubNodo) { - mapByNodo.set(String(bd.NodoSubNodo).toLowerCase(), bd); - } - } - - databaseRows = databaseRows.map((row) => { - const key = String(row.visible_name ?? row.original_name ?? '').toLowerCase(); - // Intentar match por BDName primero, luego por NodoSubNodo - let match = mapByBdName.get(key); - if (!match) { - match = mapByNodo.get(key); - } - if (!match) { - console.log(`No match found for database: ${key}`); - return row; - } - - return { - ...row, - NodoSubNodo: match.NodoSubNodo, - client_name: match.Nombre, - BDName: match.BDName - }; - }); - } catch (e) { - console.error('Error enriching databaseRows with BasesDeDatos info:', e); - } - } - - } catch (e: any) { - console.error("Error processing backups/alerts hydration:", e); - } - - // CALCULAR MÉTRICAS DE RESTAURACIÓN basadas en last_restore_date (ANTES del filtro) - restoredCount = 0; - notRestoredCount = 0; - const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); - - for (const db of databaseRows) { - // Verificar si tiene una restauración en las últimas 24 horas - if (db.last_restore_date) { - const restoreDate = new Date(db.last_restore_date); - if (restoreDate > oneDayAgo) { - restoredCount++; - } else { - notRestoredCount++; - } - } else { - notRestoredCount++; - } - } - - // Aplicar filtro de permisos de usuario (si no es admin) - if (!currentUser.es_admin) { - databaseRows = await filterDatabasesByUserPermissions(currentUser.id, databaseRows); - alertsData = await filterDatabasesByUserPermissions(currentUser.id, alertsData); - - // Filtrar backups según las bases de datos permitidas (usar NodoSubNodo) - const allowedNodos = new Set(databaseRows.map(db => (db.NodoSubNodo || db.visible_name).toLowerCase())); - backupFiles = backupFiles.filter(backup => { - const backupNodo = (backup.nodo_name || '').toLowerCase(); - return allowedNodos.has(backupNodo); - }); - - // RECALCULAR MÉTRICAS basadas en las bases de datos filtradas - summaryMain.total_size_gb = databaseRows.reduce((sum, db) => sum + (db.total_size_gb || 0), 0); - summaryMain.total_size_gb = Math.round(summaryMain.total_size_gb * 100) / 100; - - restoredCount = 0; - notRestoredCount = 0; - for (const db of databaseRows) { - // Verificar si tiene una restauración en las últimas 24 horas - if (db.last_restore_date) { - const restoreDate = new Date(db.last_restore_date); - if (restoreDate > oneDayAgo) { - restoredCount++; - } else { - notRestoredCount++; - } - } else { - notRestoredCount++; - } - } - } - - return { - databaseRows, - summaryMain, - restoredCount, - notRestoredCount, - - databaseRowsAZ, - summaryAZ, - - backupFiles, - clientsData, - alertsData, - basesDeDatosList, - usuariosList, - restoreHistory, - effectivenessByDb, - - errors, // Return the collected errors - currentUser // Añadir usuario actual para la UI - }; -}; - -// Acciones para actualizar estado de clientes (activar/desactivar) y editar nombre de base de datos -export const actions: Actions = { - toggleClient: async ({ request }) => { - try { - const formData = await request.formData(); - const idRaw = formData.get('id'); - const activoRaw = formData.get('activo'); - - if (!idRaw || !activoRaw) { - return { success: false, message: 'Parámetros incompletos' }; - } - - const id = Number(idRaw); - const activo = activoRaw === 'true'; - - await updateNodeActive(id, activo); - - return { success: true }; - } catch (e: any) { - console.error('Error updating client active state:', e); - return { success: false, message: e.message }; - } - }, - - updateDatabaseName: async ({ request }) => { - try { - const formData = await request.formData(); - const idRaw = formData.get('id'); - const nombreRaw = formData.get('nombre'); - - if (!idRaw || !nombreRaw) { - return { success: false, message: 'Parámetros incompletos' }; - } - - const id = Number(idRaw); - const nombre = String(nombreRaw).trim(); - - if (!nombre) { - return { success: false, message: 'El nombre no puede estar vacío' }; - } - - await updateNodeLegalName(id, nombre); - - return { success: true }; - } catch (e: any) { - console.error('Error updating database name:', e); - return { success: false, message: e.message }; - } - }, - - createDatabase: async ({ request }) => { - try { - const formData = await request.formData(); - const nodoSubNodo = String(formData.get('NodoSubNodo') || '').trim(); - const rfc = String(formData.get('RFC') || '').trim(); - const nombre = String(formData.get('Nombre') || '').trim(); - const sucursal = String(formData.get('Sucursal') || '').trim(); - const correo = String(formData.get('CorreoNotificacion') || '').trim(); - const serverName = String(formData.get('ServerName') || '').trim(); - const bdName = String(formData.get('BDName') || '').trim(); - const activo = parseActivoField(formData); - - if (!nodoSubNodo || !rfc || !nombre || !sucursal || !correo || !serverName || !bdName) { - return { success: false, message: 'Todos los campos son requeridos' }; - } - - await insertDatabaseNode({ - nodoSubNodo, - rfc, - nombre, - sucursal, - correo, - serverName, - bdName, - activo - }); - - return { success: true }; - } catch (e: any) { - console.error('Error creating database:', e); - return { success: false, message: e.message }; - } - }, - - updateDatabase: async ({ request }) => { - try { - const formData = await request.formData(); - const id = Number(formData.get('ID')); - const nodoSubNodo = String(formData.get('NodoSubNodo') || '').trim(); - const rfc = String(formData.get('RFC') || '').trim(); - const nombre = String(formData.get('Nombre') || '').trim(); - const sucursal = String(formData.get('Sucursal') || '').trim(); - const correo = String(formData.get('CorreoNotificacion') || '').trim(); - const serverName = String(formData.get('ServerName') || '').trim(); - const bdName = String(formData.get('BDName') || '').trim(); - const activo = parseActivoField(formData); - - if (!id || !nodoSubNodo || !rfc || !nombre || !sucursal || !correo || !serverName || !bdName) { - return { success: false, message: 'Todos los campos son requeridos' }; - } - - await updateDatabaseNode(id, { - nodoSubNodo, - rfc, - nombre, - sucursal, - correo, - serverName, - bdName, - activo - }); - - return { success: true }; - } catch (e: any) { - console.error('Error updating database:', e); - return { success: false, message: e.message }; - } - }, - - deleteDatabase: async ({ request }) => { - try { - const formData = await request.formData(); - const id = Number(formData.get('ID')); - - if (!id) { - return { success: false, message: 'ID requerido' }; - } - - await deleteDatabaseNode(id); - - return { success: true }; - } catch (e: any) { - console.error('Error deleting database:', e); - return { success: false, message: e.message }; - } - }, - - // ---- Acciones para a24c.portal_users (antes CONTROLDESK.dbo.Usuarios) ---- - - createUsuario: async ({ request }) => { - try { - const formData = await request.formData(); - const idNodoSubNodo = Number(formData.get('IDNodoSubNodo')); - const clienteAutoridad = Number(formData.get('ClienteAutoridad') ?? 0); - const nombre = String(formData.get('Nombre') || '').trim(); - const usuario = String(formData.get('Usuario') || '').trim(); - const password = String(formData.get('Password') || ''); - const bdShelter = String(formData.get('BD_Shelter') || '').trim() || null; - - if (!Number.isFinite(idNodoSubNodo) || idNodoSubNodo <= 0 || !nombre || !usuario || !password) { - return { success: false, message: 'Todos los campos obligatorios deben completarse' }; - } - if (!Number.isFinite(clienteAutoridad)) { - return { success: false, message: 'Cliente autoridad inválido' }; - } - - // Hash WinDev-compatible: SHA-512(password + "soluciones"), hex UPPERCASE - const passwordHash = crypto - .createHash('sha512') - .update(password + 'soluciones', 'utf8') - .digest('hex') - .toUpperCase(); - - await insertPortalUser({ - databaseNodeId: idNodoSubNodo, - isAuthorityClient: clienteAutoridad, - fullName: nombre, - username: usuario, - passwordHash, - bdShelter - }); - - return { success: true }; - } catch (e: any) { - console.error('Error creating usuario:', e); - return { success: false, message: e.message }; - } - }, - - updateUsuario: async ({ request }) => { - try { - const formData = await request.formData(); - const id = Number(formData.get('ID')); - const idNodoSubNodo = Number(formData.get('IDNodoSubNodo')); - const clienteAutoridad = Number(formData.get('ClienteAutoridad') ?? 0); - const nombre = String(formData.get('Nombre') || '').trim(); - const usuario = String(formData.get('Usuario') || '').trim(); - const password = String(formData.get('Password') || ''); - const bdShelter = String(formData.get('BD_Shelter') || '').trim() || null; - - if (!id || !Number.isFinite(idNodoSubNodo) || idNodoSubNodo <= 0 || !nombre || !usuario) { - return { success: false, message: 'Todos los campos obligatorios deben completarse' }; - } - if (!Number.isFinite(clienteAutoridad)) { - return { success: false, message: 'Cliente autoridad inválido' }; - } - - if (password) { - const passwordHash = crypto - .createHash('sha512') - .update(password + 'soluciones', 'utf8') - .digest('hex') - .toUpperCase(); - await updatePortalUser(id, { - databaseNodeId: idNodoSubNodo, - isAuthorityClient: clienteAutoridad, - fullName: nombre, - username: usuario, - bdShelter, - passwordHash - }); - } else { - await updatePortalUser(id, { - databaseNodeId: idNodoSubNodo, - isAuthorityClient: clienteAutoridad, - fullName: nombre, - username: usuario, - bdShelter - }); - } - - return { success: true }; - } catch (e: any) { - console.error('Error updating usuario:', e); - return { success: false, message: e.message }; - } - }, - - deleteUsuario: async ({ request }) => { - try { - const formData = await request.formData(); - const id = Number(formData.get('ID')); - - if (!id) { - return { success: false, message: 'ID requerido' }; - } - - await deletePortalUser(id); - - return { success: true }; - } catch (e: any) { - console.error('Error deleting usuario:', e); - return { success: false, message: e.message }; - } - } -}; +import { env } from '$env/dynamic/private'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad, Actions } from './$types'; +import { verifyToken } from '$lib/server/auth'; +import { getUserById, filterDatabasesByUserPermissions } from '$lib/server/users'; +import { loadSqlDashboardFromNodes, type CatalogNodeRow } from '$lib/server/mssql-nodes'; +import { + listClientsCatalog, + listDatabaseNodes, + listDatabaseNodesForMssql, + listPortalUsers, + matchNodeRowFromBackupStem, + lookupAlertClientData, + updateNodeActive, + updateNodeLegalName, + insertDatabaseNode, + updateDatabaseNode, + deleteDatabaseNode, + insertPortalUser, + updatePortalUser, + deletePortalUser +} from '$lib/server/controldesk-pg'; + +function parseActivoField(formData: FormData): number { + const v = formData.get('Activo'); + return v === 'true' || v === '1' ? 1 : 0; +} + +// Helper to check disk space (Simple Windows implementation) +// Note: In production, consider a specialized library +async function getDiskSpace(drive: string) { + try { + // Using fs.statfs if available (Node 18.15+) or just mock for now + // Implementing proper disk check via Powershell is safer + return { free: 0, total: 0 }; + } catch { + return { free: 0, total: 0 }; + } +} + + +export const load: PageServerLoad = async ({ cookies }) => { + // 1. Auth Check - Verificar token JWT + const token = cookies.get('session_token'); + if (!token) { + throw redirect(303, '/login'); + } + + const session = verifyToken(token); + if (!session) { + throw redirect(303, '/login'); + } + + const currentUser = await getUserById(session.userId); + if (!currentUser || !currentUser.activo) { + throw redirect(303, '/login'); + } + + // Initialize result containers + let databaseRows: any[] = []; + let summaryMain: any = { total_databases: 0, total_size_gb: 0 }; + let restoredCount = 0; + let notRestoredCount = 0; + + let databaseRowsAZ: any[] = []; + let summaryAZ: any = null; + + let backupFiles: any[] = []; + let clientsData: any[] = []; + let alertsData: any[] = []; + let basesDeDatosList: any[] = []; + let usuariosList: any[] = []; + let restoreHistory: Record = {}; + let effectivenessByDb: Record = {}; + + // Connection errors to be passed to UI + let errors = { + primary: null as string | null, + secondary: null as string | null, + azure: null as string | null, + backups: null as string | null + }; + + // --- 1. SQL Server: una conexión por servidor (master) según a24c.database_nodes --- + let nodesForSql: CatalogNodeRow[] = []; + try { + nodesForSql = (await listDatabaseNodesForMssql()) as CatalogNodeRow[]; + } catch (e: any) { + console.error('Error leyendo database_nodes para SQL Server:', e); + errors.primary = `PostgreSQL / database_nodes: ${e.message}`; + } + + try { + const bundle = await loadSqlDashboardFromNodes(nodesForSql); + databaseRows = bundle.databaseRows; + summaryMain = bundle.summaryMain; + alertsData = bundle.alertsData; + restoreHistory = bundle.restoreHistory; + effectivenessByDb = bundle.effectivenessByDb; + databaseRowsAZ = [...bundle.databaseRows]; + summaryAZ = { ...bundle.summaryMain }; + } catch (e: any) { + console.error('Error métricas SQL Server por nodo:', e); + errors.primary = `${errors.primary ? errors.primary + ' · ' : ''}SQL Server (nodos): ${e.message}`; + } + + // --- 3. Catálogo ControlDesk (PostgreSQL, esquema a24c) --- + let controlDeskOk = false; + try { + clientsData = await listClientsCatalog(); + basesDeDatosList = await listDatabaseNodes(); + usuariosList = await listPortalUsers(); + controlDeskOk = true; + } catch (e: any) { + console.error('Error loading ControlDesk (PostgreSQL):', e); + errors.azure = `Error conectando al catálogo ControlDesk (PostgreSQL): ${e.message}`; + } + + // --- 4. Process Backups, Hydrate Alerts & Enriquecer databaseRows con datos de BasesDeDatos --- + try { + let files: string[] = []; + try { + files = await fs.readdir(env.BACKUP_PATH as string); + } catch (e) { + files = []; + errors.backups = "No se pudo acceder a la carpeta de respaldos."; + } + + for (const file of files) { + if (file === '.' || file === '..') continue; + + const filePath = path.join(env.BACKUP_PATH as string, file); + let stats; + try { + stats = await fs.stat(filePath); + } catch { continue; } + + if (!stats.isFile()) continue; + + const nodoName = path.parse(file).name; + let clientData: any = null; + + if (controlDeskOk && basesDeDatosList.length) { + try { + clientData = matchNodeRowFromBackupStem(nodoName, basesDeDatosList); + } catch { + /* ignore */ + } + } + + backupFiles.push({ + name: file, + nodo_name: (clientData?.NodoSubNodo as string | undefined) || nodoName, + client_name: clientData?.Nombre ?? 'Cliente no identificado', + client_authority: clientData?.RFC ?? 'N/A', + bd_shelter: 'N/A', + date: stats.mtime, + size: (stats.size / 1024 / 1024).toFixed(2) + " MB" + }); + } + + // Ordenar respaldos de más reciente a más antiguo por fecha de modificación + backupFiles.sort((a, b) => { + const da = new Date(a.date).getTime(); + const db = new Date(b.date).getTime(); + return db - da; + }); + + if (controlDeskOk && alertsData.length > 0) { + const hydratedAlerts = []; + for (const alert of alertsData) { + const nodoName = alert.visible_name; + let cData: any = null; + try { + cData = await lookupAlertClientData(String(nodoName)); + } catch { + /* ignore */ + } + + hydratedAlerts.push({ ...alert, clientData: cData }); + } + alertsData = hydratedAlerts; + } + + // Enriquecer databaseRows con catálogo ControlDesk (PostgreSQL) + if (controlDeskOk && databaseRows.length > 0) { + try { + const bases = (await listDatabaseNodes()) as any[]; + + const mapByBdName = new Map(); + const mapByNodo = new Map(); + for (const bd of bases) { + if (bd.BDName) { + mapByBdName.set(String(bd.BDName).toLowerCase(), bd); + } + if (bd.NodoSubNodo) { + mapByNodo.set(String(bd.NodoSubNodo).toLowerCase(), bd); + } + } + + databaseRows = databaseRows.map((row) => { + const key = String(row.visible_name ?? row.original_name ?? '').toLowerCase(); + // Intentar match por BDName primero, luego por NodoSubNodo + let match = mapByBdName.get(key); + if (!match) { + match = mapByNodo.get(key); + } + if (!match) { + console.log(`No match found for database: ${key}`); + return row; + } + + return { + ...row, + NodoSubNodo: match.NodoSubNodo, + client_name: match.Nombre, + BDName: match.BDName + }; + }); + } catch (e) { + console.error('Error enriching databaseRows with BasesDeDatos info:', e); + } + } + + } catch (e: any) { + console.error("Error processing backups/alerts hydration:", e); + } + + // CALCULAR MÉTRICAS DE RESTAURACIÓN basadas en last_restore_date (ANTES del filtro) + restoredCount = 0; + notRestoredCount = 0; + const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); + + for (const db of databaseRows) { + // Verificar si tiene una restauración en las últimas 24 horas + if (db.last_restore_date) { + const restoreDate = new Date(db.last_restore_date); + if (restoreDate > oneDayAgo) { + restoredCount++; + } else { + notRestoredCount++; + } + } else { + notRestoredCount++; + } + } + + // Aplicar filtro de permisos de usuario (si no es admin) + if (!currentUser.es_admin) { + databaseRows = await filterDatabasesByUserPermissions(currentUser.id, databaseRows); + alertsData = await filterDatabasesByUserPermissions(currentUser.id, alertsData); + + // Filtrar backups según las bases de datos permitidas (usar NodoSubNodo) + const allowedNodos = new Set(databaseRows.map(db => (db.NodoSubNodo || db.visible_name).toLowerCase())); + backupFiles = backupFiles.filter(backup => { + const backupNodo = (backup.nodo_name || '').toLowerCase(); + return allowedNodos.has(backupNodo); + }); + + // RECALCULAR MÉTRICAS basadas en las bases de datos filtradas + summaryMain.total_size_gb = databaseRows.reduce((sum, db) => sum + (db.total_size_gb || 0), 0); + summaryMain.total_size_gb = Math.round(summaryMain.total_size_gb * 100) / 100; + + restoredCount = 0; + notRestoredCount = 0; + for (const db of databaseRows) { + // Verificar si tiene una restauración en las últimas 24 horas + if (db.last_restore_date) { + const restoreDate = new Date(db.last_restore_date); + if (restoreDate > oneDayAgo) { + restoredCount++; + } else { + notRestoredCount++; + } + } else { + notRestoredCount++; + } + } + } + + return { + databaseRows, + summaryMain, + restoredCount, + notRestoredCount, + + databaseRowsAZ, + summaryAZ, + + backupFiles, + clientsData, + alertsData, + basesDeDatosList, + usuariosList, + restoreHistory, + effectivenessByDb, + + errors, // Return the collected errors + currentUser // Añadir usuario actual para la UI + }; +}; + +// Acciones para actualizar estado de clientes (activar/desactivar) y editar nombre de base de datos +export const actions: Actions = { + toggleClient: async ({ request }) => { + try { + const formData = await request.formData(); + const idRaw = formData.get('id'); + const activoRaw = formData.get('activo'); + + if (!idRaw || !activoRaw) { + return { success: false, message: 'Parámetros incompletos' }; + } + + const id = Number(idRaw); + const activo = activoRaw === 'true'; + + await updateNodeActive(id, activo); + + return { success: true }; + } catch (e: any) { + console.error('Error updating client active state:', e); + return { success: false, message: e.message }; + } + }, + + updateDatabaseName: async ({ request }) => { + try { + const formData = await request.formData(); + const idRaw = formData.get('id'); + const nombreRaw = formData.get('nombre'); + + if (!idRaw || !nombreRaw) { + return { success: false, message: 'Parámetros incompletos' }; + } + + const id = Number(idRaw); + const nombre = String(nombreRaw).trim(); + + if (!nombre) { + return { success: false, message: 'El nombre no puede estar vacío' }; + } + + await updateNodeLegalName(id, nombre); + + return { success: true }; + } catch (e: any) { + console.error('Error updating database name:', e); + return { success: false, message: e.message }; + } + }, + + createDatabase: async ({ request }) => { + try { + const formData = await request.formData(); + const nodoSubNodo = String(formData.get('NodoSubNodo') || '').trim(); + const rfc = String(formData.get('RFC') || '').trim(); + const nombre = String(formData.get('Nombre') || '').trim(); + const sucursal = String(formData.get('Sucursal') || '').trim(); + const correo = String(formData.get('CorreoNotificacion') || '').trim(); + const serverName = String(formData.get('ServerName') || '').trim(); + const bdName = String(formData.get('BDName') || '').trim(); + const activo = parseActivoField(formData); + + if (!nodoSubNodo || !rfc || !nombre || !sucursal || !correo || !serverName || !bdName) { + return { success: false, message: 'Todos los campos son requeridos' }; + } + + await insertDatabaseNode({ + nodoSubNodo, + rfc, + nombre, + sucursal, + correo, + serverName, + bdName, + activo + }); + + return { success: true }; + } catch (e: any) { + console.error('Error creating database:', e); + return { success: false, message: e.message }; + } + }, + + updateDatabase: async ({ request }) => { + try { + const formData = await request.formData(); + const id = Number(formData.get('ID')); + const nodoSubNodo = String(formData.get('NodoSubNodo') || '').trim(); + const rfc = String(formData.get('RFC') || '').trim(); + const nombre = String(formData.get('Nombre') || '').trim(); + const sucursal = String(formData.get('Sucursal') || '').trim(); + const correo = String(formData.get('CorreoNotificacion') || '').trim(); + const serverName = String(formData.get('ServerName') || '').trim(); + const bdName = String(formData.get('BDName') || '').trim(); + const activo = parseActivoField(formData); + + if (!id || !nodoSubNodo || !rfc || !nombre || !sucursal || !correo || !serverName || !bdName) { + return { success: false, message: 'Todos los campos son requeridos' }; + } + + await updateDatabaseNode(id, { + nodoSubNodo, + rfc, + nombre, + sucursal, + correo, + serverName, + bdName, + activo + }); + + return { success: true }; + } catch (e: any) { + console.error('Error updating database:', e); + return { success: false, message: e.message }; + } + }, + + deleteDatabase: async ({ request }) => { + try { + const formData = await request.formData(); + const id = Number(formData.get('ID')); + + if (!id) { + return { success: false, message: 'ID requerido' }; + } + + await deleteDatabaseNode(id); + + return { success: true }; + } catch (e: any) { + console.error('Error deleting database:', e); + return { success: false, message: e.message }; + } + }, + + // ---- Acciones para a24c.portal_users (antes CONTROLDESK.dbo.Usuarios) ---- + + createUsuario: async ({ request }) => { + try { + const formData = await request.formData(); + const idNodoSubNodo = Number(formData.get('IDNodoSubNodo')); + const clienteAutoridad = Number(formData.get('ClienteAutoridad') ?? 0); + const nombre = String(formData.get('Nombre') || '').trim(); + const usuario = String(formData.get('Usuario') || '').trim(); + const password = String(formData.get('Password') || ''); + const bdShelter = String(formData.get('BD_Shelter') || '').trim() || null; + + if (!Number.isFinite(idNodoSubNodo) || idNodoSubNodo <= 0 || !nombre || !usuario || !password) { + return { success: false, message: 'Todos los campos obligatorios deben completarse' }; + } + if (!Number.isFinite(clienteAutoridad)) { + return { success: false, message: 'Cliente autoridad inválido' }; + } + + // Hash WinDev-compatible: SHA-512(password + "soluciones"), hex UPPERCASE + const passwordHash = crypto + .createHash('sha512') + .update(password + 'soluciones', 'utf8') + .digest('hex') + .toUpperCase(); + + await insertPortalUser({ + databaseNodeId: idNodoSubNodo, + isAuthorityClient: clienteAutoridad, + fullName: nombre, + username: usuario, + passwordHash, + bdShelter + }); + + return { success: true }; + } catch (e: any) { + console.error('Error creating usuario:', e); + return { success: false, message: e.message }; + } + }, + + updateUsuario: async ({ request }) => { + try { + const formData = await request.formData(); + const id = Number(formData.get('ID')); + const idNodoSubNodo = Number(formData.get('IDNodoSubNodo')); + const clienteAutoridad = Number(formData.get('ClienteAutoridad') ?? 0); + const nombre = String(formData.get('Nombre') || '').trim(); + const usuario = String(formData.get('Usuario') || '').trim(); + const password = String(formData.get('Password') || ''); + const bdShelter = String(formData.get('BD_Shelter') || '').trim() || null; + + if (!id || !Number.isFinite(idNodoSubNodo) || idNodoSubNodo <= 0 || !nombre || !usuario) { + return { success: false, message: 'Todos los campos obligatorios deben completarse' }; + } + if (!Number.isFinite(clienteAutoridad)) { + return { success: false, message: 'Cliente autoridad inválido' }; + } + + if (password) { + const passwordHash = crypto + .createHash('sha512') + .update(password + 'soluciones', 'utf8') + .digest('hex') + .toUpperCase(); + await updatePortalUser(id, { + databaseNodeId: idNodoSubNodo, + isAuthorityClient: clienteAutoridad, + fullName: nombre, + username: usuario, + bdShelter, + passwordHash + }); + } else { + await updatePortalUser(id, { + databaseNodeId: idNodoSubNodo, + isAuthorityClient: clienteAutoridad, + fullName: nombre, + username: usuario, + bdShelter + }); + } + + return { success: true }; + } catch (e: any) { + console.error('Error updating usuario:', e); + return { success: false, message: e.message }; + } + }, + + deleteUsuario: async ({ request }) => { + try { + const formData = await request.formData(); + const id = Number(formData.get('ID')); + + if (!id) { + return { success: false, message: 'ID requerido' }; + } + + await deletePortalUser(id); + + return { success: true }; + } catch (e: any) { + console.error('Error deleting usuario:', e); + return { success: false, message: e.message }; + } + } +}; diff --git a/src/routes/backup/+server.ts b/src/routes/backup/+server.ts index e2d3519..bcba839 100644 --- a/src/routes/backup/+server.ts +++ b/src/routes/backup/+server.ts @@ -1,9 +1,12 @@ import { env } from '$env/dynamic/private'; +import { createReadStream } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; +import { Readable } from 'node:stream'; +import type { RequestHandler } from './$types'; -// GET /backup?file=nombre.bak -> descarga el archivo físico desde BACKUP_PATH -export const GET = async ({ url }: { url: URL }) => { +// GET /backup?file=nombre.bak -> descarga el archivo físico desde BACKUP_PATH (streaming; soporta archivos muy grandes) +export const GET: RequestHandler = async ({ url, request }) => { const fileName = url.searchParams.get('file'); if (!fileName) { return new Response('Missing file parameter', { status: 400 }); @@ -28,14 +31,20 @@ export const GET = async ({ url }: { url: URL }) => { console.error('Backup path is not a regular file:', filePath); return new Response('Backup file not found or inaccessible', { status: 404 }); } - console.log('Serving backup file from', filePath); - const data = await fs.readFile(filePath); + console.log('Serving backup file from', filePath, 'size', st.size); + const nodeStream = createReadStream(filePath, { highWaterMark: 1024 * 1024 }); + request.signal.addEventListener('abort', () => { + nodeStream.destroy(); + }); + const body = Readable.toWeb(nodeStream) as unknown as ReadableStream; const headers = new Headers(); headers.set('Content-Type', 'application/octet-stream'); headers.set('Content-Disposition', `attachment; filename="${fileName}"`); - return new Response(data, { status: 200, headers }); - } catch (e: any) { - console.error('Error serving backup file:', e?.message ?? e); + headers.set('Content-Length', String(st.size)); + return new Response(body, { status: 200, headers }); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + console.error('Error serving backup file:', msg); return new Response('Backup file not found or inaccessible', { status: 404 }); } };