V.1.2 - Improved Secrurity, Better Ticket Handling
This commit is contained in:
1 parent
cb5d091be2
commit
3136c81533
16 files changed
+879
-738
No files matched your search
@@ -5,6 +5,8 @@ import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import moment from 'moment-timezone';
|
||||
import dotenv from 'dotenv';
|
||||
import multer from 'multer';
|
||||
import fs from 'fs';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
@@ -15,11 +17,13 @@ const __dirname = path.dirname(__filename);
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
// Load webhook URLs from environment
|
||||
// Load webhook URLs from environment (kept server-side only!)
|
||||
const ADMIN_WEBHOOK_URL = process.env.ADMIN_WEBHOOK_URL;
|
||||
const USER_WEBHOOK_URL = process.env.USER_WEBHOOK_URL;
|
||||
|
||||
const timestamp = moment().utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
// Read version from package.json
|
||||
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8'));
|
||||
const APP_VERSION = pkg.version || '0.0.0';
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
@@ -27,27 +31,75 @@ app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use('/assets', express.static(path.join(__dirname, 'assets')));
|
||||
|
||||
const dbPath = path.resolve(__dirname, 'tickets.db');
|
||||
const db = new sqlite3.Database(dbPath, err => {
|
||||
if (err) {
|
||||
console.error('Failed to connect to DB:', err.message);
|
||||
} else {
|
||||
console.log('Connected to SQLite DB.');
|
||||
// --- Profile Picture Upload (supports user + admin) ---
|
||||
function createPfpUpload(filename) {
|
||||
const storage = multer.diskStorage({
|
||||
destination: path.join(__dirname, 'assets'),
|
||||
filename: (_req, _file, cb) => cb(null, filename),
|
||||
});
|
||||
return multer({
|
||||
storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (file.mimetype.startsWith('image/')) cb(null, true);
|
||||
else cb(new Error('Only image files are allowed'));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- Categories Config ---
|
||||
const categoriesPath = path.resolve(__dirname, 'categories.json');
|
||||
function loadCategories() {
|
||||
try {
|
||||
const data = fs.readFileSync(categoriesPath, 'utf-8');
|
||||
return JSON.parse(data).categories || [];
|
||||
} catch {
|
||||
return ['HELP', 'IT-Support', 'Suggestion...', 'Other'];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Database ---
|
||||
const dbPath = path.resolve(__dirname, 'data', 'tickets.db');
|
||||
const db = new sqlite3.Database(dbPath, err => {
|
||||
if (err) console.error('Failed to connect to DB:', err.message);
|
||||
else console.log('Connected to SQLite DB.');
|
||||
});
|
||||
|
||||
// Enable WAL mode for better concurrent reads
|
||||
db.run('PRAGMA journal_mode=WAL');
|
||||
|
||||
// Promisified DB helpers
|
||||
function dbAll(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(sql, params, (err, rows) => (err ? reject(err) : resolve(rows)));
|
||||
});
|
||||
}
|
||||
function dbGet(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(sql, params, (err, row) => (err ? reject(err) : resolve(row)));
|
||||
});
|
||||
}
|
||||
function dbRun(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(sql, params, function (err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ lastID: this.lastID, changes: this.changes });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create tables
|
||||
db.serialize(() => {
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS tickets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT,
|
||||
requester TEXT,
|
||||
title TEXT NOT NULL,
|
||||
requester TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
status TEXT,
|
||||
priority TEXT,
|
||||
status TEXT DEFAULT 'Open',
|
||||
priority TEXT DEFAULT 'Medium',
|
||||
category TEXT,
|
||||
total_time INTEGER DEFAULT 0
|
||||
)
|
||||
@@ -61,7 +113,7 @@ db.serialize(() => {
|
||||
comment TEXT,
|
||||
author TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(ticket_id) REFERENCES tickets(id)
|
||||
FOREIGN KEY(ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
@@ -74,230 +126,404 @@ db.serialize(() => {
|
||||
author TEXT,
|
||||
role TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(ticket_id) REFERENCES tickets(id)
|
||||
FOREIGN KEY(ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_time_logs_ticket ON time_logs(ticket_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_history_ticket ON history(ticket_id)`);
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_time_logs_ticket ON time_logs(ticket_id)');
|
||||
db.run('CREATE INDEX IF NOT EXISTS idx_history_ticket ON history(ticket_id)');
|
||||
db.run('PRAGMA foreign_keys = ON');
|
||||
});
|
||||
|
||||
// Log history
|
||||
// --- Helpers ---
|
||||
function utcNow() {
|
||||
return moment().utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
function logHistory(ticketId, action, message, author = 'system', role = 'user') {
|
||||
const timestamp = moment().utc().format('YYYY-MM-DD HH:mm:ss');
|
||||
console.log(`Logging history for ticket: ${ticketId}, timestamp: ${timestamp}`); // Debugging line
|
||||
db.run(
|
||||
'INSERT INTO history (ticket_id, action, message, author, role, timestamp) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[ticketId, action, message, author, role, timestamp]
|
||||
[ticketId, action, message, author, role, utcNow()]
|
||||
);
|
||||
}
|
||||
|
||||
// GET webhook configuration for frontend
|
||||
app.get('/api/webhooks', (req, res) => {
|
||||
// --- Server-side webhook sending (URLs never exposed to frontend) ---
|
||||
async function sendWebhook(url, payload) {
|
||||
if (!url) return;
|
||||
try {
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Webhook send failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Discord embed colors
|
||||
const COLORS = {
|
||||
green: 0x00ff00,
|
||||
orange: 0xffa500,
|
||||
blue: 0x4a90e2,
|
||||
red: 0xd0021b,
|
||||
purple: 0x9b59b6,
|
||||
yellow: 0xf5a623,
|
||||
};
|
||||
|
||||
// --- Admin Notifications ---
|
||||
function notifyAdmin(ticket, event = 'created') {
|
||||
if (!ADMIN_WEBHOOK_URL) return;
|
||||
|
||||
const embeds = {
|
||||
created: {
|
||||
title: '🎫 New Ticket Created',
|
||||
color: COLORS.green,
|
||||
description: `**Ticket #${ticket.id}**: ${ticket.title}`,
|
||||
fields: [
|
||||
{ name: '👤 Requester', value: ticket.requester || 'Unknown' },
|
||||
{ name: '📊 Status', value: ticket.status || 'Open', inline: true },
|
||||
{ name: '⚡ Priority', value: ticket.priority || 'None', inline: true },
|
||||
{ name: '📂 Category', value: ticket.category || 'None', inline: true },
|
||||
...(ticket.description ? [{
|
||||
name: '📝 Description',
|
||||
value: ticket.description.length > 200 ? ticket.description.substring(0, 200) + '...' : ticket.description,
|
||||
}] : []),
|
||||
],
|
||||
},
|
||||
comment: {
|
||||
title: '💬 New User Comment',
|
||||
color: COLORS.blue,
|
||||
description: `**Ticket #${ticket.id}**: ${ticket.title}`,
|
||||
fields: [
|
||||
{ name: '👤 Author', value: ticket.commentAuthor || 'Unknown', inline: true },
|
||||
{ name: '📊 Status', value: ticket.status || '-', inline: true },
|
||||
{ name: '💬 Comment', value: ticket.commentContent?.length > 200 ? ticket.commentContent.substring(0, 200) + '...' : (ticket.commentContent || '-') },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const embed = embeds[event] || embeds.created;
|
||||
embed.timestamp = new Date().toISOString();
|
||||
embed.footer = { text: 'Ticket System' };
|
||||
|
||||
sendWebhook(ADMIN_WEBHOOK_URL, { embeds: [embed] });
|
||||
}
|
||||
|
||||
// --- User Notifications ---
|
||||
function notifyUser(type, data = {}) {
|
||||
if (!USER_WEBHOOK_URL) return;
|
||||
|
||||
const embeds = {
|
||||
statusChanged: {
|
||||
title: '📊 Status Changed',
|
||||
color: data.newStatus === 'Closed' ? COLORS.red : COLORS.orange,
|
||||
description: `**Ticket #${data.ticketId}**: ${data.title || ''}`,
|
||||
fields: [
|
||||
{ name: 'Previous', value: data.oldStatus || '-', inline: true },
|
||||
{ name: 'New', value: data.newStatus || '-', inline: true },
|
||||
{ name: '👤 Changed by', value: data.actor || 'Admin', inline: true },
|
||||
],
|
||||
},
|
||||
priorityChanged: {
|
||||
title: '⚡ Priority Changed',
|
||||
color: COLORS.yellow,
|
||||
description: `**Ticket #${data.ticketId}**: ${data.title || ''}`,
|
||||
fields: [
|
||||
{ name: 'Previous', value: data.oldPriority || '-', inline: true },
|
||||
{ name: 'New', value: data.newPriority || '-', inline: true },
|
||||
{ name: '👤 Changed by', value: data.actor || 'Admin', inline: true },
|
||||
],
|
||||
},
|
||||
comment: {
|
||||
title: '💬 New Admin Comment',
|
||||
color: COLORS.purple,
|
||||
description: `**Ticket #${data.ticketId}**: ${data.title || ''}`,
|
||||
fields: [
|
||||
{ name: '👤 Author', value: data.author || 'Admin', inline: true },
|
||||
{ name: '💬 Comment', value: data.content?.length > 200 ? data.content.substring(0, 200) + '...' : (data.content || '-') },
|
||||
],
|
||||
},
|
||||
timeLogged: {
|
||||
title: '⏱ Time Booked',
|
||||
color: COLORS.blue,
|
||||
description: `**Ticket #${data.ticketId}**: ${data.title || ''}`,
|
||||
fields: [
|
||||
{ name: '⏱ Duration', value: `${data.minutes || 0} min`, inline: true },
|
||||
{ name: '👤 By', value: data.author || 'Admin', inline: true },
|
||||
...(data.comment ? [{ name: '📝 Note', value: data.comment }] : []),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const embed = embeds[type];
|
||||
if (!embed) return;
|
||||
|
||||
embed.timestamp = new Date().toISOString();
|
||||
embed.footer = { text: 'Ticket System' };
|
||||
|
||||
sendWebhook(USER_WEBHOOK_URL, { embeds: [embed] });
|
||||
}
|
||||
|
||||
// --- Input validation middleware ---
|
||||
function validateTicketBody(req, res, next) {
|
||||
const { title, requester } = req.body;
|
||||
if (!title?.trim()) return res.status(400).json({ error: 'Title is required' });
|
||||
if (!requester?.trim()) return res.status(400).json({ error: 'Requester is required' });
|
||||
next();
|
||||
}
|
||||
|
||||
// ==================== API ROUTES ====================
|
||||
|
||||
// GET webhook status (only tells frontend if configured, NOT the URLs)
|
||||
app.get('/api/webhooks', (_req, res) => {
|
||||
res.json({
|
||||
admin: ADMIN_WEBHOOK_URL || null,
|
||||
user: USER_WEBHOOK_URL || null,
|
||||
configured: {
|
||||
admin: !!ADMIN_WEBHOOK_URL,
|
||||
user: !!USER_WEBHOOK_URL
|
||||
}
|
||||
user: !!USER_WEBHOOK_URL,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Upload profile picture (user)
|
||||
app.post('/api/upload-pfp', createPfpUpload('user.jpg').single('pfp'), (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
||||
res.json({ message: 'Profile picture updated', filename: req.file.filename });
|
||||
});
|
||||
|
||||
// Upload profile picture (admin)
|
||||
app.post('/api/upload-admin-pfp', createPfpUpload('admin.jpg').single('pfp'), (req, res) => {
|
||||
if (!req.file) return res.status(400).json({ error: 'No file uploaded' });
|
||||
res.json({ message: 'Admin profile picture updated', filename: req.file.filename });
|
||||
});
|
||||
|
||||
// GET categories
|
||||
app.get('/api/categories', (_req, res) => {
|
||||
res.json(loadCategories());
|
||||
});
|
||||
|
||||
// GET app version
|
||||
app.get('/api/version', (_req, res) => {
|
||||
res.json({ version: APP_VERSION });
|
||||
});
|
||||
|
||||
// GET all tickets
|
||||
app.get('/api/tickets', (req, res) => {
|
||||
db.all(
|
||||
`SELECT id, title, requester, description, status, priority, category, total_time,
|
||||
strftime('%Y-%m-%dT%H:%M:%S', created_at) AS created_at,
|
||||
strftime('%Y-%m-%dT%H:%M:%S', updated_at) AS updated_at
|
||||
FROM tickets
|
||||
ORDER BY created_at DESC`,
|
||||
(err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
app.get('/api/tickets', async (_req, res) => {
|
||||
try {
|
||||
const rows = await dbAll(`
|
||||
SELECT id, title, requester, description, status, priority, category, total_time,
|
||||
strftime('%Y-%m-%dT%H:%M:%S', created_at) AS created_at,
|
||||
strftime('%Y-%m-%dT%H:%M:%S', updated_at) AS updated_at
|
||||
FROM tickets ORDER BY created_at DESC
|
||||
`);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET single ticket
|
||||
app.get('/api/tickets/:id', (req, res) => {
|
||||
db.get('SELECT * FROM tickets WHERE id = ?', [req.params.id], (err, row) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
app.get('/api/tickets/:id', async (req, res) => {
|
||||
try {
|
||||
const row = await dbGet('SELECT * FROM tickets WHERE id = ?', [req.params.id]);
|
||||
if (!row) return res.status(404).json({ error: 'Ticket not found' });
|
||||
res.json(row);
|
||||
});
|
||||
});
|
||||
|
||||
// GET ticket history (ISO formatted date)
|
||||
app.get('/api/tickets/:id/history', (req, res) => {
|
||||
db.all(
|
||||
`SELECT id, ticket_id, action, message, author, role,
|
||||
strftime('%Y-%m-%dT%H:%M:%S', timestamp) AS date
|
||||
FROM history
|
||||
WHERE ticket_id = ?
|
||||
ORDER BY timestamp DESC`,
|
||||
[req.params.id],
|
||||
(err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST new ticket
|
||||
app.post('/api/tickets', (req, res) => {
|
||||
app.post('/api/tickets', validateTicketBody, async (req, res) => {
|
||||
const { title, requester, description, status, priority, category } = req.body;
|
||||
const statusValue = status && status.trim() ? status : 'open';
|
||||
|
||||
// Get current Berlin time and convert it to UTC
|
||||
const berlinTime = moment.tz('Europe/Berlin');
|
||||
const utcTime = berlinTime.clone().utc().format('YYYY-MM-DD HH:mm:ss'); // Convert Berlin time to UTC
|
||||
|
||||
db.run(
|
||||
'INSERT INTO tickets (title, requester, description, status, priority, category, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[title, requester, description, statusValue, priority, category, utcTime, utcTime],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
logHistory(this.lastID, 'Ticket Created', `Ticket "${title}" created`, requester);
|
||||
res.status(201).json({ id: this.lastID });
|
||||
}
|
||||
);
|
||||
const ts = utcNow();
|
||||
try {
|
||||
const result = await dbRun(
|
||||
`INSERT INTO tickets (title, requester, description, status, priority, category, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
[title.trim(), requester.trim(), description?.trim() || '', status || 'Open', priority || 'Medium', category || '', ts, ts]
|
||||
);
|
||||
const ticket = { id: result.lastID, title, requester, description, status: status || 'Open', priority: priority || 'Medium', category };
|
||||
logHistory(result.lastID, 'Ticket Created', `Ticket "${title}" created`, requester);
|
||||
notifyAdmin(ticket, 'created');
|
||||
res.status(201).json({ id: result.lastID });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT update ticket
|
||||
app.put('/api/tickets/:id', (req, res) => {
|
||||
app.put('/api/tickets/:id', async (req, res) => {
|
||||
const { title, requester, description, status, priority, category, author } = req.body;
|
||||
const ts = utcNow();
|
||||
try {
|
||||
// Fetch old ticket for change detection
|
||||
const old = await dbGet('SELECT * FROM tickets WHERE id = ?', [req.params.id]);
|
||||
if (!old) return res.status(404).json({ error: 'Ticket not found' });
|
||||
|
||||
// Get current Berlin time and convert it to UTC
|
||||
const berlinTime = moment.tz('Europe/Berlin');
|
||||
const utcTime = berlinTime.clone().utc().format('YYYY-MM-DD HH:mm:ss'); // Convert Berlin time to UTC
|
||||
await dbRun(
|
||||
`UPDATE tickets SET title=?, requester=?, description=?, status=?, priority=?, category=?, updated_at=? WHERE id=?`,
|
||||
[title, requester || old.requester, description, status, priority, category, ts, req.params.id]
|
||||
);
|
||||
|
||||
db.run(
|
||||
`UPDATE tickets SET
|
||||
title = ?,
|
||||
requester = ?,
|
||||
description = ?,
|
||||
status = ?,
|
||||
priority = ?,
|
||||
category = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[title, requester, description, status, priority, category, utcTime, req.params.id],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (this.changes === 0) return res.status(404).json({ error: 'Ticket not found' });
|
||||
logHistory(req.params.id, 'Ticket Updated', 'Ticket details updated', author || 'system');
|
||||
res.json({ message: 'Ticket updated' });
|
||||
const actor = author || 'system';
|
||||
|
||||
// Log specific changes to history
|
||||
if (status !== old.status) {
|
||||
logHistory(req.params.id, 'Status Changed', `Status changed from "${old.status}" to "${status}"`, actor);
|
||||
notifyUser('statusChanged', { ticketId: req.params.id, title: old.title, oldStatus: old.status, newStatus: status, actor });
|
||||
}
|
||||
);
|
||||
if (priority !== old.priority) {
|
||||
logHistory(req.params.id, 'Priority Changed', `Priority changed from "${old.priority}" to "${priority}"`, actor);
|
||||
notifyUser('priorityChanged', { ticketId: req.params.id, title: old.title, oldPriority: old.priority, newPriority: priority, actor });
|
||||
}
|
||||
if (category !== (old.category || '')) {
|
||||
logHistory(req.params.id, 'Category Changed', `Category changed from "${old.category || 'none'}" to "${category}"`, actor);
|
||||
}
|
||||
if (title !== old.title || description !== old.description) {
|
||||
logHistory(req.params.id, 'Ticket Updated', 'Ticket details updated', actor);
|
||||
}
|
||||
|
||||
res.json({ message: 'Ticket updated' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE ticket
|
||||
app.delete('/api/tickets/:id', (req, res) => {
|
||||
db.run('DELETE FROM tickets WHERE id = ?', [req.params.id], function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (this.changes === 0) return res.status(404).json({ error: 'Ticket not found' });
|
||||
logHistory(req.params.id, 'Ticket Deleted', 'Ticket deleted');
|
||||
app.delete('/api/tickets/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await dbRun('DELETE FROM tickets WHERE id = ?', [req.params.id]);
|
||||
if (result.changes === 0) return res.status(404).json({ error: 'Ticket not found' });
|
||||
res.json({ message: 'Ticket deleted' });
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST comment with role
|
||||
app.post('/api/tickets/:id/comments', (req, res) => {
|
||||
// POST comment
|
||||
app.post('/api/tickets/:id/comments', async (req, res) => {
|
||||
const { author, role, content } = req.body;
|
||||
if (!author || !content) return res.status(400).json({ error: 'Author and content required' });
|
||||
if (!author?.trim() || !content?.trim()) {
|
||||
return res.status(400).json({ error: 'Author and content required' });
|
||||
}
|
||||
try {
|
||||
const result = await dbRun(
|
||||
'INSERT INTO history (ticket_id, action, message, author, role, timestamp) VALUES (?, "Comment", ?, ?, ?, ?)',
|
||||
[req.params.id, content.trim(), author.trim(), role || 'user', utcNow()]
|
||||
);
|
||||
|
||||
const timestamp = moment().utc().format('YYYY-MM-DD HH:mm:ss'); // Explicit UTC timestamp
|
||||
// Server-side webhook for comments
|
||||
const ticket = await dbGet('SELECT * FROM tickets WHERE id = ?', [req.params.id]);
|
||||
const ticketTitle = ticket?.title || '';
|
||||
|
||||
db.run(
|
||||
'INSERT INTO history (ticket_id, action, message, author, role, timestamp) VALUES (?, "Comment", ?, ?, ?, ?)',
|
||||
[req.params.id, content, author, role || 'user', timestamp],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.status(201).json({ id: this.lastID });
|
||||
if (role === 'admin') {
|
||||
notifyUser('comment', { ticketId: req.params.id, title: ticketTitle, author, content: content.trim() });
|
||||
} else {
|
||||
notifyAdmin({
|
||||
id: req.params.id,
|
||||
title: ticketTitle,
|
||||
requester: ticket?.requester || author,
|
||||
status: ticket?.status || '-',
|
||||
commentAuthor: author,
|
||||
commentContent: content.trim(),
|
||||
}, 'comment');
|
||||
}
|
||||
);
|
||||
|
||||
res.status(201).json({ id: result.lastID });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET comments (ISO formatted date)
|
||||
app.get('/api/tickets/:id/comments', (req, res) => {
|
||||
db.all(
|
||||
`SELECT id, ticket_id, message AS content, author, role,
|
||||
strftime('%Y-%m-%dT%H:%M:%S', timestamp) AS date
|
||||
FROM history
|
||||
WHERE ticket_id = ? AND action = "Comment"
|
||||
ORDER BY timestamp DESC`,
|
||||
[req.params.id],
|
||||
(err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
// GET comments
|
||||
app.get('/api/tickets/:id/comments', async (req, res) => {
|
||||
try {
|
||||
const rows = await dbAll(
|
||||
`SELECT id, ticket_id, message AS content, author, role,
|
||||
strftime('%Y-%m-%dT%H:%M:%S', timestamp) AS date
|
||||
FROM history WHERE ticket_id = ? AND action = "Comment"
|
||||
ORDER BY timestamp DESC`,
|
||||
[req.params.id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET ticket history
|
||||
app.get('/api/tickets/:id/history', async (req, res) => {
|
||||
try {
|
||||
const rows = await dbAll(
|
||||
`SELECT id, ticket_id, action, message, author, role,
|
||||
strftime('%Y-%m-%dT%H:%M:%S', timestamp) AS date
|
||||
FROM history WHERE ticket_id = ?
|
||||
ORDER BY timestamp DESC`,
|
||||
[req.params.id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST time log
|
||||
app.post('/api/tickets/:id/time', (req, res) => {
|
||||
app.post('/api/tickets/:id/time', async (req, res) => {
|
||||
const { minutes, comment, author } = req.body;
|
||||
if (typeof minutes !== 'number' || minutes <= 0)
|
||||
if (typeof minutes !== 'number' || minutes <= 0) {
|
||||
return res.status(400).json({ error: 'Minutes must be a positive number.' });
|
||||
if (!author)
|
||||
}
|
||||
if (!author?.trim()) {
|
||||
return res.status(400).json({ error: 'Author is required.' });
|
||||
|
||||
db.get('SELECT * FROM tickets WHERE id = ?', [req.params.id], (err, ticket) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
}
|
||||
try {
|
||||
const ticket = await dbGet('SELECT * FROM tickets WHERE id = ?', [req.params.id]);
|
||||
if (!ticket) return res.status(404).json({ error: 'Ticket not found' });
|
||||
|
||||
db.run(
|
||||
await dbRun(
|
||||
'INSERT INTO time_logs (ticket_id, minutes, comment, author) VALUES (?, ?, ?, ?)',
|
||||
[req.params.id, minutes, comment || '', author],
|
||||
function (err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
|
||||
db.get(
|
||||
'SELECT SUM(minutes) as total FROM time_logs WHERE ticket_id = ?',
|
||||
[req.params.id],
|
||||
(err, row) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
|
||||
const totalTime = row.total || 0;
|
||||
db.run(
|
||||
'UPDATE tickets SET total_time = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[totalTime, req.params.id],
|
||||
err => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
logHistory(
|
||||
req.params.id,
|
||||
'Time Logged',
|
||||
`Added ${minutes} minutes by ${author}. Comment: ${comment || 'none'}`,
|
||||
author
|
||||
);
|
||||
// Send back the updated total_time to the frontend
|
||||
res.status(201).json({ message: 'Time logged', total_time: totalTime });
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
[req.params.id, minutes, comment || '', author]
|
||||
);
|
||||
});
|
||||
|
||||
const { total } = await dbGet(
|
||||
'SELECT SUM(minutes) as total FROM time_logs WHERE ticket_id = ?',
|
||||
[req.params.id]
|
||||
);
|
||||
const totalTime = total || 0;
|
||||
|
||||
await dbRun(
|
||||
'UPDATE tickets SET total_time = ?, updated_at = ? WHERE id = ?',
|
||||
[totalTime, utcNow(), req.params.id]
|
||||
);
|
||||
|
||||
logHistory(req.params.id, 'Time Logged', `Added ${minutes} min by ${author}. Comment: ${comment || 'none'}`, author);
|
||||
notifyUser('timeLogged', { ticketId: req.params.id, title: ticket.title, minutes, author, comment });
|
||||
|
||||
res.status(201).json({ message: 'Time logged', total_time: totalTime });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET time logs
|
||||
app.get('/api/tickets/:id/time', (req, res) => {
|
||||
db.all(
|
||||
'SELECT * FROM time_logs WHERE ticket_id = ? ORDER BY created_at DESC',
|
||||
[req.params.id],
|
||||
(err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json(rows);
|
||||
}
|
||||
);
|
||||
app.get('/api/tickets/:id/time', async (req, res) => {
|
||||
try {
|
||||
const rows = await dbAll(
|
||||
'SELECT * FROM time_logs WHERE ticket_id = ? ORDER BY created_at DESC',
|
||||
[req.params.id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// --- Start ---
|
||||
app.listen(port, () => {
|
||||
console.log(`Server läuft auf http://localhost:${port}`);
|
||||
console.log('Webhook URLs loaded:', {
|
||||
console.log(`Server running on http://localhost:${port}`);
|
||||
console.log('Webhooks:', {
|
||||
admin: ADMIN_WEBHOOK_URL ? 'Configured' : 'Not configured',
|
||||
user: USER_WEBHOOK_URL ? 'Configured' : 'Not configured'
|
||||
user: USER_WEBHOOK_URL ? 'Configured' : 'Not configured',
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user