507 lines
15 KiB
JavaScript
507 lines
15 KiB
JavaScript
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();
|