Add NetBox device inventory integration framework

Adds API helper and route module for NetBox DCIM integration as a future
replacement for Granite inventory management.

Helper (helpers/netboxApi.js):
- Token-based auth (Authorization: Token <token>)
- Device CRUD: list, get, create, update, delete
- IP cross-reference: find device by IP via IPAM
- Reference data: sites, device types, device roles
- Connection test, TLS skip support

Routes (routes/netbox.js) mounted at /api/netbox:
- GET /status — config and connectivity check
- GET /devices — list with filters and pagination
- GET /devices/search — general search
- GET /devices/by-ip/:ip — cross-reference IP to device
- GET /devices/:id — single device detail
- POST /devices — create (Admin, Standard_User)
- PATCH /devices/:id — partial update
- DELETE /devices/:id — delete (Admin only)
- GET /sites, /device-types, /device-roles — reference data

Integration is optional — gracefully returns 503 if env vars are unset.
New env vars: NETBOX_API_URL, NETBOX_API_TOKEN, NETBOX_SKIP_TLS
This commit is contained in:
Jordan Ramos
2026-07-06 15:19:33 -06:00
parent e8a5bdc196
commit 121d044fb6
4 changed files with 831 additions and 0 deletions

View File

@@ -64,6 +64,14 @@ CARD_API_PASS=
# Set to true if behind Charter's SSL inspection proxy
CARD_SKIP_TLS=false
# NetBox Device Inventory API (replacing Granite for inventory management)
# Token-based auth — create a token in NetBox under Admin > API Tokens.
# The token must have read/write permissions on DCIM and IPAM models.
NETBOX_API_URL=
NETBOX_API_TOKEN=
# Set to true if behind Charter's SSL inspection proxy
NETBOX_SKIP_TLS=false
# PostgreSQL Database (Docker container steam-postgres)
# If set, the backend uses Postgres instead of SQLite.
# Format: postgresql://user:password@host:port/database

View File

@@ -0,0 +1,374 @@
// NetBox API helpers
// Centralizes HTTP calls for the NetBox DCIM (device inventory) API.
// Follows the same pattern as cardApi.js — token-based auth, generic request
// wrapper, convenience methods for device CRUD.
//
// NetBox REST API conventions:
// - Base URL: NETBOX_API_URL (e.g., https://netbox.example.com)
// - Auth: Token-based — `Authorization: Token <NETBOX_API_TOKEN>`
// - Content-Type: application/json
// - Pagination: `?limit=N&offset=N`, response: { count, next, previous, results }
// - CRUD: GET (list/detail), POST (create), PATCH (partial update), PUT (full update), DELETE
// - Device endpoint: /api/dcim/devices/
const https = require('https');
const http = require('http');
// ---------------------------------------------------------------------------
// Configuration — read from process.env at module load
// ---------------------------------------------------------------------------
const NETBOX_API_URL = process.env.NETBOX_API_URL || '';
const NETBOX_API_TOKEN = process.env.NETBOX_API_TOKEN || '';
const NETBOX_SKIP_TLS = process.env.NETBOX_SKIP_TLS === 'true';
const requiredVars = ['NETBOX_API_URL', 'NETBOX_API_TOKEN'];
const missingVars = requiredVars.filter((v) => !process.env[v]);
if (missingVars.length > 0) {
console.warn(`[netbox-api] WARNING: Missing required environment variables: ${missingVars.join(', ')}. NetBox API calls will fail.`);
}
const isConfigured = missingVars.length === 0;
// ---------------------------------------------------------------------------
// Generic request — supports GET, POST, PATCH, PUT, DELETE with Token auth
// ---------------------------------------------------------------------------
function netboxRequest(method, urlPath, body, options) {
const timeout = (options && options.timeout) || 30000;
return new Promise((resolve, reject) => {
const fullUrl = new URL(NETBOX_API_URL + urlPath);
const isHttps = fullUrl.protocol === 'https:';
const transport = isHttps ? https : http;
const headers = {
'accept': 'application/json',
'authorization': 'Token ' + NETBOX_API_TOKEN,
};
let bodyStr = null;
if (body !== null && body !== undefined) {
bodyStr = JSON.stringify(body);
headers['content-type'] = 'application/json';
headers['content-length'] = Buffer.byteLength(bodyStr);
}
const reqOptions = {
hostname: fullUrl.hostname,
port: fullUrl.port || (isHttps ? 443 : 80),
path: fullUrl.pathname + fullUrl.search,
method,
headers,
timeout,
};
if (isHttps) {
reqOptions.rejectUnauthorized = !NETBOX_SKIP_TLS;
}
const req = transport.request(reqOptions, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => resolve({ status: res.statusCode, body: data }));
});
req.on('timeout', () => req.destroy(new Error(`${method} ${urlPath} timed out`)));
req.on('error', (err) => {
reject(new Error(`[netbox-api] ${method} ${urlPath} failed: ${err.message}`));
});
if (bodyStr) req.write(bodyStr);
req.end();
});
}
// ---------------------------------------------------------------------------
// Convenience wrappers
// ---------------------------------------------------------------------------
function netboxGet(urlPath, options) {
return netboxRequest('GET', urlPath, null, options);
}
function netboxPost(urlPath, body, options) {
return netboxRequest('POST', urlPath, body, options);
}
function netboxPatch(urlPath, body, options) {
return netboxRequest('PATCH', urlPath, body, options);
}
function netboxPut(urlPath, body, options) {
return netboxRequest('PUT', urlPath, body, options);
}
function netboxDelete(urlPath, options) {
return netboxRequest('DELETE', urlPath, null, options);
}
// ---------------------------------------------------------------------------
// High-level helpers — Device CRUD
// ---------------------------------------------------------------------------
/**
* Test connection by fetching /api/status/. Returns { ok, version } or { ok, error }.
*/
async function testConnection() {
try {
const res = await netboxGet('/api/status/');
if (res.status >= 200 && res.status < 300) {
let parsed;
try { parsed = JSON.parse(res.body); } catch (_) { parsed = {}; }
return { ok: true, version: parsed['netbox-version'] || 'unknown' };
}
return { ok: false, error: `HTTP ${res.status}` };
} catch (err) {
return { ok: false, error: err.message };
}
}
/**
* List devices with optional filters and pagination.
*
* @param {object} [filters] - Query parameters (name, site, role, status, tag, etc.)
* @param {object} [options] - { limit, offset, timeout }
* @returns {{ status, body, ok }}
*/
async function listDevices(filters, options) {
const params = new URLSearchParams();
if (filters) {
for (const [key, val] of Object.entries(filters)) {
if (val !== undefined && val !== null && val !== '') {
params.set(key, String(val));
}
}
}
const limit = (options && options.limit) || 50;
const offset = (options && options.offset) || 0;
params.set('limit', String(limit));
params.set('offset', String(offset));
const qs = params.toString();
const res = await netboxGet(`/api/dcim/devices/?${qs}`, options);
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
}
/**
* Get a single device by ID.
*
* @param {number|string} deviceId - NetBox device ID
* @param {object} [options] - { timeout }
* @returns {{ status, body, ok }}
*/
async function getDevice(deviceId, options) {
const res = await netboxGet(`/api/dcim/devices/${deviceId}/`, options);
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
}
/**
* Create a new device.
*
* Required fields (minimum): name, role (slug or id), device_type (id), site (id)
* Optional: serial, asset_tag, status, platform, tenant, primary_ip4, custom_fields, tags, etc.
*
* @param {object} deviceData - Device object matching NetBox device serializer
* @param {object} [options] - { timeout }
* @returns {{ status, body, ok }}
*/
async function createDevice(deviceData, options) {
const res = await netboxPost('/api/dcim/devices/', deviceData, options);
return { status: res.status, body: res.body, ok: res.status === 201 };
}
/**
* Update a device (partial update via PATCH).
*
* @param {number|string} deviceId - NetBox device ID
* @param {object} updates - Fields to update
* @param {object} [options] - { timeout }
* @returns {{ status, body, ok }}
*/
async function updateDevice(deviceId, updates, options) {
const res = await netboxPatch(`/api/dcim/devices/${deviceId}/`, updates, options);
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
}
/**
* Delete a device.
*
* @param {number|string} deviceId - NetBox device ID
* @param {object} [options] - { timeout }
* @returns {{ status, body, ok }}
*/
async function deleteDevice(deviceId, options) {
const res = await netboxDelete(`/api/dcim/devices/${deviceId}/`, options);
return { status: res.status, body: res.body, ok: res.status === 204 };
}
/**
* Search devices by name or IP address.
* NetBox supports ?q= for general search across name/serial/asset_tag.
*
* @param {string} query - Search string
* @param {object} [options] - { limit, offset, timeout }
* @returns {{ status, body, ok }}
*/
async function searchDevices(query, options) {
const params = new URLSearchParams({ q: query });
const limit = (options && options.limit) || 50;
const offset = (options && options.offset) || 0;
params.set('limit', String(limit));
params.set('offset', String(offset));
const res = await netboxGet(`/api/dcim/devices/?${params.toString()}`, options);
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
}
// ---------------------------------------------------------------------------
// IP Address helpers — for cross-referencing with Ivanti findings
// ---------------------------------------------------------------------------
/**
* List IP addresses with optional filters.
* Useful for finding devices by IP (cross-reference with Ivanti host findings).
*
* @param {object} [filters] - Query parameters (address, device, interface, etc.)
* @param {object} [options] - { limit, offset, timeout }
* @returns {{ status, body, ok }}
*/
async function listIpAddresses(filters, options) {
const params = new URLSearchParams();
if (filters) {
for (const [key, val] of Object.entries(filters)) {
if (val !== undefined && val !== null && val !== '') {
params.set(key, String(val));
}
}
}
const limit = (options && options.limit) || 50;
const offset = (options && options.offset) || 0;
params.set('limit', String(limit));
params.set('offset', String(offset));
const qs = params.toString();
const res = await netboxGet(`/api/ipam/ip-addresses/?${qs}`, options);
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
}
/**
* Search for a device by its IP address via IPAM.
* Returns the device record if the IP is assigned to an interface on a device.
*
* @param {string} ipAddress - IP address to look up (e.g., "10.244.11.55")
* @param {object} [options] - { timeout }
* @returns {{ status, device, ip, ok }} - device is null if IP not found or not assigned
*/
async function findDeviceByIp(ipAddress, options) {
// NetBox IPAM accepts CIDR or bare IP (auto-matches /32 or /128)
const searchIp = ipAddress.includes('/') ? ipAddress : ipAddress;
const params = new URLSearchParams({ address: searchIp, limit: '1' });
const res = await netboxGet(`/api/ipam/ip-addresses/?${params.toString()}`, options);
if (res.status < 200 || res.status >= 300) {
return { status: res.status, device: null, ip: null, ok: false };
}
let parsed;
try { parsed = JSON.parse(res.body); } catch (_) { parsed = { results: [] }; }
const ipRecord = parsed.results && parsed.results[0];
if (!ipRecord) {
return { status: 404, device: null, ip: null, ok: false };
}
// IP may be assigned to an interface → interface belongs to a device
const assignedDevice = ipRecord.assigned_object && ipRecord.assigned_object.device;
return {
status: res.status,
device: assignedDevice || null,
ip: ipRecord,
ok: true,
};
}
// ---------------------------------------------------------------------------
// Site & Device Type helpers — needed when creating devices
// ---------------------------------------------------------------------------
/**
* List sites (paginated).
*/
async function listSites(filters, options) {
const params = new URLSearchParams();
if (filters) {
for (const [key, val] of Object.entries(filters)) {
if (val !== undefined && val !== null && val !== '') {
params.set(key, String(val));
}
}
}
params.set('limit', String((options && options.limit) || 100));
params.set('offset', String((options && options.offset) || 0));
const res = await netboxGet(`/api/dcim/sites/?${params.toString()}`, options);
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
}
/**
* List device types (paginated).
*/
async function listDeviceTypes(filters, options) {
const params = new URLSearchParams();
if (filters) {
for (const [key, val] of Object.entries(filters)) {
if (val !== undefined && val !== null && val !== '') {
params.set(key, String(val));
}
}
}
params.set('limit', String((options && options.limit) || 100));
params.set('offset', String((options && options.offset) || 0));
const res = await netboxGet(`/api/dcim/device-types/?${params.toString()}`, options);
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
}
/**
* List device roles (paginated).
*/
async function listDeviceRoles(filters, options) {
const params = new URLSearchParams();
if (filters) {
for (const [key, val] of Object.entries(filters)) {
if (val !== undefined && val !== null && val !== '') {
params.set(key, String(val));
}
}
}
params.set('limit', String((options && options.limit) || 100));
params.set('offset', String((options && options.offset) || 0));
const res = await netboxGet(`/api/dcim/device-roles/?${params.toString()}`, options);
return { status: res.status, body: res.body, ok: res.status >= 200 && res.status < 300 };
}
module.exports = {
isConfigured,
missingVars,
netboxRequest,
netboxGet,
netboxPost,
netboxPatch,
netboxPut,
netboxDelete,
testConnection,
listDevices,
getDevice,
createDevice,
updateDevice,
deleteDevice,
searchDevices,
listIpAddresses,
findDeviceByIp,
listSites,
listDeviceTypes,
listDeviceRoles,
};

445
backend/routes/netbox.js Normal file
View File

@@ -0,0 +1,445 @@
// NetBox Device Inventory API Routes
// Proxies NetBox DCIM operations (list, get, create, update, delete devices)
// and provides cross-reference lookups between Ivanti findings and NetBox devices.
const express = require('express');
const { requireAuth, requireGroup, requireTeam } = require('../middleware/auth');
const logAudit = require('../helpers/auditLog');
const {
isConfigured,
missingVars,
testConnection,
listDevices,
getDevice,
createDevice,
updateDevice,
deleteDevice,
searchDevices,
findDeviceByIp,
listSites,
listDeviceTypes,
listDeviceRoles,
} = require('../helpers/netboxApi');
// ---------------------------------------------------------------------------
// Error classification — maps NetBox API errors to client responses
// ---------------------------------------------------------------------------
function handleNetboxError(err, res) {
const msg = err.message || String(err);
console.error('[netbox-api]', msg);
if (msg.includes('401') || msg.includes('Unauthorized')) {
return res.status(401).json({ error: 'NetBox authorization failed. Check API token.' });
}
if (msg.includes('403') || msg.includes('Forbidden')) {
return res.status(403).json({ error: 'Insufficient NetBox permissions for this operation.' });
}
if (msg.includes('timed out')) {
return res.status(504).json({ error: 'NetBox API request timed out.', timeout: true });
}
return res.status(502).json({ error: 'NetBox API request failed.', details: msg });
}
// ---------------------------------------------------------------------------
// Response parser helper
// ---------------------------------------------------------------------------
function parseBody(raw) {
try { return JSON.parse(raw); } catch (_) { return raw; }
}
// ---------------------------------------------------------------------------
// Router factory
// ---------------------------------------------------------------------------
function createNetboxRouter() {
const router = express.Router();
// All NetBox routes require authentication and Admin or Standard_User group
router.use(requireAuth(), requireGroup('Admin', 'Standard_User'));
/**
* GET /status
*
* Returns whether the NetBox integration is configured and reachable.
*
* @response 200 - { configured: true, connected: true, version: string }
* @response 503 - { configured: false, error: string, missingVars: string[] }
*/
router.get('/status', async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ configured: false, error: 'NetBox API is not configured.', missingVars });
}
try {
const result = await testConnection();
if (result.ok) {
return res.json({ configured: true, connected: true, version: result.version });
}
return res.json({ configured: true, connected: false, error: result.error });
} catch (err) {
return res.json({ configured: true, connected: false, error: err.message });
}
});
/**
* GET /devices
*
* List devices with optional filters and pagination.
*
* @query {string} [name] - Filter by device name (exact or contains)
* @query {string} [site] - Filter by site slug
* @query {string} [role] - Filter by device role slug
* @query {string} [status] - Filter by status (active, planned, staged, decommissioning, offline)
* @query {string} [tag] - Filter by tag
* @query {string} [q] - General search (name, serial, asset_tag)
* @query {number} [limit=50] - Results per page
* @query {number} [offset=0] - Pagination offset
* @response 200 - { count, next, previous, results: device[] }
* @response 503 - NetBox not configured
*/
router.get('/devices', requireTeam(), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { limit, offset, ...filters } = req.query;
try {
const result = await listDevices(filters, {
limit: limit ? parseInt(limit, 10) : 50,
offset: offset ? parseInt(offset, 10) : 0,
});
if (result.ok) {
return res.json(parseBody(result.body));
}
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
/**
* GET /devices/search
*
* Search devices by name, serial, or asset tag.
*
* @query {string} q - Search query (required)
* @query {number} [limit=50] - Results per page
* @query {number} [offset=0] - Pagination offset
* @response 200 - { count, next, previous, results: device[] }
* @response 400 - { error: string } — missing query
*/
router.get('/devices/search', requireTeam(), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { q, limit, offset } = req.query;
if (!q || !q.trim()) {
return res.status(400).json({ error: 'Search query (q) is required.' });
}
try {
const result = await searchDevices(q.trim(), {
limit: limit ? parseInt(limit, 10) : 50,
offset: offset ? parseInt(offset, 10) : 0,
});
if (result.ok) {
return res.json(parseBody(result.body));
}
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
/**
* GET /devices/by-ip/:ip
*
* Look up a NetBox device by IP address (cross-reference with Ivanti findings).
*
* @param {string} ip - IP address to look up
* @response 200 - { device, ip } — device info and IP record
* @response 404 - { error: string } — IP not found or not assigned to a device
*/
router.get('/devices/by-ip/:ip', requireTeam(), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { ip } = req.params;
if (!ip || !ip.trim()) {
return res.status(400).json({ error: 'IP address is required.' });
}
try {
const result = await findDeviceByIp(ip.trim());
if (result.ok && result.device) {
return res.json({ device: result.device, ip: result.ip });
}
if (result.ok && !result.device) {
return res.status(404).json({ error: 'IP found in NetBox but not assigned to a device.', ip: result.ip });
}
return res.status(404).json({ error: `IP ${ip} not found in NetBox.` });
} catch (err) {
return handleNetboxError(err, res);
}
});
/**
* GET /devices/:id
*
* Get a single device by NetBox ID.
*
* @param {string} id - NetBox device ID
* @response 200 - Device object
* @response 404 - { error: string }
*/
router.get('/devices/:id', requireTeam(), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { id } = req.params;
try {
const result = await getDevice(id);
if (result.ok) {
return res.json(parseBody(result.body));
}
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
/**
* POST /devices
*
* Create a new device in NetBox.
*
* @body {string} name - Device name (required)
* @body {number|object} device_type - Device type ID or nested object (required)
* @body {number|object} role - Device role ID or slug (required)
* @body {number|object} site - Site ID or slug (required)
* @body {string} [serial] - Serial number
* @body {string} [asset_tag] - Asset tag
* @body {string} [status] - Status (active, planned, staged, decommissioning, offline)
* @body {object} [custom_fields] - Custom field values
* @body {string[]} [tags] - Tag slugs
* @response 201 - Created device object
* @response 400 - { error: string } — validation errors from NetBox
*/
router.post('/devices', async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { name, device_type, role, site } = req.body;
// Basic validation — NetBox will do full validation, but catch obvious omissions
if (!name || !name.trim()) {
return res.status(400).json({ error: 'Device name is required.' });
}
if (!device_type) {
return res.status(400).json({ error: 'device_type is required.' });
}
if (!role) {
return res.status(400).json({ error: 'role is required.' });
}
if (!site) {
return res.status(400).json({ error: 'site is required.' });
}
try {
const result = await createDevice(req.body);
if (result.ok) {
const created = parseBody(result.body);
logAudit({
userId: req.user.id,
username: req.user.username,
action: 'netbox_device_create',
entityType: 'netbox_device',
entityId: String(created.id || name),
details: { name, site, role, device_type },
ipAddress: req.ip,
});
return res.status(201).json(created);
}
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
/**
* PATCH /devices/:id
*
* Partially update a device in NetBox.
*
* @param {string} id - NetBox device ID
* @body {object} - Fields to update (any writable device field)
* @response 200 - Updated device object
* @response 400 - { error: string } — validation errors from NetBox
* @response 404 - { error: string } — device not found
*/
router.patch('/devices/:id', async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { id } = req.params;
const updates = req.body;
if (!updates || Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'No update fields provided.' });
}
try {
const result = await updateDevice(id, updates);
if (result.ok) {
const updated = parseBody(result.body);
logAudit({
userId: req.user.id,
username: req.user.username,
action: 'netbox_device_update',
entityType: 'netbox_device',
entityId: String(id),
details: { updatedFields: Object.keys(updates) },
ipAddress: req.ip,
});
return res.json(updated);
}
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
/**
* DELETE /devices/:id
*
* Delete a device from NetBox. Admin only.
*
* @param {string} id - NetBox device ID
* @response 204 - No content (success)
* @response 404 - { error: string } — device not found
*/
router.delete('/devices/:id', requireGroup('Admin'), async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { id } = req.params;
try {
const result = await deleteDevice(id);
if (result.ok) {
logAudit({
userId: req.user.id,
username: req.user.username,
action: 'netbox_device_delete',
entityType: 'netbox_device',
entityId: String(id),
details: {},
ipAddress: req.ip,
});
return res.status(204).send();
}
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
// -----------------------------------------------------------------------
// Reference data endpoints — needed for device creation forms
// -----------------------------------------------------------------------
/**
* GET /sites
*
* List available NetBox sites for device creation.
*
* @query {number} [limit=100] - Results per page
* @query {number} [offset=0] - Pagination offset
* @response 200 - { count, results: site[] }
*/
router.get('/sites', async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { limit, offset, ...filters } = req.query;
try {
const result = await listSites(filters, {
limit: limit ? parseInt(limit, 10) : 100,
offset: offset ? parseInt(offset, 10) : 0,
});
if (result.ok) return res.json(parseBody(result.body));
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
/**
* GET /device-types
*
* List available device types for device creation.
*
* @query {number} [limit=100] - Results per page
* @query {number} [offset=0] - Pagination offset
* @response 200 - { count, results: deviceType[] }
*/
router.get('/device-types', async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { limit, offset, ...filters } = req.query;
try {
const result = await listDeviceTypes(filters, {
limit: limit ? parseInt(limit, 10) : 100,
offset: offset ? parseInt(offset, 10) : 0,
});
if (result.ok) return res.json(parseBody(result.body));
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
/**
* GET /device-roles
*
* List available device roles for device creation.
*
* @query {number} [limit=100] - Results per page
* @query {number} [offset=0] - Pagination offset
* @response 200 - { count, results: role[] }
*/
router.get('/device-roles', async (req, res) => {
if (!isConfigured) {
return res.status(503).json({ error: 'NetBox API is not configured.', missingVars });
}
const { limit, offset, ...filters } = req.query;
try {
const result = await listDeviceRoles(filters, {
limit: limit ? parseInt(limit, 10) : 100,
offset: offset ? parseInt(offset, 10) : 0,
});
if (result.ok) return res.json(parseBody(result.body));
return res.status(result.status).json(parseBody(result.body));
} catch (err) {
return handleNetboxError(err, res);
}
});
return router;
}
module.exports = createNetboxRouter;

View File

@@ -40,6 +40,7 @@ const createCardApiRouter = require('./routes/cardApi');
const createFeedbackRouter = require('./routes/feedback');
const createWebhooksRouter = require('./routes/webhooks');
const createNotificationsRouter = require('./routes/notifications');
const createNetboxRouter = require('./routes/netbox');
const app = express();
const PORT = process.env.PORT || 3001;
@@ -281,6 +282,9 @@ app.use('/api/feedback', createFeedbackRouter());
// In-app notifications routes (authenticated users)
app.use('/api/notifications', createNotificationsRouter());
// NetBox device inventory routes — DCIM device CRUD, IP cross-reference
app.use('/api/netbox', createNetboxRouter());
// GitLab webhook routes — receives issue lifecycle events (no auth required)
app.use('/api/webhooks', createWebhooksRouter());