feat: ora è possibile scorrere gli allegati e modificare i pdf, aggiunte personalizzazioni di immagini e forme per poterle richiamare più rapidamente.

This commit is contained in:
Gabriele Cimaschi committed 2026-09-09 17:03:41 +02:00
1 parent ec1fb25d4e
commit 719fc09c66
8 files changed
+2168 -71

No files matched your search

+178 -2
View File
@@ -146,8 +146,8 @@ router.post('/env/restore/:history_id', async (req, res) => {
}
});
// DELETE /api/config/env/history/:id — Delete a specific .env history entry
router.delete('/env/history/:id', async (req, res) => {
// Helper handler for deleting a specific .env history entry
async function handleDeleteHistory(req, res) {
try {
const { id } = req.params;
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
@@ -181,6 +181,182 @@ router.delete('/env/history/:id', async (req, res) => {
console.error('Error deleting .env history entry:', err);
res.status(500).json({ error: 'Errore durante l\'eliminazione della versione dello storico', message: err.message });
}
}
// DELETE & POST /api/config/env/history/:id — Delete a specific .env history entry
router.delete('/env/history/:id', handleDeleteHistory);
router.post('/env/history/:id/delete', handleDeleteHistory);
// GET /api/config/user-signatures — Fetch all user signature images
router.get('/user-signatures', (req, res) => {
try {
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
const signatures = db.prepare(`
SELECT * FROM user_signatures WHERE agent_id = ? ORDER BY id DESC
`).all(operatorId);
res.json(signatures);
} catch (err) {
console.error('Error fetching user signatures:', err);
res.status(500).json({ error: 'Errore durante il recupero delle firme utente', message: err.message });
}
});
// POST /api/config/user-signatures — Add new user signature image
router.post('/user-signatures', (req, res) => {
try {
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
const { name, image_data } = req.body;
if (!name || !image_data) {
return res.status(400).json({ error: 'Nome e dati immagine sono obbligatori' });
}
const info = db.prepare(`
INSERT INTO user_signatures (agent_id, name, image_data)
VALUES (?, ?, ?)
`).run(operatorId, name.trim(), image_data);
const signatures = db.prepare(`
SELECT * FROM user_signatures WHERE agent_id = ? ORDER BY id DESC
`).all(operatorId);
res.json({
success: true,
message: 'Immagine/Firma salvata nel database con successo!',
id: info.lastInsertRowid,
signatures
});
} catch (err) {
console.error('Error saving user signature:', err);
res.status(500).json({ error: 'Errore durante il salvataggio della firma', message: err.message });
}
});
// DELETE /api/config/user-signatures/:id — Delete user signature image
router.delete('/user-signatures/:id', (req, res) => {
try {
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
const { id } = req.params;
db.prepare(`DELETE FROM user_signatures WHERE id = ? AND agent_id = ?`).run(id, operatorId);
const signatures = db.prepare(`
SELECT * FROM user_signatures WHERE agent_id = ? ORDER BY id DESC
`).all(operatorId);
res.json({
success: true,
message: 'Firma eliminata con successo!',
signatures
});
} catch (err) {
console.error('Error deleting user signature:', err);
res.status(500).json({ error: 'Errore durante l\'eliminazione della firma', message: err.message });
}
});
// GET /api/config/user-shapes — List custom user shapes
router.get('/user-shapes', (req, res) => {
try {
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
const shapes = db.prepare(`
SELECT * FROM user_shapes WHERE agent_id = ? ORDER BY id DESC
`).all(operatorId);
res.json(shapes);
} catch (err) {
console.error('Error fetching user shapes:', err);
res.status(500).json({ error: 'Errore durante il recupero delle forme utente', message: err.message });
}
});
// POST /api/config/user-shapes — Create a new custom shape
router.post('/user-shapes', (req, res) => {
try {
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
const { name, shape_type, fill_color, border_color, border_width, is_favorite } = req.body;
if (!name) {
return res.status(400).json({ error: 'Il nome della forma è obbligatorio' });
}
const info = db.prepare(`
INSERT INTO user_shapes (agent_id, name, shape_type, fill_color, border_color, border_width, is_favorite)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
operatorId,
name.trim(),
shape_type || 'rectangle',
fill_color || '#3b82f6',
border_color || '#1e3a8a',
parseInt(border_width || 3, 10),
is_favorite !== undefined ? (is_favorite ? 1 : 0) : 1
);
const shapes = db.prepare(`
SELECT * FROM user_shapes WHERE agent_id = ? ORDER BY id DESC
`).all(operatorId);
res.json({
success: true,
message: 'Forma salvata nei preferiti con successo!',
id: info.lastInsertRowid,
shapes
});
} catch (err) {
console.error('Error saving user shape:', err);
res.status(500).json({ error: 'Errore durante il salvataggio della forma', message: err.message });
}
});
// PUT /api/config/user-shapes/:id/toggle-favorite — Toggle favorite status
router.put('/user-shapes/:id/toggle-favorite', (req, res) => {
try {
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
const { id } = req.params;
const shape = db.prepare(`SELECT * FROM user_shapes WHERE id = ? AND agent_id = ?`).get(id, operatorId);
if (!shape) {
return res.status(404).json({ error: 'Forma non trovata' });
}
const newFav = shape.is_favorite ? 0 : 1;
db.prepare(`UPDATE user_shapes SET is_favorite = ? WHERE id = ? AND agent_id = ?`).run(newFav, id, operatorId);
const shapes = db.prepare(`
SELECT * FROM user_shapes WHERE agent_id = ? ORDER BY id DESC
`).all(operatorId);
res.json({
success: true,
shapes
});
} catch (err) {
console.error('Error toggling shape favorite status:', err);
res.status(500).json({ error: 'Errore durante l\'aggiornamento dei preferiti', message: err.message });
}
});
// DELETE /api/config/user-shapes/:id — Delete a custom shape
router.delete('/user-shapes/:id', (req, res) => {
try {
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
const { id } = req.params;
db.prepare(`DELETE FROM user_shapes WHERE id = ? AND agent_id = ?`).run(id, operatorId);
const shapes = db.prepare(`
SELECT * FROM user_shapes WHERE agent_id = ? ORDER BY id DESC
`).all(operatorId);
res.json({
success: true,
message: 'Forma eliminata con successo!',
shapes
});
} catch (err) {
console.error('Error deleting user shape:', err);
res.status(500).json({ error: 'Errore durante l\'eliminazione della forma', message: err.message });
}
});
module.exports = router;
+111 -33
View File
@@ -457,27 +457,75 @@ router.get('/:id', async (req, res) => {
[id]
);
const iconv = require('iconv-lite');
const decodeTextContent = (rawContent, contentTypeStr = '') => {
if (!rawContent) return '';
let rawBuf = null;
if (Buffer.isBuffer(rawContent)) {
rawBuf = rawContent;
} else if (typeof rawContent === 'string') {
const cleaned = rawContent.replace(/\s+/g, '');
if (cleaned.length > 20 && cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(cleaned)) {
try {
rawBuf = Buffer.from(cleaned, 'base64');
} catch (_) {
rawBuf = Buffer.from(rawContent, 'utf-8');
}
} else {
rawBuf = Buffer.from(rawContent, 'utf-8');
}
} else {
return String(rawContent);
}
// Detect charset from content-type header
let charset = 'utf-8';
const charsetMatch = (contentTypeStr || '').match(/charset=["']?([^"';\s]+)/i);
if (charsetMatch && charsetMatch[1]) {
charset = charsetMatch[1].toLowerCase();
}
let text = '';
try {
if (iconv.encodingExists(charset)) {
text = iconv.decode(rawBuf, charset);
} else {
text = rawBuf.toString('utf-8');
}
} catch (_) {
text = rawBuf.toString('utf-8');
}
// If UTF-8 decoding contains replacement character U+FFFD (), retry decoding as win1252/latin1
if (text.includes('\uFFFD') || text.includes('')) {
try {
const latin1Text = iconv.decode(rawBuf, 'windows-1252');
if (latin1Text && !latin1Text.includes('\uFFFD')) {
text = latin1Text;
}
} catch (_) {}
}
// Repair Double-UTF-8 / Mojibake (e.g., ’ -> ’, ù -> ù, è -> è, à -> à, à -> È)
if (/(?:Ã[\s\xa0¡-ÿ]|â€[™œ]|–)/.test(text)) {
try {
const reEncoded = iconv.encode(text, 'win1252');
const repaired = iconv.decode(reEncoded, 'utf-8');
if (repaired && !repaired.includes('\uFFFD')) {
text = repaired;
}
} catch (_) {}
}
return text;
};
const htmlBodyMap = {};
for (const row of htmlAttachmentsRes.rows) {
if (row.content) {
let contentStr = '';
if (Buffer.isBuffer(row.content)) {
contentStr = row.content.toString('utf-8');
} else if (typeof row.content === 'string') {
contentStr = row.content;
}
const cleaned = contentStr.replace(/\s+/g, '');
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (cleaned.length > 20 && cleaned.length % 4 === 0 && base64Regex.test(cleaned)) {
try {
const decoded = Buffer.from(cleaned, 'base64').toString('utf-8');
if (decoded && /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(decoded)) {
contentStr = decoded;
}
} catch (e) {}
}
const contentStr = decodeTextContent(row.content, row.content_type);
if (contentStr && /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(contentStr)) {
const fname = (row.filename || '').toLowerCase();
if (!htmlBodyMap[row.article_id] || fname.startsWith('file-1') || fname.startsWith('file-2')) {
@@ -488,6 +536,14 @@ router.get('/:id', async (req, res) => {
}
articlesResult.rows.forEach(a => {
// Ensure a.a_body and subject are properly decoded if legacy encoded
if (a.a_body) {
a.a_body = decodeTextContent(a.a_body, a.a_content_type);
}
if (a.a_subject) {
a.a_subject = decodeTextContent(a.a_subject, 'text/plain; charset=utf-8');
}
if (htmlBodyMap[a.article_id]) {
a.html_body = htmlBodyMap[a.article_id];
a.has_html = true;
@@ -1821,34 +1877,56 @@ router.get('/attachments/:id', async (req, res) => {
}
const attachment = result.rows[0];
// Decode base64 if necessary
let contentBuffer;
if (attachment.content) {
let contentStr = '';
if (Buffer.isBuffer(attachment.content)) {
contentStr = attachment.content.toString('utf-8');
} else if (typeof attachment.content === 'string') {
contentStr = attachment.content;
}
// Check for raw binary signatures (%PDF, JPEG, PNG, Zip)
const isRawBinary = attachment.content.length > 4 && (
(attachment.content[0] === 0x25 && attachment.content[1] === 0x50 && attachment.content[2] === 0x44 && attachment.content[3] === 0x46) || // %PDF
(attachment.content[0] === 0xFF && attachment.content[1] === 0xD8) || // JPEG
(attachment.content[0] === 0x89 && attachment.content[1] === 0x50) || // PNG
(attachment.content[0] === 0x50 && attachment.content[1] === 0x4B) // ZIP/DOCX/XLSX
);
const cleaned = contentStr.replace(/\s+/g, '');
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (cleaned.length % 4 === 0 && base64Regex.test(cleaned)) {
try {
contentBuffer = Buffer.from(cleaned, 'base64');
} catch (e) {
contentBuffer = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content);
if (isRawBinary) {
contentBuffer = attachment.content;
} else {
const str = attachment.content.toString('utf-8');
const cleaned = str.replace(/\s+/g, '');
if (cleaned.length > 0 && cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(cleaned)) {
try {
contentBuffer = Buffer.from(cleaned, 'base64');
} catch (_) {
contentBuffer = attachment.content;
}
} else {
contentBuffer = attachment.content;
}
}
} else if (typeof attachment.content === 'string') {
const cleaned = attachment.content.replace(/\s+/g, '');
if (cleaned.length > 0 && cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(cleaned)) {
try {
contentBuffer = Buffer.from(cleaned, 'base64');
} catch (_) {
contentBuffer = Buffer.from(attachment.content, 'utf-8');
}
} else {
contentBuffer = Buffer.from(attachment.content, 'utf-8');
}
} else {
contentBuffer = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content);
contentBuffer = Buffer.from(attachment.content);
}
} else {
contentBuffer = Buffer.alloc(0);
}
const isDownload = req.query.download === '1' || req.query.disposition === 'attachment';
const disposition = isDownload ? 'attachment' : 'inline';
const safeFilename = attachment.filename || 'attachment';
res.setHeader('Content-Type', attachment.content_type || 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(safeFilename)}"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`);
res.setHeader('Content-Disposition', `${disposition}; filename="${encodeURIComponent(safeFilename)}"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`);
res.send(contentBuffer);
} catch (err) {
console.error('Errore download allegato:', err);