import express from 'express'; import cors from 'cors'; import { checkConnection, query, initDb } from './db.js'; import { randomUUID } from 'crypto'; import multer from 'multer'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; const app = express(); const PORT = process.env.PORT || 3000; // Fix per __dirname in ES Modules const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Ensure uploads directory exists const uploadDir = path.join(__dirname, 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } // Multer Configuration const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, uploadDir) }, filename: function (req, file, cb) { const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9) cb(null, uniqueSuffix + '-' + file.originalname) } }) const upload = multer({ storage: storage }); app.use(cors()); app.use(express.json()); // Serve uploaded files statically at /api/uploads to match the frontend request path passed via Nginx app.use('/api/uploads', express.static(uploadDir)); // --- HELPER FUNCTIONS --- const safeJsonParse = (val, fallback) => { if (!val) return fallback; if (typeof val === 'object') return val; // Already parsed by mysql2 try { return JSON.parse(val); } catch (e) { console.warn('JSON Parse error:', e); return fallback; } }; // --- AUTH ENDPOINTS --- app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; try { // 1. Check Agents const agents = await query('SELECT * FROM agents WHERE email = ? AND password = ?', [email, password]); if (agents.length > 0) { const agent = agents[0]; agent.queues = safeJsonParse(agent.queues, []); agent.skills = safeJsonParse(agent.skills, []); agent.avatarConfig = safeJsonParse(agent.avatar_config, {x: 50, y: 50, scale: 1}); return res.json({ user: agent, role: agent.role }); } // 2. Check Clients const clients = await query('SELECT * FROM client_users WHERE email = ? AND password = ?', [email, password]); if (clients.length > 0) { const client = clients[0]; return res.json({ user: client, role: 'client' }); } res.status(401).json({ error: 'Credenziali non valide' }); } catch (e) { console.error(e); res.status(500).json({ error: e.message }); } }); app.post('/api/auth/register', async (req, res) => { const { name, email, password, company } = req.body; const id = randomUUID(); try { await query( 'INSERT INTO client_users (id, name, email, password, company, status) VALUES (?, ?, ?, ?, ?, ?)', [id, name, email, password, company || null, 'active'] ); res.json({ success: true, user: { id, name, email, company, status: 'active' } }); } catch (e) { res.status(500).json({ error: 'Errore registrazione. Email potrebbe essere già in uso.' }); } }); // --- SETTINGS ENDPOINTS --- app.get('/api/settings', async (req, res) => { try { const rows = await query('SELECT * FROM settings WHERE id = 1'); if (rows.length === 0) return res.status(404).json({ error: 'Settings not found' }); const s = rows[0]; const settings = { branding: safeJsonParse(s.branding, {}), smtp: safeJsonParse(s.smtp, {}), emailTemplates: safeJsonParse(s.email_templates, []), features: safeJsonParse(s.features, {}), aiConfig: safeJsonParse(s.ai_config, {}) }; res.json(settings); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/settings', async (req, res) => { const { branding, smtp, emailTemplates, features, aiConfig } = req.body; try { await query( 'UPDATE settings SET branding=?, smtp=?, email_templates=?, features=?, ai_config=? WHERE id=1', [ JSON.stringify(branding), JSON.stringify(smtp), JSON.stringify(emailTemplates), JSON.stringify(features), JSON.stringify(aiConfig) ] ); res.json({ success: true }); } catch (e) { console.error("Error saving settings:", e); res.status(500).json({ error: e.message }); } }); // --- DATA FETCHING ENDPOINTS --- app.get('/api/initial-data', async (req, res) => { try { // Fetch Agents const agents = await query('SELECT * FROM agents'); const parsedAgents = agents.map(a => ({ ...a, queues: safeJsonParse(a.queues, []), skills: safeJsonParse(a.skills, []), avatarConfig: safeJsonParse(a.avatar_config, {x: 50, y: 50, scale: 1}) })); // Fetch Client Users const clientUsers = await query('SELECT * FROM client_users'); // Fetch Queues const queues = await query('SELECT * FROM queues'); // Fetch KB Articles const articles = await query('SELECT * FROM kb_articles ORDER BY last_updated DESC'); // Fetch Surveys const surveys = await query('SELECT * FROM survey_results ORDER BY timestamp DESC'); // Fetch Settings let settings = null; const settingsRows = await query('SELECT * FROM settings WHERE id = 1'); if (settingsRows.length > 0) { const s = settingsRows[0]; settings = { branding: safeJsonParse(s.branding, {}), smtp: safeJsonParse(s.smtp, {}), emailTemplates: safeJsonParse(s.email_templates, []), features: safeJsonParse(s.features, {}), aiConfig: safeJsonParse(s.ai_config, {}) }; } res.json({ agents: parsedAgents, clientUsers, queues, articles, surveys, settings }); } catch (e) { console.error("Initial Data Error:", e); res.status(500).json({ error: e.message }); } }); // --- UPLOAD ENDPOINT --- app.post('/api/upload', upload.single('file'), (req, res) => { if (!req.file) { return res.status(400).json({ error: 'Nessun file caricato' }); } // Construct absolute URL for the file // In prod, this would be S3 url or similar const fileUrl = `/api/uploads/${req.file.filename}`; res.json({ id: req.file.filename, name: req.file.originalname, url: fileUrl, type: req.file.mimetype }); }); // --- TICKET ENDPOINTS --- app.get('/api/tickets', async (req, res) => { try { const sql = ` SELECT t.*, t.assigned_agent_id as assignedAgentId, t.customer_name as customerName, t.created_at as createdAt, t.has_been_analyzed as hasBeenAnalyzed, ( SELECT JSON_ARRAYAGG( JSON_OBJECT( 'id', m.id, 'role', m.role, 'content', m.content, 'timestamp', m.timestamp, 'attachments', m.attachments ) ) FROM ticket_messages m WHERE m.ticket_id = t.id ) as messages FROM tickets t ORDER BY t.created_at DESC `; const rows = await query(sql); const tickets = rows.map(t => ({ ...t, hasBeenAnalyzed: !!t.hasBeenAnalyzed, // Cast to boolean attachments: safeJsonParse(t.attachments, []), messages: safeJsonParse(t.messages, []).map(m => ({ ...m, attachments: safeJsonParse(m.attachments, []) })) })); res.json(tickets); } catch (e) { console.error(e); res.status(500).json({ error: e.message }); } }); app.post('/api/tickets', async (req, res) => { const { subject, description, priority, queue, customerName, attachments } = req.body; const id = `T-${Date.now()}`; try { await query( 'INSERT INTO tickets (id, subject, description, status, priority, customer_name, queue, attachments, has_been_analyzed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [id, subject, description, 'APERTO', priority, customerName, queue, JSON.stringify(attachments || []), false] ); const newTicket = { id, subject, description, status: 'APERTO', priority, customerName, queue, attachments: attachments || [], messages: [], hasBeenAnalyzed: false, createdAt: new Date().toISOString() }; res.json(newTicket); } catch (e) { console.error(e); res.status(500).json({ error: e.message }); } }); app.patch('/api/tickets/:id', async (req, res) => { const { id } = req.params; const updates = req.body; const fields = []; const values = []; if (updates.status) { fields.push('status = ?'); values.push(updates.status); } if (updates.priority) { fields.push('priority = ?'); values.push(updates.priority); } if (updates.assignedAgentId !== undefined) { fields.push('assigned_agent_id = ?'); values.push(updates.assignedAgentId || null); } if (updates.queue) { fields.push('queue = ?'); values.push(updates.queue); } if (updates.hasBeenAnalyzed !== undefined) { fields.push('has_been_analyzed = ?'); values.push(updates.hasBeenAnalyzed); } if (fields.length === 0) return res.json({ success: true }); values.push(id); try { await query(`UPDATE tickets SET ${fields.join(', ')} WHERE id = ?`, values); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/tickets/mark-analyzed', async (req, res) => { const { ticketIds } = req.body; if (!Array.isArray(ticketIds) || ticketIds.length === 0) { return res.json({ success: true, count: 0 }); } try { // Safe parameter expansion const placeholders = ticketIds.map(() => '?').join(','); await query(`UPDATE tickets SET has_been_analyzed = TRUE WHERE id IN (${placeholders})`, ticketIds); res.json({ success: true, count: ticketIds.length }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/tickets/:id/messages', async (req, res) => { const { id } = req.params; const { role, content, attachments } = req.body; const messageId = `m-${Date.now()}`; try { await query( 'INSERT INTO ticket_messages (id, ticket_id, role, content, attachments) VALUES (?, ?, ?, ?, ?)', [messageId, id, role, content, JSON.stringify(attachments || [])] ); if (role === 'user') { await query("UPDATE tickets SET status = 'APERTO' WHERE id = ? AND status = 'RISOLTO'", [id]); } res.json({ id: messageId, role, content, attachments: attachments || [], timestamp: new Date().toISOString() }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- MANAGEMENT ENDPOINTS (Agents, Users, Queues) --- app.post('/api/agents', async (req, res) => { const agent = req.body; try { await query( 'INSERT INTO agents (id, name, email, password, role, avatar, avatar_config, skills, queues) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [ agent.id, agent.name, agent.email, agent.password, agent.role, agent.avatar, JSON.stringify(agent.avatarConfig || {}), JSON.stringify(agent.skills || []), JSON.stringify(agent.queues || []) ] ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/agents/:id', async (req, res) => { const { id } = req.params; const agent = req.body; try { await query( 'UPDATE agents SET name=?, email=?, password=?, role=?, avatar=?, avatar_config=?, skills=?, queues=? WHERE id=?', [ agent.name, agent.email, agent.password, agent.role, agent.avatar, JSON.stringify(agent.avatarConfig), JSON.stringify(agent.skills), JSON.stringify(agent.queues), id ] ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/agents/:id', async (req, res) => { try { await query('DELETE FROM agents WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/client_users/:id', async (req, res) => { const { id } = req.params; const { name, email, company, status } = req.body; try { await query( 'UPDATE client_users SET name=?, email=?, company=?, status=? WHERE id=?', [name, email, company, status, id] ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/client_users/:id', async (req, res) => { try { await query('DELETE FROM client_users WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/queues', async (req, res) => { const { id, name, description } = req.body; try { await query('INSERT INTO queues (id, name, description) VALUES (?, ?, ?)', [id, name, description]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/queues/:id', async (req, res) => { try { await query('DELETE FROM queues WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- SURVEY & KB ENDPOINTS --- app.post('/api/surveys', async (req, res) => { const { rating, comment, source, referenceId } = req.body; const id = randomUUID(); try { await query( 'INSERT INTO survey_results (id, rating, comment, source, reference_id) VALUES (?, ?, ?, ?, ?)', [id, rating, comment || null, source, referenceId || null] ); res.json({ success: true, id }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/articles', async (req, res) => { const { title, content, category, type, url, source, visibility } = req.body; const id = `kb-${Date.now()}`; try { await query( 'INSERT INTO kb_articles (id, title, content, category, type, url, source, visibility) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [id, title, content, category, type, url || null, source || 'manual', visibility || 'public'] ); res.json({ success: true, id, lastUpdated: new Date().toISOString() }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.patch('/api/articles/:id', async (req, res) => { const { id } = req.params; const { title, content, category, type, url, visibility } = req.body; try { await query( 'UPDATE kb_articles SET title=?, content=?, category=?, type=?, url=?, visibility=? WHERE id=?', [title, content, category, type, url || null, visibility || 'public', id] ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/articles/:id', async (req, res) => { try { await query('DELETE FROM kb_articles WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); const startServer = async () => { await initDb(); app.listen(PORT, () => { console.log(`🚀 Backend Server running on port ${PORT}`); checkConnection(); }); }; startServer();