Auto-resolve bare IP to CARD asset ID with suffix lookup

The CARD API requires asset IDs in the format {IP}-{SUFFIX} (e.g.,
10.240.78.110-CTEC) but the frontend only has the bare IP. Add
resolveAssetId() helper that tries known suffixes (CTEC, NATL,
CHTR, COML, RESI, WIFI, VOIP) via owner lookup until one succeeds.

Apply resolution to confirm, decline, and redirect handlers so
they accept bare IPs from the frontend and resolve them
automatically before calling the CARD mutation APIs.
This commit is contained in:
Jordan Ramos
2026-05-27 18:56:40 -06:00
parent bd772087c4
commit 8fc7c33cff
2 changed files with 75 additions and 6 deletions

View File

@@ -290,6 +290,43 @@ async function redirectAsset(assetId, fromTeam, toTeam, updateToken) {
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 }; return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
} }
/**
* Resolve a bare IP address to a full CARD asset ID by trying known suffixes.
* Returns the first asset ID that returns a valid owner record, or null if none found.
*/
async function resolveAssetId(ip) {
const SUFFIXES = ['CTEC', 'NATL', 'CHTR', 'COML', 'RESI', 'WIFI', 'VOIP'];
const trimmedIp = (ip || '').trim();
if (!trimmedIp) return null;
// If it already has a suffix (contains a dash followed by letters), use as-is
if (/\d+-[A-Z]+$/i.test(trimmedIp)) {
const result = await getOwner(trimmedIp);
if (result.ok) return trimmedIp;
}
// Try each suffix
for (const suffix of SUFFIXES) {
const candidate = `${trimmedIp}-${suffix}`;
try {
const result = await getOwner(candidate);
if (result.ok) return candidate;
} catch (_) {
// Continue to next suffix
}
}
// Try bare IP as last resort
try {
const result = await getOwner(trimmedIp);
if (result.ok) return trimmedIp;
} catch (_) {
// Not found
}
return null;
}
module.exports = { module.exports = {
isConfigured, isConfigured,
missingVars, missingVars,
@@ -304,4 +341,5 @@ module.exports = {
declineAsset, declineAsset,
redirectAsset, redirectAsset,
invalidateToken, invalidateToken,
resolveAssetId,
}; };

View File

@@ -15,6 +15,7 @@ const {
confirmAsset, confirmAsset,
declineAsset, declineAsset,
redirectAsset, redirectAsset,
resolveAssetId,
} = require('../helpers/cardApi'); } = require('../helpers/cardApi');
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -213,15 +214,25 @@ function createCardApiRouter() {
} }
const { queueItemId } = req.params; const { queueItemId } = req.params;
const { teamName, assetId, comment } = req.body; const { teamName, assetId: rawAssetId, comment } = req.body;
if (!teamName || typeof teamName !== 'string' || !teamName.trim()) { if (!teamName || typeof teamName !== 'string' || !teamName.trim()) {
return res.status(400).json({ error: 'teamName is required.' }); return res.status(400).json({ error: 'teamName is required.' });
} }
if (!assetId || typeof assetId !== 'string' || !assetId.trim()) { if (!rawAssetId || typeof rawAssetId !== 'string' || !rawAssetId.trim()) {
return res.status(400).json({ error: 'assetId is required.' }); return res.status(400).json({ error: 'assetId is required.' });
} }
// Resolve bare IP to full CARD asset ID
let assetId = rawAssetId.trim();
if (!/\d+-[A-Z]+$/i.test(assetId)) {
const resolved = await resolveAssetId(assetId);
if (!resolved) {
return res.status(404).json({ error: `Asset not found in CARD for IP: ${assetId}` });
}
assetId = resolved;
}
try { try {
const { rows } = await pool.query( const { rows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3', 'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',
@@ -305,15 +316,25 @@ function createCardApiRouter() {
} }
const { queueItemId } = req.params; const { queueItemId } = req.params;
const { teamName, assetId, comment } = req.body; const { teamName, assetId: rawAssetId, comment } = req.body;
if (!teamName || typeof teamName !== 'string' || !teamName.trim()) { if (!teamName || typeof teamName !== 'string' || !teamName.trim()) {
return res.status(400).json({ error: 'teamName is required.' }); return res.status(400).json({ error: 'teamName is required.' });
} }
if (!assetId || typeof assetId !== 'string' || !assetId.trim()) { if (!rawAssetId || typeof rawAssetId !== 'string' || !rawAssetId.trim()) {
return res.status(400).json({ error: 'assetId is required.' }); return res.status(400).json({ error: 'assetId is required.' });
} }
// Resolve bare IP to full CARD asset ID
let assetId = rawAssetId.trim();
if (!/\d+-[A-Z]+$/i.test(assetId)) {
const resolved = await resolveAssetId(assetId);
if (!resolved) {
return res.status(404).json({ error: `Asset not found in CARD for IP: ${assetId}` });
}
assetId = resolved;
}
try { try {
const { rows } = await pool.query( const { rows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3', 'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',
@@ -397,7 +418,7 @@ function createCardApiRouter() {
} }
const { queueItemId } = req.params; const { queueItemId } = req.params;
const { fromTeam, toTeam, assetId } = req.body; const { fromTeam, toTeam, assetId: rawAssetId } = req.body;
if (!fromTeam || typeof fromTeam !== 'string' || !fromTeam.trim()) { if (!fromTeam || typeof fromTeam !== 'string' || !fromTeam.trim()) {
return res.status(400).json({ error: 'fromTeam is required.' }); return res.status(400).json({ error: 'fromTeam is required.' });
@@ -405,10 +426,20 @@ function createCardApiRouter() {
if (!toTeam || typeof toTeam !== 'string' || !toTeam.trim()) { if (!toTeam || typeof toTeam !== 'string' || !toTeam.trim()) {
return res.status(400).json({ error: 'toTeam is required.' }); return res.status(400).json({ error: 'toTeam is required.' });
} }
if (!assetId || typeof assetId !== 'string' || !assetId.trim()) { if (!rawAssetId || typeof rawAssetId !== 'string' || !rawAssetId.trim()) {
return res.status(400).json({ error: 'assetId is required.' }); return res.status(400).json({ error: 'assetId is required.' });
} }
// Resolve bare IP to full CARD asset ID (e.g., 10.240.78.110 → 10.240.78.110-CTEC)
let assetId = rawAssetId.trim();
if (!/\d+-[A-Z]+$/i.test(assetId)) {
const resolved = await resolveAssetId(assetId);
if (!resolved) {
return res.status(404).json({ error: `Asset not found in CARD for IP: ${assetId}` });
}
assetId = resolved;
}
try { try {
const { rows } = await pool.query( const { rows } = await pool.query(
'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3', 'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',