Cambios para manejar respaldo
This commit is contained in:
6
.env
6
.env
@@ -8,3 +8,9 @@ VITE_APP_TITLE=ADVICOM SQL Manager
|
|||||||
VITE_USER_ID=1
|
VITE_USER_ID=1
|
||||||
# Privilegios: READ (solo lectura) | WRITE (permite INSERT, UPDATE, DELETE)
|
# Privilegios: READ (solo lectura) | WRITE (permite INSERT, UPDATE, DELETE)
|
||||||
VITE_USER_PRIVILEGIO=READ
|
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/
|
||||||
@@ -8,3 +8,9 @@ VITE_APP_TITLE=Remote SQL Admin
|
|||||||
VITE_USER_ID=1
|
VITE_USER_ID=1
|
||||||
# Privilegios: READ (solo lectura) | WRITE (permite INSERT, UPDATE, DELETE)
|
# Privilegios: READ (solo lectura) | WRITE (permite INSERT, UPDATE, DELETE)
|
||||||
VITE_USER_PRIVILEGIO=READ
|
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/
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
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_APP_TITLE=ADVICOM SQL Manager
|
||||||
- VITE_USER_ID=1
|
- VITE_USER_ID=1
|
||||||
- VITE_USER_PRIVILEGIO=READ
|
- VITE_USER_PRIVILEGIO=READ
|
||||||
|
|||||||
111
src/App.jsx
111
src/App.jsx
@@ -1,11 +1,12 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 StoreSelector from './components/StoreSelector';
|
||||||
import SqlEditor from './components/SqlEditor';
|
import SqlEditor from './components/SqlEditor';
|
||||||
import ResultTable from './components/ResultTable';
|
import ResultTable from './components/ResultTable';
|
||||||
import CommandHistory from './components/CommandHistory';
|
import CommandHistory from './components/CommandHistory';
|
||||||
import DatabaseSelector from './components/DatabaseSelector';
|
import DatabaseSelector from './components/DatabaseSelector';
|
||||||
import Login from './components/Login';
|
import Login from './components/Login';
|
||||||
|
import BackupModal from './components/BackupModal';
|
||||||
import { commandService, authService } from './services/api';
|
import { commandService, authService } from './services/api';
|
||||||
import { injectDatabaseToSql } from './utils/sqlValidator';
|
import { injectDatabaseToSql } from './utils/sqlValidator';
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ function App() {
|
|||||||
const [selectedDatabase, setSelectedDatabase] = useState(null);
|
const [selectedDatabase, setSelectedDatabase] = useState(null);
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [selectedClient, setSelectedClient] = useState('2'); // Peru is 2, Colombia is 3
|
const [selectedClient, setSelectedClient] = useState('2'); // Peru is 2, Colombia is 3
|
||||||
|
const [isBackupModalOpen, setIsBackupModalOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check if user is already logged in
|
// 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
|
* 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)
|
// Check for nested result object (main format) or json_response (history format)
|
||||||
const dataContainer = result.result || result.json_response;
|
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) {
|
if (dataContainer) {
|
||||||
// Get execution time
|
// Get execution time
|
||||||
if (dataContainer.execution_time_ms) {
|
if (dataContainer.execution_time_ms) {
|
||||||
@@ -330,6 +421,16 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
{selectedStore && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsBackupModalOpen(true)}
|
||||||
|
className="btn-primary"
|
||||||
|
style={{ padding: '0.375rem 0.75rem', fontSize: '0.75rem', backgroundColor: 'var(--warning)' }}
|
||||||
|
title="Solicitar Respaldo de Base de Datos"
|
||||||
|
>
|
||||||
|
<Download size={14} /> Respaldo DB
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div className="flex items-center gap-2 pr-4" style={{ borderRight: '1px solid var(--border)' }}>
|
<div className="flex items-center gap-2 pr-4" style={{ borderRight: '1px solid var(--border)' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '32px', height: '32px',
|
width: '32px', height: '32px',
|
||||||
@@ -379,6 +480,14 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<BackupModal
|
||||||
|
isOpen={isBackupModalOpen}
|
||||||
|
onClose={() => setIsBackupModalOpen(false)}
|
||||||
|
store={selectedStore}
|
||||||
|
currentDatabase={selectedDatabase}
|
||||||
|
onConfirm={handleBackupRequest}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
.animate-spin {
|
.animate-spin {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
|
|||||||
288
src/components/BackupModal.jsx
Normal file
288
src/components/BackupModal.jsx
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { X, Shield, Server, Database, Folder, Key, User, Download, AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function BackupModal({ isOpen, onClose, store, currentDatabase, onConfirm }) {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
database: currentDatabase || '',
|
||||||
|
host: import.meta.env.VITE_FTP_HOST || '',
|
||||||
|
user: import.meta.env.VITE_FTP_USER || '',
|
||||||
|
pass: import.meta.env.VITE_FTP_PASS || '',
|
||||||
|
path: import.meta.env.VITE_FTP_PATH || '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentDatabase) {
|
||||||
|
setFormData(prev => ({ ...prev, database: currentDatabase }));
|
||||||
|
}
|
||||||
|
}, [currentDatabase]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onConfirm(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bk-modal-overlay">
|
||||||
|
<div className="bk-modal-content">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bk-modal-header">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Download size={20} style={{ color: 'var(--text-accent)' }} />
|
||||||
|
<h2 style={{ margin: 0, fontSize: '1rem', color: '#fff', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||||
|
Solicitar Respaldo DB
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="bk-close-btn">
|
||||||
|
<X size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<form onSubmit={handleSubmit} className="bk-modal-body">
|
||||||
|
<div className="bk-info-banner">
|
||||||
|
<AlertCircle size={18} style={{ color: 'var(--warning)', flexShrink: 0 }} />
|
||||||
|
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', lineHeight: '1.5' }}>
|
||||||
|
Se enviará un comando <code style={{ color: 'var(--text-accent)', background: 'rgba(0,0,0,0.3)', padding: '2px 4px', borderRadius: '4px' }}>BACKUP</code> al agente remoto de <b style={{ color: '#fff' }}>{store?.name}</b>.
|
||||||
|
El proceso puede tardar varios minutos.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bk-form-grid">
|
||||||
|
{/* Database Name */}
|
||||||
|
<div className="bk-input-group full-width">
|
||||||
|
<label><Database size={12} /> Base de Datos</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.database}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, database: e.target.value }))}
|
||||||
|
placeholder="nombre_bd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FTP Host */}
|
||||||
|
<div className="bk-input-group">
|
||||||
|
<label><Server size={12} /> Host FTP</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.host}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, host: e.target.value }))}
|
||||||
|
placeholder="ftp.servidor.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FTP User */}
|
||||||
|
<div className="bk-input-group">
|
||||||
|
<label><User size={12} /> Usuario FTP</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.user}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, user: e.target.value }))}
|
||||||
|
placeholder="usuario"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FTP Password */}
|
||||||
|
<div className="bk-input-group">
|
||||||
|
<label><Key size={12} /> Contraseña FTP</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={formData.pass}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, pass: e.target.value }))}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FTP Path */}
|
||||||
|
<div className="bk-input-group">
|
||||||
|
<label><Folder size={12} /> Ruta Destino</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={formData.path}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, path: e.target.value }))}
|
||||||
|
placeholder="/respaldos/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bk-modal-footer">
|
||||||
|
<button type="button" onClick={onClose} className="bk-btn-secondary">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="bk-btn-primary">
|
||||||
|
<Shield size={18} /> Iniciar Respaldo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
.bk-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-modal-content {
|
||||||
|
background-color: var(--bg-panel);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
animation: modalEntry 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalEntry {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-modal-header {
|
||||||
|
background-color: var(--bg-input);
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-close-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-close-btn:hover {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-info-banner {
|
||||||
|
background-color: rgba(245, 158, 11, 0.1);
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.2);
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-input-group.full-width {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-input-group label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-input-group input {
|
||||||
|
background-color: var(--bg-input);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.625rem 0.875rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-input-group input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-btn-secondary {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-btn-secondary:hover {
|
||||||
|
background-color: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-btn-primary {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: var(--accent);
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bk-btn-primary:hover {
|
||||||
|
background-color: var(--accent-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -186,7 +186,29 @@ export default function ResultTable({ result, loading, pollingProgress }) {
|
|||||||
<h3 style={{ color: 'var(--text-primary)', fontWeight: 600, marginBottom: '0.5rem' }}>
|
<h3 style={{ color: 'var(--text-primary)', fontWeight: 600, marginBottom: '0.5rem' }}>
|
||||||
{result.message || 'Consulta ejecutada correctamente'}
|
{result.message || 'Consulta ejecutada correctamente'}
|
||||||
</h3>
|
</h3>
|
||||||
{result.command_id && (
|
{result.downloadUrl && (
|
||||||
|
<div style={{ marginTop: '1.5rem', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
|
||||||
|
<a
|
||||||
|
href={result.downloadUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-2 px-8 py-3 rounded-lg font-bold transition-all hover:scale-105 shadow-lg shadow-blue-500/20"
|
||||||
|
style={{
|
||||||
|
background: 'var(--accent)',
|
||||||
|
color: 'white',
|
||||||
|
textDecoration: 'none',
|
||||||
|
fontSize: '1rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download size={20} />
|
||||||
|
Descargar Respaldo Ahora
|
||||||
|
</a>
|
||||||
|
<p className="text-xs" style={{ color: 'var(--text-muted)' }}>
|
||||||
|
El archivo se encuentra disponible en el servidor FTP especificado.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{result.command_id && !result.downloadUrl && (
|
||||||
<p className="font-mono text-xs flex items-center gap-1" style={{ color: 'var(--text-accent)' }}>
|
<p className="font-mono text-xs flex items-center gap-1" style={{ color: 'var(--text-accent)' }}>
|
||||||
<Hash size={12} /> Command ID: {result.command_id}
|
<Hash size={12} /> Command ID: {result.command_id}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios';
|
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({
|
const api = axios.create({
|
||||||
baseURL: API_URL,
|
baseURL: API_URL,
|
||||||
@@ -71,7 +71,7 @@ export const commandService = {
|
|||||||
* @param {Number} [params.id_user] - User ID requesting the command (Optional)
|
* @param {Number} [params.id_user] - User ID requesting the command (Optional)
|
||||||
* @returns {Promise<{success: boolean, message: string, command_id: number}>}
|
* @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 };
|
const payload = { store_id, sql, privilegio };
|
||||||
if (id_user !== undefined) {
|
if (id_user !== undefined) {
|
||||||
payload.id_user = id_user;
|
payload.id_user = id_user;
|
||||||
@@ -79,6 +79,12 @@ export const commandService = {
|
|||||||
if (id_cliente !== undefined) {
|
if (id_cliente !== undefined) {
|
||||||
payload.id_cliente = id_cliente;
|
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);
|
return api.post('/commands', payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Base allowed SQL commands (always allowed)
|
// 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)
|
// Write commands (only allowed with WRITE privilege)
|
||||||
const WRITE_COMMANDS = ['DELETE', 'UPDATE', 'INSERT'];
|
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
|
// Regex para encontrar tablas después de palabras clave comunes
|
||||||
// Busca: FROM table, JOIN table, UPDATE table, INTO table, DESCRIBE table, etc.
|
// 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 (.)
|
// 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'];
|
const tableKeywords = ['FROM', 'JOIN', 'UPDATE', 'INTO', 'DESCRIBE', 'DESC', 'TABLE'];
|
||||||
let transformedSql = sql;
|
let transformedSql = sql;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user