feat(postgres): migrate all route files from SQLite to pg pool

- All 16 route files now import pool from ../db directly
- Removed db parameter from all factory functions
- All callbacks replaced with async/await pool.query()
- All ? placeholders converted to $1, $2... numbered params
- datetime('now') → NOW(), INSERT OR IGNORE → ON CONFLICT DO NOTHING
- LIKE → ILIKE for case-insensitive searches
- Error detection: err.code === '23505' for unique violations
- server.js no longer passes pool/db/requireAuth to route factories
- Only ivantiFindings.js still receives pool (pending task 8 rewrite)
This commit is contained in:
Jordan Ramos
2026-05-06 11:44:17 -06:00
parent 845d843e71
commit 33927b150b
18 changed files with 2164 additions and 4432 deletions

View File

@@ -3,7 +3,8 @@
// the two-step update_token flow for mutations.
const express = require('express');
const { requireGroup } = require('../middleware/auth');
const pool = require('../db');
const { requireAuth, requireGroup } = require('../middleware/auth');
const logAudit = require('../helpers/auditLog');
const {
isConfigured,
@@ -16,21 +17,6 @@ const {
redirectAsset,
} = require('../helpers/cardApi');
// ---------------------------------------------------------------------------
// DB helpers — promise wrappers for callback-based SQLite API
// ---------------------------------------------------------------------------
function dbRun(db, sql, params = []) {
return new Promise((resolve, reject) => {
db.run(sql, params, function (err) { if (err) reject(err); else resolve(this); });
});
}
function dbGet(db, sql, params = []) {
return new Promise((resolve, reject) => {
db.get(sql, params, (err, row) => { if (err) reject(err); else resolve(row); });
});
}
// ---------------------------------------------------------------------------
// Error classification — maps CARD API / token errors to client responses
// ---------------------------------------------------------------------------
@@ -38,7 +24,6 @@ function handleCardError(err, res) {
const msg = err.message || String(err);
console.error('[card-api]', msg);
// Token endpoint errors (from acquireToken rejections)
if (msg.includes('Token acquisition failed')) {
if (msg.includes('HTTP 401')) {
return res.status(401).json({ error: 'CARD authorization failed. Check service account credentials.' });
@@ -51,7 +36,6 @@ function handleCardError(err, res) {
}
}
// API call errors (after automatic 401 retry in helper)
if (msg.includes('401')) {
return res.status(401).json({ error: 'CARD token expired or invalid. The request has been retried once automatically.' });
}
@@ -59,73 +43,47 @@ function handleCardError(err, res) {
return res.status(403).json({ error: 'Insufficient CARD permissions for this operation.' });
}
// Catch-all
return res.status(502).json({ error: 'CARD API request failed.', details: msg });
}
// ---------------------------------------------------------------------------
// Router factory
// ---------------------------------------------------------------------------
function createCardApiRouter(db, requireAuth) {
function createCardApiRouter() {
const router = express.Router();
// -------------------------------------------------------------------
// GET /status
// Returns whether the CARD API integration is configured.
// -------------------------------------------------------------------
router.get('/status', requireAuth(db), (req, res) => {
router.get('/status', requireAuth(), (req, res) => {
if (!isConfigured) {
return res.status(503).json({
configured: false,
error: 'CARD API is not configured.',
missingVars,
});
return res.status(503).json({ configured: false, error: 'CARD API is not configured.', missingVars });
}
res.json({ configured: true });
});
// -------------------------------------------------------------------
// GET /teams
// Proxy CARD teams list.
// -------------------------------------------------------------------
router.get('/teams', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
router.get('/teams', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
}
try {
const result = await getTeams();
if (result.ok) {
let body;
try {
body = JSON.parse(result.body);
} catch (_) {
body = result.body;
}
// CARD API wraps teams in { teams: [...], response_time: ... }
try { body = JSON.parse(result.body); } catch (_) { body = result.body; }
const teams = Array.isArray(body) ? body : (body && body.teams) || [];
return res.json(teams);
}
// Forward CARD error status
let errorBody;
try {
errorBody = JSON.parse(result.body);
} catch (_) {
errorBody = { error: result.body };
}
try { errorBody = JSON.parse(result.body); } catch (_) { errorBody = { error: result.body }; }
return res.status(result.status).json(errorBody);
} catch (err) {
return handleCardError(err, res);
}
});
// -------------------------------------------------------------------
// GET /teams/:teamName/assets
// Proxy team assets with required disposition filter.
// -------------------------------------------------------------------
router.get('/teams/:teamName/assets', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
router.get('/teams/:teamName/assets', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
}
@@ -146,20 +104,15 @@ function createCardApiRouter(db, requireAuth) {
if (result.ok) {
let body;
try {
body = JSON.parse(result.body);
} catch (_) {
body = result.body;
}
try { body = JSON.parse(result.body); } catch (_) { body = result.body; }
// Audit log for asset search (fire-and-forget)
let resultCount = 0;
if (body && typeof body === 'object' && typeof body.total === 'number') {
resultCount = body.total;
} else if (body && Array.isArray(body.assets)) {
resultCount = body.assets.length;
}
logAudit(db, {
logAudit({
userId: req.user.id,
username: req.user.username,
action: 'card_search',
@@ -173,22 +126,15 @@ function createCardApiRouter(db, requireAuth) {
}
let errorBody;
try {
errorBody = JSON.parse(result.body);
} catch (_) {
errorBody = { error: result.body };
}
try { errorBody = JSON.parse(result.body); } catch (_) { errorBody = { error: result.body }; }
return res.status(result.status).json(errorBody);
} catch (err) {
return handleCardError(err, res);
}
});
// -------------------------------------------------------------------
// GET /owner/:assetId
// Proxy owner record lookup.
// -------------------------------------------------------------------
router.get('/owner/:assetId', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
router.get('/owner/:assetId', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
}
@@ -197,34 +143,21 @@ function createCardApiRouter(db, requireAuth) {
try {
const result = await getOwner(assetId);
if (result.ok) {
let body;
try {
body = JSON.parse(result.body);
} catch (_) {
body = result.body;
}
try { body = JSON.parse(result.body); } catch (_) { body = result.body; }
return res.json(body);
}
let errorBody;
try {
errorBody = JSON.parse(result.body);
} catch (_) {
errorBody = { error: result.body };
}
try { errorBody = JSON.parse(result.body); } catch (_) { errorBody = { error: result.body }; }
return res.status(result.status).json(errorBody);
} catch (err) {
return handleCardError(err, res);
}
});
// -------------------------------------------------------------------
// POST /queue/:queueItemId/confirm
// Confirm asset to a team via CARD API.
// -------------------------------------------------------------------
router.post('/queue/:queueItemId/confirm', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
router.post('/queue/:queueItemId/confirm', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
}
@@ -232,7 +165,6 @@ function createCardApiRouter(db, requireAuth) {
const { queueItemId } = req.params;
const { teamName, assetId, comment } = req.body;
// Validate required fields
if (!teamName || typeof teamName !== 'string' || !teamName.trim()) {
return res.status(400).json({ error: 'teamName is required.' });
}
@@ -241,11 +173,11 @@ function createCardApiRouter(db, requireAuth) {
}
try {
// Validate queue item
const item = await dbGet(db,
'SELECT * FROM ivanti_todo_queue WHERE id = ? AND user_id = ? AND workflow_type = ?',
const { rows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',
[queueItemId, req.user.id, 'CARD']
);
const item = rows[0];
if (!item) {
return res.status(404).json({ error: 'Queue item not found.' });
@@ -254,20 +186,10 @@ function createCardApiRouter(db, requireAuth) {
return res.status(400).json({ error: 'Only pending queue items can be executed.' });
}
// Step 1: Get owner record for update_token
const ownerResult = await getOwner(assetId);
if (!ownerResult.ok) {
const errMsg = `Failed to fetch owner record: HTTP ${ownerResult.status}`;
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: ownerResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: ownerResult.status }, ipAddress: req.ip });
let errorBody;
try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
return res.status(ownerResult.status).json(errorBody);
@@ -279,82 +201,39 @@ function createCardApiRouter(db, requireAuth) {
if (!updateToken) {
const errMsg = 'update_token not found in owner record.';
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: null },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: null }, ipAddress: req.ip });
return res.status(502).json({ error: 'CARD API request failed.', details: errMsg });
}
// Step 2: Execute confirm mutation
const confirmResult = await confirmAsset(assetId, teamName.trim(), updateToken, comment || '');
if (confirmResult.ok) {
// Update queue item to complete
await dbRun(db,
"UPDATE ivanti_todo_queue SET status = 'complete', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
await pool.query(
"UPDATE ivanti_todo_queue SET status = 'complete', updated_at = NOW() WHERE id = $1",
[queueItemId]
);
let cardResponse;
try { cardResponse = JSON.parse(confirmResult.body); } catch (_) { cardResponse = confirmResult.body; }
// Audit log (fire-and-forget)
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_confirm',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { assetId, teamName: teamName.trim(), comment: comment || '', cardStatus: confirmResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_confirm', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { assetId, teamName: teamName.trim(), comment: comment || '', cardStatus: confirmResult.status }, ipAddress: req.ip });
return res.json({ success: true, cardResponse });
}
// Mutation failed — leave queue item as pending
const errMsg = `Confirm failed: HTTP ${confirmResult.status}`;
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: confirmResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'confirm', assetId, error: errMsg, cardStatus: confirmResult.status }, ipAddress: req.ip });
let errorBody;
try { errorBody = JSON.parse(confirmResult.body); } catch (_) { errorBody = { error: confirmResult.body }; }
return res.status(confirmResult.status).json(errorBody);
} catch (err) {
console.error('[card-api] Confirm error:', err.message);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'confirm', assetId, error: err.message, cardStatus: null },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'confirm', assetId, error: err.message, cardStatus: null }, ipAddress: req.ip });
return handleCardError(err, res);
}
});
// -------------------------------------------------------------------
// POST /queue/:queueItemId/decline
// Decline asset from a team via CARD API.
// -------------------------------------------------------------------
router.post('/queue/:queueItemId/decline', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
router.post('/queue/:queueItemId/decline', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
}
@@ -362,7 +241,6 @@ function createCardApiRouter(db, requireAuth) {
const { queueItemId } = req.params;
const { teamName, assetId, comment } = req.body;
// Validate required fields
if (!teamName || typeof teamName !== 'string' || !teamName.trim()) {
return res.status(400).json({ error: 'teamName is required.' });
}
@@ -371,11 +249,11 @@ function createCardApiRouter(db, requireAuth) {
}
try {
// Validate queue item
const item = await dbGet(db,
'SELECT * FROM ivanti_todo_queue WHERE id = ? AND user_id = ? AND workflow_type = ?',
const { rows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',
[queueItemId, req.user.id, 'CARD']
);
const item = rows[0];
if (!item) {
return res.status(404).json({ error: 'Queue item not found.' });
@@ -384,20 +262,10 @@ function createCardApiRouter(db, requireAuth) {
return res.status(400).json({ error: 'Only pending queue items can be executed.' });
}
// Step 1: Get owner record for update_token
const ownerResult = await getOwner(assetId);
if (!ownerResult.ok) {
const errMsg = `Failed to fetch owner record: HTTP ${ownerResult.status}`;
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'decline', assetId, error: errMsg, cardStatus: ownerResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'decline', assetId, error: errMsg, cardStatus: ownerResult.status }, ipAddress: req.ip });
let errorBody;
try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
return res.status(ownerResult.status).json(errorBody);
@@ -409,80 +277,39 @@ function createCardApiRouter(db, requireAuth) {
if (!updateToken) {
const errMsg = 'update_token not found in owner record.';
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'decline', assetId, error: errMsg, cardStatus: null },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'decline', assetId, error: errMsg, cardStatus: null }, ipAddress: req.ip });
return res.status(502).json({ error: 'CARD API request failed.', details: errMsg });
}
// Step 2: Execute decline mutation
const declineResult = await declineAsset(assetId, teamName.trim(), updateToken, comment || '');
if (declineResult.ok) {
await dbRun(db,
"UPDATE ivanti_todo_queue SET status = 'complete', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
await pool.query(
"UPDATE ivanti_todo_queue SET status = 'complete', updated_at = NOW() WHERE id = $1",
[queueItemId]
);
let cardResponse;
try { cardResponse = JSON.parse(declineResult.body); } catch (_) { cardResponse = declineResult.body; }
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_decline',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { assetId, teamName: teamName.trim(), comment: comment || '', cardStatus: declineResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_decline', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { assetId, teamName: teamName.trim(), comment: comment || '', cardStatus: declineResult.status }, ipAddress: req.ip });
return res.json({ success: true, cardResponse });
}
// Mutation failed
const errMsg = `Decline failed: HTTP ${declineResult.status}`;
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'decline', assetId, error: errMsg, cardStatus: declineResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'decline', assetId, error: errMsg, cardStatus: declineResult.status }, ipAddress: req.ip });
let errorBody;
try { errorBody = JSON.parse(declineResult.body); } catch (_) { errorBody = { error: declineResult.body }; }
return res.status(declineResult.status).json(errorBody);
} catch (err) {
console.error('[card-api] Decline error:', err.message);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'decline', assetId, error: err.message, cardStatus: null },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'decline', assetId, error: err.message, cardStatus: null }, ipAddress: req.ip });
return handleCardError(err, res);
}
});
// -------------------------------------------------------------------
// POST /queue/:queueItemId/redirect
// Redirect asset from one team to another via CARD API.
// -------------------------------------------------------------------
router.post('/queue/:queueItemId/redirect', requireAuth(db), requireGroup('Admin', 'Standard_User'), async (req, res) => {
router.post('/queue/:queueItemId/redirect', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
}
@@ -490,7 +317,6 @@ function createCardApiRouter(db, requireAuth) {
const { queueItemId } = req.params;
const { fromTeam, toTeam, assetId } = req.body;
// Validate required fields
if (!fromTeam || typeof fromTeam !== 'string' || !fromTeam.trim()) {
return res.status(400).json({ error: 'fromTeam is required.' });
}
@@ -502,11 +328,11 @@ function createCardApiRouter(db, requireAuth) {
}
try {
// Validate queue item
const item = await dbGet(db,
'SELECT * FROM ivanti_todo_queue WHERE id = ? AND user_id = ? AND workflow_type = ?',
const { rows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',
[queueItemId, req.user.id, 'CARD']
);
const item = rows[0];
if (!item) {
return res.status(404).json({ error: 'Queue item not found.' });
@@ -515,20 +341,10 @@ function createCardApiRouter(db, requireAuth) {
return res.status(400).json({ error: 'Only pending queue items can be executed.' });
}
// Step 1: Get owner record for update_token
const ownerResult = await getOwner(assetId);
if (!ownerResult.ok) {
const errMsg = `Failed to fetch owner record: HTTP ${ownerResult.status}`;
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: ownerResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: ownerResult.status }, ipAddress: req.ip });
let errorBody;
try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
return res.status(ownerResult.status).json(errorBody);
@@ -540,71 +356,33 @@ function createCardApiRouter(db, requireAuth) {
if (!updateToken) {
const errMsg = 'update_token not found in owner record.';
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: null },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: null }, ipAddress: req.ip });
return res.status(502).json({ error: 'CARD API request failed.', details: errMsg });
}
// Step 2: Execute redirect mutation
const redirectResult = await redirectAsset(assetId, fromTeam.trim(), toTeam.trim(), updateToken);
if (redirectResult.ok) {
await dbRun(db,
"UPDATE ivanti_todo_queue SET status = 'complete', updated_at = CURRENT_TIMESTAMP WHERE id = ?",
await pool.query(
"UPDATE ivanti_todo_queue SET status = 'complete', updated_at = NOW() WHERE id = $1",
[queueItemId]
);
let cardResponse;
try { cardResponse = JSON.parse(redirectResult.body); } catch (_) { cardResponse = redirectResult.body; }
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_redirect',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { assetId, fromTeam: fromTeam.trim(), toTeam: toTeam.trim(), cardStatus: redirectResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_redirect', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { assetId, fromTeam: fromTeam.trim(), toTeam: toTeam.trim(), cardStatus: redirectResult.status }, ipAddress: req.ip });
return res.json({ success: true, cardResponse });
}
// Mutation failed
const errMsg = `Redirect failed: HTTP ${redirectResult.status}`;
console.error('[card-api]', errMsg);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: redirectResult.status },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'redirect', assetId, error: errMsg, cardStatus: redirectResult.status }, ipAddress: req.ip });
let errorBody;
try { errorBody = JSON.parse(redirectResult.body); } catch (_) { errorBody = { error: redirectResult.body }; }
return res.status(redirectResult.status).json(errorBody);
} catch (err) {
console.error('[card-api] Redirect error:', err.message);
logAudit(db, {
userId: req.user.id,
username: req.user.username,
action: 'card_action_failed',
entityType: 'ivanti_todo_queue',
entityId: String(queueItemId),
details: { actionType: 'redirect', assetId, error: err.message, cardStatus: null },
ipAddress: req.ip,
});
logAudit({ userId: req.user.id, username: req.user.username, action: 'card_action_failed', entityType: 'ivanti_todo_queue', entityId: String(queueItemId), details: { actionType: 'redirect', assetId, error: err.message, cardStatus: null }, ipAddress: req.ip });
return handleCardError(err, res);
}
});