From 8fc7c33cffdaa4fee945a74dbb92ea3f180a0bc6 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Wed, 27 May 2026 18:56:40 -0600 Subject: [PATCH] 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. --- backend/helpers/cardApi.js | 38 +++++++++++++++++++++++++++++++++ backend/routes/cardApi.js | 43 ++++++++++++++++++++++++++++++++------ 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/backend/helpers/cardApi.js b/backend/helpers/cardApi.js index 8d0aa21..5f3e562 100644 --- a/backend/helpers/cardApi.js +++ b/backend/helpers/cardApi.js @@ -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 }; } +/** + * 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 = { isConfigured, missingVars, @@ -304,4 +341,5 @@ module.exports = { declineAsset, redirectAsset, invalidateToken, + resolveAssetId, }; diff --git a/backend/routes/cardApi.js b/backend/routes/cardApi.js index d1cab84..62092e7 100644 --- a/backend/routes/cardApi.js +++ b/backend/routes/cardApi.js @@ -15,6 +15,7 @@ const { confirmAsset, declineAsset, redirectAsset, + resolveAssetId, } = require('../helpers/cardApi'); // --------------------------------------------------------------------------- @@ -213,15 +214,25 @@ function createCardApiRouter() { } const { queueItemId } = req.params; - const { teamName, assetId, comment } = req.body; + const { teamName, assetId: rawAssetId, comment } = req.body; if (!teamName || typeof teamName !== 'string' || !teamName.trim()) { 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.' }); } + // 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 { const { rows } = await pool.query( '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 { teamName, assetId, comment } = req.body; + const { teamName, assetId: rawAssetId, comment } = req.body; if (!teamName || typeof teamName !== 'string' || !teamName.trim()) { 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.' }); } + // 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 { const { rows } = await pool.query( '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 { fromTeam, toTeam, assetId } = req.body; + const { fromTeam, toTeam, assetId: rawAssetId } = req.body; if (!fromTeam || typeof fromTeam !== 'string' || !fromTeam.trim()) { return res.status(400).json({ error: 'fromTeam is required.' }); @@ -405,10 +426,20 @@ function createCardApiRouter() { if (!toTeam || typeof toTeam !== 'string' || !toTeam.trim()) { 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.' }); } + // 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 { const { rows } = await pool.query( 'SELECT * FROM ivanti_todo_queue WHERE id = $1 AND user_id = $2 AND workflow_type = $3',