const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const { pool, initDb } = require('./db'); const { v4: uuidv4 } = require('uuid'); const nodemailer = require('nodemailer'); const app = express(); const PORT = process.env.PORT || 3001; const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_key_123'; app.use(cors()); // Increased limit to support base64 file uploads for tickets app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); // --- EMAIL HELPERS --- async function getTransporter() { const [settings] = await pool.query('SELECT smtp_config FROM settings WHERE id = 1'); if (!settings.length || !settings[0].smtp_config) return null; const config = settings[0].smtp_config; if (!config.host || !config.user || !config.pass) return null; return { transporter: nodemailer.createTransport({ host: config.host, port: config.port, secure: config.secure, auth: { user: config.user, pass: config.pass }, }), from: config.fromEmail || config.user }; } async function sendEmailToUsers(subject, body) { try { const setup = await getTransporter(); if (!setup) return; const [users] = await pool.query('SELECT email FROM users WHERE receive_alerts = TRUE AND email IS NOT NULL AND email != ""'); if (users.length === 0) return; const bccList = users.map(u => u.email).join(','); await setup.transporter.sendMail({ from: setup.from, bcc: bccList, subject: subject, text: body, }); } catch (error) { console.error('Email error:', error.message); } } async function sendDirectEmail(to, subject, body) { try { const setup = await getTransporter(); if (!setup) return; await setup.transporter.sendMail({ from: setup.from, to: to, subject: subject, text: body }); console.log(`Direct email sent to ${to}`); } catch (error) { console.error('Direct Email error:', error.message); } } // --- MIDDLEWARE --- const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) return res.sendStatus(401); jwt.verify(token, JWT_SECRET, (err, user) => { // Return 401 for token errors (expired/invalid) to trigger frontend logout if (err) { console.error("Token verification failed:", err.message); return res.sendStatus(401); } req.user = user; next(); }); }; const requireAdmin = (req, res, next) => { if (req.user && req.user.role === 'admin') { next(); } else { console.warn(`Access denied for user ${req.user?.email} with role ${req.user?.role}`); res.status(403).json({ message: 'Access denied: Admins only' }); } }; // --- AUTH --- app.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; try { const [users] = await pool.query('SELECT * FROM users WHERE email = ?', [email]); if (users.length === 0) return res.status(401).json({ message: 'Invalid credentials' }); const user = users[0]; const validPassword = await bcrypt.compare(password, user.password_hash); if (!validPassword) return res.status(401).json({ message: 'Invalid credentials' }); // Ensure role is captured correctly from DB const token = jwt.sign( { id: user.id, email: user.email, role: user.role, familyId: user.family_id }, JWT_SECRET, { expiresIn: '24h' } ); res.json({ token, user: { id: user.id, email: user.email, name: user.name, role: user.role, familyId: user.family_id, receiveAlerts: !!user.receive_alerts } }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- PROFILE --- app.put('/api/profile', authenticateToken, async (req, res) => { const userId = req.user.id; const { name, phone, password, receiveAlerts } = req.body; try { let query = 'UPDATE users SET name = ?, phone = ?, receive_alerts = ?'; let params = [name, phone, receiveAlerts]; if (password && password.trim() !== '') { const hashedPassword = await bcrypt.hash(password, 10); query += ', password_hash = ?'; params.push(hashedPassword); } query += ' WHERE id = ?'; params.push(userId); await pool.query(query, params); const [updatedUser] = await pool.query('SELECT id, email, name, role, phone, family_id, receive_alerts FROM users WHERE id = ?', [userId]); res.json({ success: true, user: { ...updatedUser[0], receiveAlerts: !!updatedUser[0].receive_alerts } }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- SETTINGS --- app.get('/api/settings', authenticateToken, async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM settings WHERE id = 1'); if (rows.length > 0) { res.json({ currentYear: rows[0].current_year, smtpConfig: rows[0].smtp_config || {}, features: rows[0].features || { multiCondo: true, tickets: true, payPal: true, notices: true } }); } else { res.status(404).json({ message: 'Settings not found' }); } } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/settings', authenticateToken, requireAdmin, async (req, res) => { const { currentYear, smtpConfig, features } = req.body; try { await pool.query( 'UPDATE settings SET current_year = ?, smtp_config = ?, features = ? WHERE id = 1', [currentYear, JSON.stringify(smtpConfig), JSON.stringify(features)] ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/years', authenticateToken, async (req, res) => { try { const [rows] = await pool.query('SELECT DISTINCT for_year FROM payments ORDER BY for_year DESC'); const [settings] = await pool.query('SELECT current_year FROM settings WHERE id = 1'); const years = new Set(rows.map(r => r.for_year)); if (settings.length > 0) years.add(settings[0].current_year); res.json(Array.from(years).sort((a, b) => b - a)); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- CONDOS --- app.get('/api/condos', authenticateToken, async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM condos'); res.json(rows.map(r => ({ id: r.id, name: r.name, address: r.address, streetNumber: r.street_number, city: r.city, province: r.province, zipCode: r.zip_code, notes: r.notes, iban: r.iban, paypalClientId: r.paypal_client_id, // PayPal defaultMonthlyQuota: parseFloat(r.default_monthly_quota), image: r.image }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/condos', authenticateToken, requireAdmin, async (req, res) => { const { name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId } = req.body; const id = uuidv4(); try { await pool.query( 'INSERT INTO condos (id, name, address, street_number, city, province, zip_code, notes, default_monthly_quota, paypal_client_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId] ); res.json({ id, name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/condos/:id', authenticateToken, requireAdmin, async (req, res) => { const { name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId } = req.body; try { await pool.query( 'UPDATE condos SET name = ?, address = ?, street_number = ?, city = ?, province = ?, zip_code = ?, notes = ?, default_monthly_quota = ?, paypal_client_id = ? WHERE id = ?', [name, address, streetNumber, city, province, zipCode, notes, defaultMonthlyQuota, paypalClientId, req.params.id] ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/condos/:id', authenticateToken, requireAdmin, async (req, res) => { try { await pool.query('DELETE FROM condos WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- FAMILIES --- app.get('/api/families', authenticateToken, async (req, res) => { const { condoId } = req.query; try { let query = `SELECT f.* FROM families f`; let params = []; // Authorization/Filtering logic if (req.user.role !== 'admin' && req.user.role !== 'poweruser') { // Regular user: can only see their own family if (!req.user.familyId) return res.json([]); query += ' WHERE f.id = ?'; params.push(req.user.familyId); } else { // Admin: If condoId provided, filter by it. if (condoId) { query += ' WHERE f.condo_id = ?'; params.push(condoId); } } const [rows] = await pool.query(query, params); res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, name: r.name, unitNumber: r.unit_number, stair: r.stair, floor: r.floor, notes: r.notes, contactEmail: r.contact_email, customMonthlyQuota: r.custom_monthly_quota ? parseFloat(r.custom_monthly_quota) : undefined, balance: 0 }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/families', authenticateToken, requireAdmin, async (req, res) => { const { name, unitNumber, stair, floor, notes, contactEmail, condoId, customMonthlyQuota } = req.body; const id = uuidv4(); try { await pool.query( 'INSERT INTO families (id, condo_id, name, unit_number, stair, floor, notes, contact_email, custom_monthly_quota) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', [id, condoId, name, unitNumber, stair, floor, notes, contactEmail, customMonthlyQuota || null] ); res.json({ id, condoId, name, unitNumber, stair, floor, notes, contactEmail, customMonthlyQuota }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/families/:id', authenticateToken, requireAdmin, async (req, res) => { const { name, unitNumber, stair, floor, notes, contactEmail, customMonthlyQuota } = req.body; try { await pool.query( 'UPDATE families SET name = ?, unit_number = ?, stair = ?, floor = ?, notes = ?, contact_email = ?, custom_monthly_quota = ? WHERE id = ?', [name, unitNumber, stair, floor, notes, contactEmail, customMonthlyQuota || null, req.params.id] ); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/families/:id', authenticateToken, requireAdmin, async (req, res) => { try { await pool.query('DELETE FROM families WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- NOTICES --- app.get('/api/notices', authenticateToken, async (req, res) => { const { condoId } = req.query; try { let query = 'SELECT * FROM notices'; let params = []; if (condoId) { query += ' WHERE condo_id = ?'; params.push(condoId); } query += ' ORDER BY date DESC'; const [rows] = await pool.query(query, params); res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, title: r.title, content: r.content, type: r.type, link: r.link, date: r.date, active: !!r.active }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/notices', authenticateToken, requireAdmin, async (req, res) => { const { condoId, title, content, type, link, active } = req.body; const id = uuidv4(); try { await pool.query('INSERT INTO notices (id, condo_id, title, content, type, link, active, date) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())', [id, condoId, title, content, type, link, active]); res.json({ id, condoId, title, content, type, link, active }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/notices/:id', authenticateToken, requireAdmin, async (req, res) => { const { title, content, type, link, active } = req.body; try { await pool.query('UPDATE notices SET title = ?, content = ?, type = ?, link = ?, active = ? WHERE id = ?', [title, content, type, link, active, req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/notices/:id', authenticateToken, requireAdmin, async (req, res) => { try { await pool.query('DELETE FROM notices WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/notices/:id/read', authenticateToken, async (req, res) => { const { userId } = req.body; try { await pool.query('INSERT IGNORE INTO notice_reads (user_id, notice_id, read_at) VALUES (?, ?, NOW())', [userId, req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/notices/:id/reads', authenticateToken, async (req, res) => { try { const [rows] = await pool.query('SELECT * FROM notice_reads WHERE notice_id = ?', [req.params.id]); res.json(rows.map(r => ({ userId: r.user_id, noticeId: r.notice_id, readAt: r.read_at }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/notices/unread', authenticateToken, async (req, res) => { const { userId, condoId } = req.query; try { const [rows] = await pool.query(` SELECT n.* FROM notices n LEFT JOIN notice_reads nr ON n.id = nr.notice_id AND nr.user_id = ? WHERE n.condo_id = ? AND n.active = TRUE AND nr.read_at IS NULL ORDER BY n.date DESC `, [userId, condoId]); res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, title: r.title, content: r.content, type: r.type, link: r.link, date: r.date, active: !!r.active }))); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- PAYMENTS --- app.get('/api/payments', authenticateToken, async (req, res) => { const { familyId } = req.query; try { const isPrivileged = req.user.role === 'admin' || req.user.role === 'poweruser'; if (!isPrivileged) { if (familyId && familyId !== req.user.familyId) return res.status(403).json({ message: 'Forbidden' }); if (!familyId) { const [rows] = await pool.query('SELECT * FROM payments WHERE family_id = ?', [req.user.familyId]); return res.json(rows.map(mapPaymentRow)); } } let query = 'SELECT * FROM payments'; let params = []; if (familyId) { query += ' WHERE family_id = ?'; params.push(familyId); } const [rows] = await pool.query(query, params); res.json(rows.map(mapPaymentRow)); } catch (e) { res.status(500).json({ error: e.message }); } }); function mapPaymentRow(r) { return { id: r.id, familyId: r.family_id, amount: parseFloat(r.amount), datePaid: r.date_paid, forMonth: r.for_month, forYear: r.for_year, notes: r.notes }; } app.post('/api/payments', authenticateToken, async (req, res) => { const { familyId, amount, datePaid, forMonth, forYear, notes } = req.body; // Security Check: // Admin can post for anyone. // Regular users can only post for their own family (e.g. PayPal automated callback) const isPrivileged = req.user.role === 'admin' || req.user.role === 'poweruser'; if (!isPrivileged) { if (familyId !== req.user.familyId) { return res.status(403).json({message: "Forbidden: You can only record payments for your own family."}); } } const id = uuidv4(); try { await pool.query('INSERT INTO payments (id, family_id, amount, date_paid, for_month, for_year, notes) VALUES (?, ?, ?, ?, ?, ?, ?)', [id, familyId, amount, new Date(datePaid), forMonth, forYear, notes]); res.json({ id, familyId, amount, datePaid, forMonth, forYear, notes }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- USERS --- app.get('/api/users', authenticateToken, requireAdmin, async (req, res) => { const { condoId } = req.query; try { let query = 'SELECT u.id, u.email, u.name, u.role, u.phone, u.family_id, u.receive_alerts FROM users u'; let params = []; // Filter users by condo. // Logic: Users belong to families, families belong to condos. if (condoId) { query += ' LEFT JOIN families f ON u.family_id = f.id WHERE f.condo_id = ?'; params.push(condoId); } const [rows] = await pool.query(query, params); res.json(rows.map(r => ({ id: r.id, email: r.email, name: r.name, role: r.role, phone: r.phone, familyId: r.family_id, receiveAlerts: !!r.receive_alerts }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/users', authenticateToken, requireAdmin, async (req, res) => { const { email, password, name, role, familyId, phone, receiveAlerts } = req.body; try { const hashedPassword = await bcrypt.hash(password, 10); const id = uuidv4(); await pool.query('INSERT INTO users (id, email, password_hash, name, role, family_id, phone, receive_alerts) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [id, email, hashedPassword, name, role || 'user', familyId || null, phone, receiveAlerts]); res.json({ success: true, id }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/users/:id', authenticateToken, requireAdmin, async (req, res) => { const { email, role, familyId, name, phone, password, receiveAlerts } = req.body; try { let query = 'UPDATE users SET email = ?, role = ?, family_id = ?, name = ?, phone = ?, receive_alerts = ?'; let params = [email, role, familyId || null, name, phone, receiveAlerts]; if (password && password.trim() !== '') { const hashedPassword = await bcrypt.hash(password, 10); query += ', password_hash = ?'; params.push(hashedPassword); } query += ' WHERE id = ?'; params.push(req.params.id); await pool.query(query, params); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/users/:id', authenticateToken, requireAdmin, async (req, res) => { try { await pool.query('DELETE FROM users WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- ALERTS --- app.get('/api/alerts', authenticateToken, requireAdmin, async (req, res) => { const { condoId } = req.query; try { let query = 'SELECT * FROM alerts'; let params = []; if (condoId) { query += ' WHERE condo_id = ?'; params.push(condoId); } const [rows] = await pool.query(query, params); res.json(rows.map(r => ({ id: r.id, condoId: r.condo_id, subject: r.subject, body: r.body, daysOffset: r.days_offset, offsetType: r.offset_type, sendHour: r.send_hour, active: !!r.active, lastSent: r.last_sent }))); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/alerts', authenticateToken, requireAdmin, async (req, res) => { const { condoId, subject, body, daysOffset, offsetType, sendHour, active } = req.body; const id = uuidv4(); try { await pool.query('INSERT INTO alerts (id, condo_id, subject, body, days_offset, offset_type, send_hour, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [id, condoId, subject, body, daysOffset, offsetType, sendHour, active]); res.json({ id, condoId, subject, body, daysOffset, offsetType, sendHour, active }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.put('/api/alerts/:id', authenticateToken, requireAdmin, async (req, res) => { const { subject, body, daysOffset, offsetType, sendHour, active } = req.body; try { await pool.query('UPDATE alerts SET subject = ?, body = ?, days_offset = ?, offset_type = ?, send_hour = ?, active = ? WHERE id = ?', [subject, body, daysOffset, offsetType, sendHour, active, req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/alerts/:id', authenticateToken, requireAdmin, async (req, res) => { try { await pool.query('DELETE FROM alerts WHERE id = ?', [req.params.id]); res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); // --- TICKETS (SEGNALAZIONI) --- app.get('/api/tickets', authenticateToken, async (req, res) => { const { condoId } = req.query; const userId = req.user.id; const isAdmin = req.user.role === 'admin' || req.user.role === 'poweruser'; try { let query = ` SELECT t.*, u.name as user_name, u.email as user_email FROM tickets t JOIN users u ON t.user_id = u.id WHERE t.condo_id = ? `; let params = [condoId]; // If not admin, restrict to own tickets if (!isAdmin) { query += ' AND t.user_id = ?'; params.push(userId); } query += ' ORDER BY t.created_at DESC'; const [rows] = await pool.query(query, params); // Fetch attachments for these tickets const ticketIds = rows.map(r => r.id); let attachmentsMap = {}; if (ticketIds.length > 0) { const placeholders = ticketIds.map(() => '?').join(','); // Exclude 'data' column to keep listing light const [attRows] = await pool.query(`SELECT id, ticket_id, file_name, file_type FROM ticket_attachments WHERE ticket_id IN (${placeholders})`, ticketIds); attRows.forEach(a => { if (!attachmentsMap[a.ticket_id]) attachmentsMap[a.ticket_id] = []; attachmentsMap[a.ticket_id].push({ id: a.id, fileName: a.file_name, fileType: a.file_type }); }); } const result = rows.map(r => ({ id: r.id, condoId: r.condo_id, userId: r.user_id, title: r.title, description: r.description, status: r.status, priority: r.priority, category: r.category, createdAt: r.created_at, updatedAt: r.updated_at, userName: r.user_name, userEmail: r.user_email, attachments: attachmentsMap[r.id] || [] })); res.json(result); } catch (e) { res.status(500).json({ error: e.message }); } }); app.get('/api/tickets/:id/attachments/:attachmentId', authenticateToken, async (req, res) => { // Serve file content try { const [rows] = await pool.query('SELECT * FROM ticket_attachments WHERE id = ? AND ticket_id = ?', [req.params.attachmentId, req.params.id]); if (rows.length === 0) return res.status(404).json({ message: 'File not found' }); const file = rows[0]; res.json({ id: file.id, fileName: file.file_name, fileType: file.file_type, data: file.data }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.post('/api/tickets', authenticateToken, async (req, res) => { const { condoId, title, description, category, priority, attachments } = req.body; const userId = req.user.id; const ticketId = uuidv4(); // Begin transaction const connection = await pool.getConnection(); try { await connection.beginTransaction(); await connection.query( 'INSERT INTO tickets (id, condo_id, user_id, title, description, category, priority, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [ticketId, condoId, userId, title, description, category, priority || 'MEDIUM', 'OPEN'] ); if (attachments && Array.isArray(attachments)) { for (const att of attachments) { const attId = uuidv4(); await connection.query( 'INSERT INTO ticket_attachments (id, ticket_id, file_name, file_type, data) VALUES (?, ?, ?, ?, ?)', [attId, ticketId, att.fileName, att.fileType, att.data] ); } } await connection.commit(); // --- EMAIL NOTIFICATION TO ADMINS --- // Find Admins/PowerUsers for this condo (or global) who want alerts const [admins] = await connection.query(` SELECT u.email FROM users u LEFT JOIN families f ON u.family_id = f.id WHERE (u.role = 'admin' OR u.role = 'poweruser') AND (f.condo_id = ? OR u.family_id IS NULL) AND u.receive_alerts = TRUE `, [condoId]); const adminEmails = admins.map(a => a.email).filter(e => e); if (adminEmails.length > 0) { // Fetch user name for clearer email const [uRows] = await connection.query('SELECT name FROM users WHERE id = ?', [userId]); const userName = uRows[0]?.name || 'Un condomino'; const subject = `Nuova Segnalazione: ${title}`; const body = `Salve,\n\n${userName} ha aperto una nuova segnalazione.\n\nOggetto: ${title}\nCategoria: ${category}\nPriorità: ${priority || 'MEDIUM'}\n\nDescrizione:\n${description}\n\nAccedi alla piattaforma per gestire il ticket.`; // Loop to send individually or use BCC for(const email of adminEmails) { sendDirectEmail(email, subject, body); } } res.json({ success: true, id: ticketId }); } catch (e) { await connection.rollback(); res.status(500).json({ error: e.message }); } finally { connection.release(); } }); app.put('/api/tickets/:id', authenticateToken, async (req, res) => { const { status, priority } = req.body; const isAdmin = req.user.role === 'admin' || req.user.role === 'poweruser'; // Only admins/powerusers can change status/priority for now if (!isAdmin) return res.status(403).json({ message: 'Forbidden' }); try { await pool.query( 'UPDATE tickets SET status = ?, priority = ? WHERE id = ?', [status, priority, req.params.id] ); // --- EMAIL NOTIFICATION TO USER --- const [tRows] = await pool.query('SELECT t.title, t.user_id, u.email, u.receive_alerts FROM tickets t JOIN users u ON t.user_id = u.id WHERE t.id = ?', [req.params.id]); if (tRows.length > 0) { const ticket = tRows[0]; if (ticket.email && ticket.receive_alerts) { const subject = `Aggiornamento Ticket: ${ticket.title}`; const body = `Salve,\n\nIl tuo ticket "${ticket.title}" è stato aggiornato.\n\nNuovo Stato: ${status}\nPriorità: ${priority}\n\nAccedi alla piattaforma per i dettagli.`; sendDirectEmail(ticket.email, subject, body); } } res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); app.delete('/api/tickets/:id', authenticateToken, async (req, res) => { // Only delete own ticket if open, or admin can delete any const isAdmin = req.user.role === 'admin' || req.user.role === 'poweruser'; const userId = req.user.id; try { let query = 'DELETE FROM tickets WHERE id = ?'; let params = [req.params.id]; if (!isAdmin) { query += ' AND user_id = ? AND status = "OPEN"'; // Users can only delete their own OPEN tickets params.push(userId); } const [result] = await pool.query(query, params); if (result.affectedRows === 0) { return res.status(403).json({ message: 'Cannot delete ticket (Permission denied or not found)' }); } res.json({ success: true }); } catch (e) { res.status(500).json({ error: e.message }); } }); initDb().then(() => { app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); });