Web app per gestire query automatiche verso database Supabase multipli. Features: - Login con credenziali fisse - Dashboard CRUD per configurazioni database - Scheduler node-cron per esecuzioni automatiche - SQLite per persistenza dati - Log esecuzioni per ogni database Stack: Node.js + Express + SQLite Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
343 lines
9.9 KiB
JavaScript
343 lines
9.9 KiB
JavaScript
require('dotenv').config();
|
|
const express = require('express');
|
|
const cors = require('cors');
|
|
const session = require('express-session');
|
|
const Database = require('better-sqlite3');
|
|
const cron = require('node-cron');
|
|
const path = require('path');
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
// Credenziali fisse
|
|
const AUTH_USERNAME = 'testlab';
|
|
const AUTH_PASSWORD = 'magarifunzione4';
|
|
|
|
// Database SQLite
|
|
const db = new Database('./data/scheduler.db');
|
|
|
|
// Inizializza database
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS databases (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
supabase_url TEXT NOT NULL,
|
|
anon_key TEXT NOT NULL,
|
|
query TEXT NOT NULL,
|
|
frequency_minutes INTEGER NOT NULL DEFAULT 5,
|
|
is_active INTEGER NOT NULL DEFAULT 1,
|
|
last_run TEXT,
|
|
last_status TEXT,
|
|
last_error TEXT,
|
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS execution_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
database_id INTEGER NOT NULL,
|
|
executed_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
status TEXT NOT NULL,
|
|
response TEXT,
|
|
error TEXT,
|
|
duration_ms INTEGER,
|
|
FOREIGN KEY (database_id) REFERENCES databases(id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
app.use(express.static('public'));
|
|
app.use(session({
|
|
secret: process.env.SESSION_SECRET || 'supabase-scheduler-secret-key-2024',
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
secure: false, // true in production con HTTPS
|
|
maxAge: 24 * 60 * 60 * 1000 // 24 ore
|
|
}
|
|
}));
|
|
|
|
// Middleware auth check
|
|
const requireAuth = (req, res, next) => {
|
|
if (req.session && req.session.authenticated) {
|
|
next();
|
|
} else {
|
|
res.status(401).json({ error: 'Non autenticato' });
|
|
}
|
|
};
|
|
|
|
// ============ AUTH ROUTES ============
|
|
|
|
app.post('/api/login', (req, res) => {
|
|
const { username, password } = req.body;
|
|
|
|
if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
|
|
req.session.authenticated = true;
|
|
req.session.username = username;
|
|
res.json({ success: true, message: 'Login effettuato' });
|
|
} else {
|
|
res.status(401).json({ error: 'Credenziali non valide' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/logout', (req, res) => {
|
|
req.session.destroy();
|
|
res.json({ success: true, message: 'Logout effettuato' });
|
|
});
|
|
|
|
app.get('/api/auth/status', (req, res) => {
|
|
res.json({
|
|
authenticated: !!(req.session && req.session.authenticated),
|
|
username: req.session?.username || null
|
|
});
|
|
});
|
|
|
|
// ============ DATABASE CONFIG ROUTES ============
|
|
|
|
app.get('/api/databases', requireAuth, (req, res) => {
|
|
try {
|
|
const databases = db.prepare('SELECT * FROM databases ORDER BY created_at DESC').all();
|
|
res.json(databases);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/databases/:id', requireAuth, (req, res) => {
|
|
try {
|
|
const database = db.prepare('SELECT * FROM databases WHERE id = ?').get(req.params.id);
|
|
if (!database) {
|
|
return res.status(404).json({ error: 'Database non trovato' });
|
|
}
|
|
res.json(database);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/databases', requireAuth, (req, res) => {
|
|
const { name, supabase_url, anon_key, query, frequency_minutes, is_active } = req.body;
|
|
|
|
if (!name || !supabase_url || !anon_key || !query) {
|
|
return res.status(400).json({ error: 'Campi obbligatori mancanti' });
|
|
}
|
|
|
|
try {
|
|
const stmt = db.prepare(`
|
|
INSERT INTO databases (name, supabase_url, anon_key, query, frequency_minutes, is_active)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`);
|
|
const result = stmt.run(name, supabase_url, anon_key, query, frequency_minutes || 5, is_active !== false ? 1 : 0);
|
|
|
|
const newDb = db.prepare('SELECT * FROM databases WHERE id = ?').get(result.lastInsertRowid);
|
|
res.status(201).json(newDb);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.put('/api/databases/:id', requireAuth, (req, res) => {
|
|
const { name, supabase_url, anon_key, query, frequency_minutes, is_active } = req.body;
|
|
|
|
try {
|
|
const existing = db.prepare('SELECT * FROM databases WHERE id = ?').get(req.params.id);
|
|
if (!existing) {
|
|
return res.status(404).json({ error: 'Database non trovato' });
|
|
}
|
|
|
|
const stmt = db.prepare(`
|
|
UPDATE databases
|
|
SET name = ?, supabase_url = ?, anon_key = ?, query = ?,
|
|
frequency_minutes = ?, is_active = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
`);
|
|
stmt.run(
|
|
name || existing.name,
|
|
supabase_url || existing.supabase_url,
|
|
anon_key || existing.anon_key,
|
|
query || existing.query,
|
|
frequency_minutes || existing.frequency_minutes,
|
|
is_active !== undefined ? (is_active ? 1 : 0) : existing.is_active,
|
|
req.params.id
|
|
);
|
|
|
|
const updated = db.prepare('SELECT * FROM databases WHERE id = ?').get(req.params.id);
|
|
res.json(updated);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.delete('/api/databases/:id', requireAuth, (req, res) => {
|
|
try {
|
|
const existing = db.prepare('SELECT * FROM databases WHERE id = ?').get(req.params.id);
|
|
if (!existing) {
|
|
return res.status(404).json({ error: 'Database non trovato' });
|
|
}
|
|
|
|
db.prepare('DELETE FROM databases WHERE id = ?').run(req.params.id);
|
|
res.json({ success: true, message: 'Database eliminato' });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ============ EXECUTION LOGS ============
|
|
|
|
app.get('/api/databases/:id/logs', requireAuth, (req, res) => {
|
|
try {
|
|
const logs = db.prepare(`
|
|
SELECT * FROM execution_logs
|
|
WHERE database_id = ?
|
|
ORDER BY executed_at DESC
|
|
LIMIT 50
|
|
`).all(req.params.id);
|
|
res.json(logs);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ============ MANUAL EXECUTION ============
|
|
|
|
app.post('/api/databases/:id/execute', requireAuth, async (req, res) => {
|
|
try {
|
|
const database = db.prepare('SELECT * FROM databases WHERE id = ?').get(req.params.id);
|
|
if (!database) {
|
|
return res.status(404).json({ error: 'Database non trovato' });
|
|
}
|
|
|
|
const result = await executeQuery(database);
|
|
res.json(result);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ============ SCHEDULER STATUS ============
|
|
|
|
app.get('/api/scheduler/status', requireAuth, (req, res) => {
|
|
const activeCount = db.prepare('SELECT COUNT(*) as count FROM databases WHERE is_active = 1').get();
|
|
const totalCount = db.prepare('SELECT COUNT(*) as count FROM databases').get();
|
|
|
|
res.json({
|
|
running: true,
|
|
active_databases: activeCount.count,
|
|
total_databases: totalCount.count,
|
|
check_interval: '1 minute'
|
|
});
|
|
});
|
|
|
|
// ============ QUERY EXECUTION LOGIC ============
|
|
|
|
async function executeQuery(database) {
|
|
const startTime = Date.now();
|
|
let status = 'success';
|
|
let response = null;
|
|
let error = null;
|
|
|
|
try {
|
|
// Costruisci URL per Supabase REST API
|
|
const url = `${database.supabase_url}/rest/v1/${database.query}`;
|
|
|
|
const fetchResponse = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'apikey': database.anon_key,
|
|
'Authorization': `Bearer ${database.anon_key}`,
|
|
'Content-Type': 'application/json',
|
|
'Prefer': 'return=representation'
|
|
}
|
|
});
|
|
|
|
const data = await fetchResponse.json();
|
|
|
|
if (!fetchResponse.ok) {
|
|
throw new Error(data.message || data.error || `HTTP ${fetchResponse.status}`);
|
|
}
|
|
|
|
response = JSON.stringify(data).substring(0, 5000); // Limita dimensione log
|
|
|
|
} catch (err) {
|
|
status = 'error';
|
|
error = err.message;
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
// Salva log esecuzione
|
|
db.prepare(`
|
|
INSERT INTO execution_logs (database_id, status, response, error, duration_ms)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
`).run(database.id, status, response, error, duration);
|
|
|
|
// Aggiorna stato database
|
|
db.prepare(`
|
|
UPDATE databases
|
|
SET last_run = CURRENT_TIMESTAMP, last_status = ?, last_error = ?
|
|
WHERE id = ?
|
|
`).run(status, error, database.id);
|
|
|
|
return { status, response: response ? JSON.parse(response) : null, error, duration_ms: duration };
|
|
}
|
|
|
|
// ============ SCHEDULER ============
|
|
|
|
// Controlla ogni minuto quali query eseguire
|
|
cron.schedule('* * * * *', async () => {
|
|
const now = new Date();
|
|
console.log(`[${now.toISOString()}] Scheduler check...`);
|
|
|
|
const databases = db.prepare('SELECT * FROM databases WHERE is_active = 1').all();
|
|
|
|
for (const database of databases) {
|
|
const lastRun = database.last_run ? new Date(database.last_run) : null;
|
|
const frequencyMs = database.frequency_minutes * 60 * 1000;
|
|
|
|
// Esegui se mai eseguito o se passato tempo sufficiente
|
|
if (!lastRun || (now - lastRun) >= frequencyMs) {
|
|
console.log(`[${now.toISOString()}] Executing query for: ${database.name}`);
|
|
try {
|
|
await executeQuery(database);
|
|
} catch (err) {
|
|
console.error(`[${now.toISOString()}] Error executing ${database.name}:`, err.message);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log('[SCHEDULER] Cron scheduler avviato - controllo ogni minuto');
|
|
|
|
// ============ HEALTH & INFO ============
|
|
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime()
|
|
});
|
|
});
|
|
|
|
// 404 handler
|
|
app.use((req, res) => {
|
|
// Se richiesta API, ritorna JSON
|
|
if (req.path.startsWith('/api/')) {
|
|
return res.status(404).json({ error: 'Endpoint non trovato' });
|
|
}
|
|
// Altrimenti serve index.html per SPA routing
|
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
});
|
|
|
|
// Error handler
|
|
app.use((err, req, res, next) => {
|
|
console.error(err.stack);
|
|
res.status(500).json({ error: 'Errore interno del server' });
|
|
});
|
|
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`[SERVER] Running on port ${PORT}`);
|
|
console.log(`[SERVER] Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
});
|