// Ivanti Archive Routes — list, stats, and transition history for archived findings const express = require('express'); const VALID_STATES = ['ACTIVE', 'ARCHIVED', 'RETURNED', 'CLOSED']; function createIvantiArchiveRouter(db, requireAuth) { const router = express.Router(); // All routes require authentication router.use(requireAuth(db)); /** * GET / * List archive records with optional state filtering. * * @query {string} [state] - Filter by lifecycle state (ACTIVE, ARCHIVED, RETURNED, CLOSED) * @returns {Object} 200 - { archives: Array, total: number } * @returns {Object} 400 - { error: string } when state param is invalid * @returns {Object} 500 - { error: string } on database failure */ router.get('/', async (req, res) => { const { state } = req.query; if (state && !VALID_STATES.includes(state)) { return res.status(400).json({ error: 'Invalid state parameter. Valid values: ACTIVE, ARCHIVED, RETURNED, CLOSED' }); } try { let query = 'SELECT * FROM ivanti_finding_archives'; const params = []; if (state) { query += ' WHERE current_state = ?'; params.push(state); } query += ' ORDER BY last_transition_at DESC'; const archives = await new Promise((resolve, reject) => { db.all(query, params, (err, rows) => { if (err) reject(err); else resolve(rows || []); }); }); res.json({ archives, total: archives.length }); } catch (err) { console.error('Archive list error:', err); res.status(500).json({ error: 'Failed to fetch archive records' }); } }); /** * GET /stats * Summary counts of archive records by lifecycle state. * ACTIVE is implicit: live findings in the cache that have no ARCHIVED/RETURNED archive record. * * @returns {Object} 200 - { ACTIVE: number, ARCHIVED: number, RETURNED: number, CLOSED: number, total: number } * @returns {Object} 500 - { error: string } on database failure */ router.get('/stats', async (req, res) => { try { // Count archive records by state const rows = await new Promise((resolve, reject) => { db.all( `SELECT current_state, COUNT(*) as count FROM ivanti_finding_archives GROUP BY current_state`, (err, rows) => { if (err) reject(err); else resolve(rows || []); } ); }); const stats = { ACTIVE: 0, ARCHIVED: 0, RETURNED: 0, CLOSED: 0 }; for (const row of rows) { if (stats.hasOwnProperty(row.current_state)) { stats[row.current_state] = row.count; } } // Compute ACTIVE: total live findings minus those with ARCHIVED or RETURNED records const cacheRow = await new Promise((resolve, reject) => { db.get( 'SELECT total FROM ivanti_findings_cache WHERE id = 1', (err, row) => { if (err) reject(err); else resolve(row); } ); }); const liveFindingsCount = (cacheRow && cacheRow.total) || 0; // Findings that are ARCHIVED or RETURNED are "missing" from the live set, // so ACTIVE = live count (all findings currently present in sync results) stats.ACTIVE = liveFindingsCount; const total = stats.ACTIVE + stats.ARCHIVED + stats.RETURNED + stats.CLOSED; res.json({ ...stats, total }); } catch (err) { console.error('Archive stats error:', err); res.status(500).json({ error: 'Failed to fetch archive stats' }); } }); /** * GET /:findingId/history * Transition history for a specific archived finding, ordered by most recent first. * Returns an empty transitions array if the finding has no archive record. * * @param {string} findingId - Ivanti finding identifier (route param) * @returns {Object} 200 - { finding_id: string, transitions: Array } * @returns {Object} 500 - { error: string } on database failure */ router.get('/:findingId/history', async (req, res) => { const { findingId } = req.params; try { const archive = await new Promise((resolve, reject) => { db.get( 'SELECT id FROM ivanti_finding_archives WHERE finding_id = ?', [findingId], (err, row) => { if (err) reject(err); else resolve(row); } ); }); if (!archive) { return res.json({ finding_id: findingId, transitions: [] }); } const transitions = await new Promise((resolve, reject) => { db.all( `SELECT * FROM ivanti_archive_transitions WHERE archive_id = ? ORDER BY transitioned_at DESC`, [archive.id], (err, rows) => { if (err) reject(err); else resolve(rows || []); } ); }); res.json({ finding_id: findingId, transitions }); } catch (err) { console.error('Archive history error:', err); res.status(500).json({ error: 'Failed to fetch transition history' }); } }); return router; } module.exports = createIvantiArchiveRouter;