feat: add return classification for archive chart, CARD API integration, compliance charts, systemd services
This commit is contained in:
615
backend/routes/cardApi.js
Normal file
615
backend/routes/cardApi.js
Normal file
@@ -0,0 +1,615 @@
|
||||
// CARD Asset Ownership API Routes
|
||||
// Proxies CARD operations (confirm, decline, redirect, search) and orchestrates
|
||||
// the two-step update_token flow for mutations.
|
||||
|
||||
const express = require('express');
|
||||
const { requireGroup } = require('../middleware/auth');
|
||||
const logAudit = require('../helpers/auditLog');
|
||||
const {
|
||||
isConfigured,
|
||||
missingVars,
|
||||
getTeams,
|
||||
getTeamAssets,
|
||||
getOwner,
|
||||
confirmAsset,
|
||||
declineAsset,
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
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.' });
|
||||
}
|
||||
if (msg.includes('HTTP 403')) {
|
||||
return res.status(403).json({ error: 'CARD access denied. The service account may not be onboarded with the CARD team.' });
|
||||
}
|
||||
if (msg.includes('HTTP 525')) {
|
||||
return res.status(502).json({ error: 'CARD LDAP error. The service account may not be provisioned correctly.' });
|
||||
}
|
||||
}
|
||||
|
||||
// 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.' });
|
||||
}
|
||||
if (msg.includes('403')) {
|
||||
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) {
|
||||
const router = express.Router();
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// GET /status
|
||||
// Returns whether the CARD API integration is configured.
|
||||
// -------------------------------------------------------------------
|
||||
router.get('/status', requireAuth(db), (req, res) => {
|
||||
if (!isConfigured) {
|
||||
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) => {
|
||||
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: ... }
|
||||
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 };
|
||||
}
|
||||
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) => {
|
||||
if (!isConfigured) {
|
||||
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
|
||||
}
|
||||
|
||||
const { teamName } = req.params;
|
||||
const { disposition, page, page_size } = req.query;
|
||||
|
||||
if (!disposition) {
|
||||
return res.status(400).json({ error: 'disposition query parameter is required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await getTeamAssets(teamName, {
|
||||
disposition,
|
||||
page: page ? parseInt(page, 10) : undefined,
|
||||
pageSize: page_size ? parseInt(page_size, 10) : 50,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
let 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, {
|
||||
userId: req.user.id,
|
||||
username: req.user.username,
|
||||
action: 'card_search',
|
||||
entityType: 'card_asset',
|
||||
entityId: teamName,
|
||||
details: { disposition, resultCount },
|
||||
ipAddress: req.ip,
|
||||
});
|
||||
|
||||
return res.json(body);
|
||||
}
|
||||
|
||||
let errorBody;
|
||||
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) => {
|
||||
if (!isConfigured) {
|
||||
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
|
||||
}
|
||||
|
||||
const { assetId } = req.params;
|
||||
|
||||
try {
|
||||
const result = await getOwner(assetId);
|
||||
|
||||
if (result.ok) {
|
||||
let 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 };
|
||||
}
|
||||
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) => {
|
||||
if (!isConfigured) {
|
||||
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
|
||||
}
|
||||
|
||||
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.' });
|
||||
}
|
||||
if (!assetId || typeof assetId !== 'string' || !assetId.trim()) {
|
||||
return res.status(400).json({ error: 'assetId is required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate queue item
|
||||
const item = await dbGet(db,
|
||||
'SELECT * FROM ivanti_todo_queue WHERE id = ? AND user_id = ? AND workflow_type = ?',
|
||||
[queueItemId, req.user.id, 'CARD']
|
||||
);
|
||||
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: 'Queue item not found.' });
|
||||
}
|
||||
if (item.status !== 'pending') {
|
||||
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,
|
||||
});
|
||||
let errorBody;
|
||||
try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
|
||||
return res.status(ownerResult.status).json(errorBody);
|
||||
}
|
||||
|
||||
let ownerData;
|
||||
try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
|
||||
const updateToken = ownerData.owner && ownerData.owner.update_token;
|
||||
|
||||
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,
|
||||
});
|
||||
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 = ?",
|
||||
[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,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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) => {
|
||||
if (!isConfigured) {
|
||||
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
|
||||
}
|
||||
|
||||
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.' });
|
||||
}
|
||||
if (!assetId || typeof assetId !== 'string' || !assetId.trim()) {
|
||||
return res.status(400).json({ error: 'assetId is required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate queue item
|
||||
const item = await dbGet(db,
|
||||
'SELECT * FROM ivanti_todo_queue WHERE id = ? AND user_id = ? AND workflow_type = ?',
|
||||
[queueItemId, req.user.id, 'CARD']
|
||||
);
|
||||
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: 'Queue item not found.' });
|
||||
}
|
||||
if (item.status !== 'pending') {
|
||||
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,
|
||||
});
|
||||
let errorBody;
|
||||
try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
|
||||
return res.status(ownerResult.status).json(errorBody);
|
||||
}
|
||||
|
||||
let ownerData;
|
||||
try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
|
||||
const updateToken = ownerData.owner && ownerData.owner.update_token;
|
||||
|
||||
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,
|
||||
});
|
||||
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 = ?",
|
||||
[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,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
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) => {
|
||||
if (!isConfigured) {
|
||||
return res.status(503).json({ error: 'CARD API is not configured.', missingVars });
|
||||
}
|
||||
|
||||
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.' });
|
||||
}
|
||||
if (!toTeam || typeof toTeam !== 'string' || !toTeam.trim()) {
|
||||
return res.status(400).json({ error: 'toTeam is required.' });
|
||||
}
|
||||
if (!assetId || typeof assetId !== 'string' || !assetId.trim()) {
|
||||
return res.status(400).json({ error: 'assetId is required.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate queue item
|
||||
const item = await dbGet(db,
|
||||
'SELECT * FROM ivanti_todo_queue WHERE id = ? AND user_id = ? AND workflow_type = ?',
|
||||
[queueItemId, req.user.id, 'CARD']
|
||||
);
|
||||
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: 'Queue item not found.' });
|
||||
}
|
||||
if (item.status !== 'pending') {
|
||||
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,
|
||||
});
|
||||
let errorBody;
|
||||
try { errorBody = JSON.parse(ownerResult.body); } catch (_) { errorBody = { error: ownerResult.body }; }
|
||||
return res.status(ownerResult.status).json(errorBody);
|
||||
}
|
||||
|
||||
let ownerData;
|
||||
try { ownerData = JSON.parse(ownerResult.body); } catch (_) { ownerData = {}; }
|
||||
const updateToken = ownerData.owner && ownerData.owner.update_token;
|
||||
|
||||
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,
|
||||
});
|
||||
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 = ?",
|
||||
[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,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
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,
|
||||
});
|
||||
return handleCardError(err, res);
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
module.exports = createCardApiRouter;
|
||||
Reference in New Issue
Block a user