const express = require('express'); const pool = require('../db'); const { requireAdmin } = require('../middleware/auth'); const router = express.Router(); router.use(requireAdmin); const MAX_LIMIT = 200; const DEFAULT_LIMIT = 50; function parseOptionalInt(raw) { if (raw == null || raw === '') return null; const n = parseInt(String(raw), 10); return Number.isFinite(n) ? n : null; } function parseOptionalDate(raw) { if (raw == null || raw === '') return null; const d = new Date(String(raw)); return Number.isNaN(d.getTime()) ? null : d; } function buildFilters(query) { const clauses = []; const params = []; const userId = parseOptionalInt(query.user_id); if (userId != null) { params.push(userId); clauses.push(`l.user_id = $${params.length}`); } const username = typeof query.username === 'string' && query.username.trim() ? query.username.trim() : null; if (username) { params.push(username); clauses.push(`u.username = $${params.length}`); } const action = typeof query.action === 'string' && query.action.trim() ? query.action.trim() : null; if (action) { params.push(action); clauses.push(`l.action = $${params.length}`); } const resourceType = typeof query.resource_type === 'string' && query.resource_type.trim() ? query.resource_type.trim() : null; if (resourceType) { params.push(resourceType); clauses.push(`l.resource_type = $${params.length}`); } const resourceId = parseOptionalInt(query.resource_id); if (resourceId != null) { params.push(resourceId); clauses.push(`l.resource_id = $${params.length}`); } const from = parseOptionalDate(query.from); if (from) { params.push(from.toISOString()); clauses.push(`l.created_at >= $${params.length}::timestamptz`); } const to = parseOptionalDate(query.to); if (to) { params.push(to.toISOString()); clauses.push(`l.created_at <= $${params.length}::timestamptz`); } const q = typeof query.q === 'string' && query.q.trim() ? query.q.trim() : null; if (q) { params.push(`%${q}%`); const idx = params.length; clauses.push(`( l.action ILIKE $${idx} OR l.resource_type ILIKE $${idx} OR u.username ILIKE $${idx} OR COALESCE(l.details::text, '') ILIKE $${idx} OR COALESCE(l.ip_address, '') ILIKE $${idx} )`); } return { where: clauses.length ? `WHERE ${clauses.join(' AND ')}` : '', params, }; } const RESOURCE_LABEL_SQL = ` CASE l.resource_type WHEN 'painting' THEN (SELECT title FROM paintings WHERE id = l.resource_id) WHEN 'artist' THEN (SELECT name FROM artists WHERE id = l.resource_id) WHEN 'user' THEN (SELECT username FROM users WHERE id = l.resource_id) WHEN 'tour' THEN (SELECT title FROM tours WHERE id = l.resource_id) WHEN 'movement' THEN (SELECT name FROM art_movements WHERE id = l.resource_id) ELSE NULL END `; router.get('/', async (req, res) => { try { const { where, params } = buildFilters(req.query); let limit = parseOptionalInt(req.query.limit) ?? DEFAULT_LIMIT; let offset = parseOptionalInt(req.query.offset) ?? 0; limit = Math.min(MAX_LIMIT, Math.max(1, limit)); offset = Math.max(0, offset); const countResult = await pool.query( `SELECT COUNT(*)::int AS total FROM curator_audit_log l JOIN users u ON u.id = l.user_id ${where}`, params ); const listParams = [...params, limit, offset]; const limitIdx = params.length + 1; const offsetIdx = params.length + 2; const { rows } = await pool.query( `SELECT l.id, l.created_at, l.user_id, u.username, u.role AS user_role, l.action, l.resource_type, l.resource_id, l.details, l.ip_address, (${RESOURCE_LABEL_SQL}) AS resource_label FROM curator_audit_log l JOIN users u ON u.id = l.user_id ${where} ORDER BY l.created_at DESC, l.id DESC LIMIT $${limitIdx} OFFSET $${offsetIdx}`, listParams ); res.json({ database: process.env.DB_NAME || null, total: countResult.rows[0].total, limit, offset, entries: rows.map((row) => ({ id: row.id, created_at: row.created_at, user_id: row.user_id, username: row.username, user_role: row.user_role, action: row.action, resource_type: row.resource_type, resource_id: row.resource_id, resource_label: row.resource_label, details: row.details, ip_address: row.ip_address, })), }); } catch (err) { console.error('Audit list error:', err.message); res.status(500).json({ error: 'Failed to load audit log' }); } }); router.get('/summary', async (req, res) => { try { const { where, params } = buildFilters(req.query); const [byUser, byAction, byResource, totals] = await Promise.all([ pool.query( `SELECT u.id AS user_id, u.username, u.role, COUNT(*)::int AS count FROM curator_audit_log l JOIN users u ON u.id = l.user_id ${where} GROUP BY u.id, u.username, u.role ORDER BY count DESC, u.username ASC`, params ), pool.query( `SELECT l.action, COUNT(*)::int AS count FROM curator_audit_log l JOIN users u ON u.id = l.user_id ${where} GROUP BY l.action ORDER BY count DESC, l.action ASC`, params ), pool.query( `SELECT l.resource_type, COUNT(*)::int AS count FROM curator_audit_log l JOIN users u ON u.id = l.user_id ${where} GROUP BY l.resource_type ORDER BY count DESC, l.resource_type ASC`, params ), pool.query( `SELECT COUNT(*)::int AS total, COUNT(*) FILTER (WHERE l.created_at >= NOW() - INTERVAL '24 hours')::int AS last_24h, COUNT(*) FILTER (WHERE l.created_at >= NOW() - INTERVAL '7 days')::int AS last_7d, MIN(l.created_at) AS oldest, MAX(l.created_at) AS newest FROM curator_audit_log l JOIN users u ON u.id = l.user_id ${where}`, params ), ]); const t = totals.rows[0] || {}; res.json({ database: process.env.DB_NAME || null, total: t.total || 0, last_24h: t.last_24h || 0, last_7d: t.last_7d || 0, oldest: t.oldest || null, newest: t.newest || null, by_user: byUser.rows, by_action: byAction.rows, by_resource_type: byResource.rows, }); } catch (err) { console.error('Audit summary error:', err.message); res.status(500).json({ error: 'Failed to load audit summary' }); } }); router.get('/meta', async (_req, res) => { try { const [users, actions, resourceTypes] = await Promise.all([ pool.query( `SELECT DISTINCT u.id, u.username, u.role FROM curator_audit_log l JOIN users u ON u.id = l.user_id ORDER BY u.username ASC` ), pool.query( `SELECT DISTINCT action FROM curator_audit_log ORDER BY action ASC` ), pool.query( `SELECT DISTINCT resource_type FROM curator_audit_log ORDER BY resource_type ASC` ), ]); res.json({ database: process.env.DB_NAME || null, users: users.rows, actions: actions.rows.map((r) => r.action), resource_types: resourceTypes.rows.map((r) => r.resource_type), }); } catch (err) { console.error('Audit meta error:', err.message); res.status(500).json({ error: 'Failed to load audit filters' }); } }); module.exports = router;