// routes/ivantiTodoQueue.js const express = require('express'); const VALID_WORKFLOW_TYPES = ['FP', 'Archer', 'CARD']; const VALID_STATUSES = ['pending', 'complete']; function isValidVendor(vendor) { return typeof vendor === 'string' && vendor.trim().length > 0 && vendor.length <= 200; } function createIvantiTodoQueueRouter(db, requireAuth) { const router = express.Router(); // GET /api/ivanti/todo-queue // Fetch current user's queue items, ordered by vendor then created_at router.get('/', requireAuth(db), (req, res) => { db.all( `SELECT * FROM ivanti_todo_queue WHERE user_id = ? ORDER BY vendor ASC, created_at ASC`, [req.user.id], (err, rows) => { if (err) { console.error('Error fetching todo queue:', err); return res.status(500).json({ error: 'Internal server error.' }); } // Parse cves_json back to array for each row const parsed = rows.map((r) => ({ ...r, cves: r.cves_json ? JSON.parse(r.cves_json) : [], })); res.json(parsed); } ); }); // POST /api/ivanti/todo-queue // Add a finding to the queue router.post('/', requireAuth(db), (req, res) => { const { finding_id, finding_title, cves, vendor, workflow_type } = req.body; if (!finding_id || typeof finding_id !== 'string' || finding_id.trim().length === 0) { return res.status(400).json({ error: 'finding_id is required.' }); } if (!isValidVendor(vendor)) { return res.status(400).json({ error: 'vendor is required (max 200 chars).' }); } if (!VALID_WORKFLOW_TYPES.includes(workflow_type)) { return res.status(400).json({ error: 'workflow_type must be FP or Archer.' }); } const cvesJson = Array.isArray(cves) ? JSON.stringify(cves) : null; const title = finding_title && typeof finding_title === 'string' ? finding_title.slice(0, 500) : null; db.run( `INSERT INTO ivanti_todo_queue (user_id, finding_id, finding_title, cves_json, vendor, workflow_type) VALUES (?, ?, ?, ?, ?, ?)`, [req.user.id, finding_id.trim(), title, cvesJson, vendor.trim(), workflow_type], function (err) { if (err) { console.error('Error adding to queue:', err); return res.status(500).json({ error: 'Internal server error.' }); } db.get( 'SELECT * FROM ivanti_todo_queue WHERE id = ?', [this.lastID], (err2, row) => { if (err2 || !row) { return res.status(201).json({ id: this.lastID, message: 'Added to queue.' }); } res.status(201).json({ ...row, cves: row.cves_json ? JSON.parse(row.cves_json) : [] }); } ); } ); }); // PUT /api/ivanti/todo-queue/:id // Update vendor, workflow_type, or status — scoped to current user router.put('/:id', requireAuth(db), (req, res) => { const { id } = req.params; const { vendor, workflow_type, status } = req.body; if (vendor !== undefined && !isValidVendor(vendor)) { return res.status(400).json({ error: 'vendor must be a non-empty string (max 200 chars).' }); } if (workflow_type !== undefined && !VALID_WORKFLOW_TYPES.includes(workflow_type)) { return res.status(400).json({ error: 'workflow_type must be FP or Archer.' }); } if (status !== undefined && !VALID_STATUSES.includes(status)) { return res.status(400).json({ error: 'status must be pending or complete.' }); } db.get( 'SELECT * FROM ivanti_todo_queue WHERE id = ? AND user_id = ?', [id, req.user.id], (err, existing) => { if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!existing) { return res.status(404).json({ error: 'Queue item not found.' }); } const updates = []; const params = []; if (vendor !== undefined) { updates.push('vendor = ?'); params.push(vendor.trim()); } if (workflow_type !== undefined) { updates.push('workflow_type = ?'); params.push(workflow_type); } if (status !== undefined) { updates.push('status = ?'); params.push(status); } if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update.' }); } updates.push('updated_at = CURRENT_TIMESTAMP'); params.push(id, req.user.id); db.run( `UPDATE ivanti_todo_queue SET ${updates.join(', ')} WHERE id = ? AND user_id = ?`, params, function (err2) { if (err2) { console.error(err2); return res.status(500).json({ error: 'Internal server error.' }); } db.get( 'SELECT * FROM ivanti_todo_queue WHERE id = ?', [id], (err3, row) => { if (err3 || !row) { return res.json({ message: 'Queue item updated.' }); } res.json({ ...row, cves: row.cves_json ? JSON.parse(row.cves_json) : [] }); } ); } ); } ); }); // DELETE /api/ivanti/todo-queue/completed // Bulk-delete all completed items for the current user // IMPORTANT: This route must be registered BEFORE DELETE /:id router.delete('/completed', requireAuth(db), (req, res) => { db.run( "DELETE FROM ivanti_todo_queue WHERE user_id = ? AND status = 'complete'", [req.user.id], function (err) { if (err) { console.error('Error clearing completed queue items:', err); return res.status(500).json({ error: 'Internal server error.' }); } res.json({ message: 'Completed items cleared.', deleted: this.changes }); } ); }); // DELETE /api/ivanti/todo-queue/:id // Delete a single item — scoped to current user router.delete('/:id', requireAuth(db), (req, res) => { const { id } = req.params; db.get( 'SELECT id FROM ivanti_todo_queue WHERE id = ? AND user_id = ?', [id, req.user.id], (err, row) => { if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!row) { return res.status(404).json({ error: 'Queue item not found.' }); } db.run( 'DELETE FROM ivanti_todo_queue WHERE id = ? AND user_id = ?', [id, req.user.id], function (err2) { if (err2) { console.error(err2); return res.status(500).json({ error: 'Internal server error.' }); } res.json({ message: 'Queue item deleted.' }); } ); } ); }); return router; } module.exports = createIvantiTodoQueueRouter;