diff --git a/.env b/.env index 5083258..5519a30 100644 --- a/.env +++ b/.env @@ -8,3 +8,9 @@ VITE_APP_TITLE=ADVICOM SQL Manager VITE_USER_ID=1 # Privilegios: READ (solo lectura) | WRITE (permite INSERT, UPDATE, DELETE) VITE_USER_PRIVILEGIO=READ + +# Configuración FTP para Respaldos +VITE_FTP_HOST=ftp://134.122.126.66:21 +VITE_FTP_USER=mysql_inyect +VITE_FTP_PASS=YTJjrRdnCPwjTSrN +VITE_FTP_PATH=/carpeta_respaldos/ \ No newline at end of file diff --git a/.env.example b/.env.example index e4c1257..85dd1c8 100644 --- a/.env.example +++ b/.env.example @@ -8,3 +8,9 @@ VITE_APP_TITLE=Remote SQL Admin VITE_USER_ID=1 # Privilegios: READ (solo lectura) | WRITE (permite INSERT, UPDATE, DELETE) VITE_USER_PRIVILEGIO=READ + +# Configuración FTP para Respaldos +VITE_FTP_HOST=ftp.tuservidor.com +VITE_FTP_USER=usuario_ftp +VITE_FTP_PASS=clave_ftp +VITE_FTP_PATH=/carpeta_respaldos/ diff --git a/docker-compose.yml b/docker-compose.yml index 07e4395..f11e3aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: context: . dockerfile: Dockerfile args: - - VITE_API_URL=https://ws-sql-inyect.sial.cl:3120/v1 + - VITE_API_URL=https://ws-sql-inyect.sial.cl/v1 - VITE_APP_TITLE=ADVICOM SQL Manager - VITE_USER_ID=1 - VITE_USER_PRIVILEGIO=READ diff --git a/src/App.jsx b/src/App.jsx index 4c53f36..b65e5c7 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from 'react'; -import { Database, Settings, Menu, Loader2, LogOut, User as UserIcon } from 'lucide-react'; +import { Database, Settings, Menu, Loader2, LogOut, User as UserIcon, Download } from 'lucide-react'; import StoreSelector from './components/StoreSelector'; import SqlEditor from './components/SqlEditor'; import ResultTable from './components/ResultTable'; import CommandHistory from './components/CommandHistory'; import DatabaseSelector from './components/DatabaseSelector'; import Login from './components/Login'; +import BackupModal from './components/BackupModal'; import { commandService, authService } from './services/api'; import { injectDatabaseToSql } from './utils/sqlValidator'; @@ -21,6 +22,7 @@ function App() { const [selectedDatabase, setSelectedDatabase] = useState(null); const [sidebarOpen, setSidebarOpen] = useState(true); const [selectedClient, setSelectedClient] = useState('2'); // Peru is 2, Colombia is 3 + const [isBackupModalOpen, setIsBackupModalOpen] = useState(false); useEffect(() => { // Check if user is already logged in @@ -149,6 +151,84 @@ function App() { }); } }; + const handleBackupRequest = async (backupData) => { + if (!selectedStore) return; + + setIsBackupModalOpen(false); + setExecutionStatus('running'); + setCurrentResult(null); + setPollingProgress(null); + + try { + const now = new Date(); + const dateStr = now.toISOString().split('T')[0]; + const timeStr = now.getTime().toString().slice(-3); + const commandId = `BK-${dateStr}-${timeStr}`; + + const backupSql = `BACKUP ${JSON.stringify({ + database: backupData.database, + ftp: { + host: backupData.host, + user: backupData.user, + pass: backupData.pass, + path: backupData.path + } + })}`; + + // Step 1: Create the command + const createResponse = await commandService.create({ + store_id: parseInt(selectedStore.id, 10), + sql: backupSql, + privilegio: 'CRUD', // As per requirements + id_user: currentUser.id, + id_cliente: parseInt(selectedClient, 10), + command_id: commandId, + timeout: 600 + }); + + if (!createResponse.success) { + setExecutionStatus('error'); + setCurrentResult({ error: createResponse.message || 'Error al solicitar respaldo' }); + return; + } + + // Update status to polling + setExecutionStatus('polling'); + setCurrentResult({ + message: 'Solicitud de respaldo enviada. El agente está procesando...', + command_id: commandId, + stats: { info: `Backup #${commandId} iniciado. Esperando compresión y envío...` } + }); + + // Step 2: Poll for results + const result = await commandService.pollForResult(commandId, { + interval: 5000, // Poll every 5 seconds for backups + maxAttempts: 120, // Wait up to 10 minutes (600 seconds) + onProgress: ({ attempts, maxAttempts, status }) => { + setPollingProgress({ attempts, maxAttempts, status }); + setCurrentResult(prev => ({ + ...prev, + message: `Procesando respaldo remoto... (${attempts}/${maxAttempts})`, + stats: { info: `Backup #${commandId} - Intento ${attempts} de ${maxAttempts}` } + })); + } + }); + + setExecutionStatus('completed'); + setPollingProgress(null); + setCurrentResult(normalizeApiResponse(result, commandId)); + + } catch (error) { + console.error(error); + setExecutionStatus('error'); + setPollingProgress(null); + setCurrentResult({ + error: error.message || "Error al procesar el respaldo", + stats: { info: 'Error durante la ejecución del backup' } + }); + } + }; + /** * Normalizes different API response formats into a standard structure for ResultTable @@ -174,6 +254,17 @@ function App() { // Check for nested result object (main format) or json_response (history format) const dataContainer = result.result || result.json_response; + // Check for download URL in dataContainer + if (dataContainer && (dataContainer.download_url || dataContainer.url || dataContainer.file_url)) { + normalizedResult.downloadUrl = dataContainer.download_url || dataContainer.url || dataContainer.file_url; + normalizedResult.message = '¡Respaldo listo para descargar!'; + } + // Fallback search in the whole result object + else if (result.download_url || result.url || result.file_url) { + normalizedResult.downloadUrl = result.download_url || result.url || result.file_url; + normalizedResult.message = '¡Respaldo listo para descargar!'; + } + if (dataContainer) { // Get execution time if (dataContainer.execution_time_ms) { @@ -330,6 +421,16 @@ function App() {
+ {selectedStore && ( + + )}
+ setIsBackupModalOpen(false)} + store={selectedStore} + currentDatabase={selectedDatabase} + onConfirm={handleBackupRequest} + /> + +
+ ); +} diff --git a/src/components/ResultTable.jsx b/src/components/ResultTable.jsx index a472982..284f0c7 100644 --- a/src/components/ResultTable.jsx +++ b/src/components/ResultTable.jsx @@ -186,7 +186,29 @@ export default function ResultTable({ result, loading, pollingProgress }) {

{result.message || 'Consulta ejecutada correctamente'}

- {result.command_id && ( + {result.downloadUrl && ( +
+ + + Descargar Respaldo Ahora + +

+ El archivo se encuentra disponible en el servidor FTP especificado. +

+
+ )} + {result.command_id && !result.downloadUrl && (

Command ID: {result.command_id}

diff --git a/src/services/api.js b/src/services/api.js index 5e5dfd4..b3b534c 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1,6 +1,6 @@ import axios from 'axios'; -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; +const API_URL = import.meta.env.VITE_API_URL || 'https://ws-sql-inyect.sial.cl/v1'; const api = axios.create({ baseURL: API_URL, @@ -71,7 +71,7 @@ export const commandService = { * @param {Number} [params.id_user] - User ID requesting the command (Optional) * @returns {Promise<{success: boolean, message: string, command_id: number}>} */ - create: ({ store_id, sql, privilegio = 'READ', id_user, id_cliente }) => { + create: ({ store_id, sql, privilegio = 'READ', id_user, id_cliente, command_id, timeout }) => { const payload = { store_id, sql, privilegio }; if (id_user !== undefined) { payload.id_user = id_user; @@ -79,6 +79,12 @@ export const commandService = { if (id_cliente !== undefined) { payload.id_cliente = id_cliente; } + if (command_id !== undefined) { + payload.command_id = command_id; + } + if (timeout !== undefined) { + payload.timeout = timeout; + } return api.post('/commands', payload); }, diff --git a/src/utils/sqlValidator.js b/src/utils/sqlValidator.js index a683433..38dc4b2 100644 --- a/src/utils/sqlValidator.js +++ b/src/utils/sqlValidator.js @@ -6,7 +6,7 @@ */ // Base allowed SQL commands (always allowed) -const BASE_ALLOWED_COMMANDS = ['SELECT', 'SHOW', 'DESCRIBE', 'DESC', 'EXPLAIN']; +const BASE_ALLOWED_COMMANDS = ['SELECT', 'SHOW', 'DESCRIBE', 'DESC', 'EXPLAIN', 'BACKUP']; // Write commands (only allowed with WRITE privilege) const WRITE_COMMANDS = ['DELETE', 'UPDATE', 'INSERT']; @@ -127,6 +127,11 @@ export function injectDatabaseToSql(sql, dbName) { // Regex para encontrar tablas después de palabras clave comunes // Busca: FROM table, JOIN table, UPDATE table, INTO table, DESCRIBE table, etc. // Solo actúa si el nombre de la tabla NO contiene ya un punto (.) + // No aplica para comandos BACKUP que usan formato JSON + if (sql.trim().toUpperCase().startsWith('BACKUP')) { + return sql; + } + const tableKeywords = ['FROM', 'JOIN', 'UPDATE', 'INTO', 'DESCRIBE', 'DESC', 'TABLE']; let transformedSql = sql;