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',