Initial commit: enterprise digital platform with portal SSO, DigiServer, IT Assets, NetworkView, Server Monitor

This commit is contained in:
ske087
2026-05-10 21:07:50 +03:00
commit 8d9df56b0b
364 changed files with 73655 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
ENV PORT=3001
EXPOSE 3001
CMD ["node", "src/index.js"]
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "networkview-backend",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"dev": "nodemon src/index.js",
"start": "node src/index.js"
},
"dependencies": {
"better-sqlite3": "^9.4.3",
"cors": "^2.8.5",
"express": "^4.18.2",
"morgan": "^1.10.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}
+41
View File
@@ -0,0 +1,41 @@
/**
* Audit logging helper.
*
* Every mutation route calls `logAudit(req, { action, entityType, entityId, entityName, changes })`
* where `req` already has `req.actor` attached by the actor middleware (see index.js).
*/
const { v4: uuidv4 } = require('uuid');
const db = require('./db');
const insert = db.prepare(`
INSERT INTO audit_log (id, user_id, username, action, entity_type, entity_id, entity_name, changes, ip_address)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
/**
* @param {import('express').Request} req
* @param {{ action: string, entityType: string, entityId?: string, entityName?: string, changes?: object }} opts
*/
function logAudit(req, { action, entityType, entityId, entityName, changes }) {
const actor = req.actor ?? {};
const ip = (req.headers['x-forwarded-for'] ?? req.socket.remoteAddress ?? '').toString().split(',')[0].trim();
try {
insert.run(
uuidv4(),
actor.id ?? null,
actor.username ?? 'anonymous',
action,
entityType,
entityId ?? null,
entityName ?? null,
changes ? JSON.stringify(changes) : null,
ip || null,
);
} catch (e) {
// Never crash a request because audit failed
console.error('[audit] Failed to write log entry:', e.message);
}
}
module.exports = { logAudit };
+107
View File
@@ -0,0 +1,107 @@
const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
const DB_PATH = path.join(__dirname, '../../../data/networkview.db');
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
const db = new Database(DB_PATH);
// Enable WAL mode for better performance
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS sites (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
location TEXT,
notes TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS rooms (
id TEXT PRIMARY KEY,
site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
name TEXT NOT NULL,
notes TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS racks (
id TEXT PRIMARY KEY,
room_id TEXT REFERENCES rooms(id) ON DELETE CASCADE,
name TEXT NOT NULL,
total_units INTEGER DEFAULT 42,
manufacturer TEXT,
model TEXT,
notes TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS components (
id TEXT PRIMARY KEY,
rack_id TEXT REFERENCES racks(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'other',
position INTEGER,
height_units INTEGER DEFAULT 1,
manufacturer TEXT,
model TEXT,
serial_number TEXT,
asset_tag TEXT,
ip_address TEXT,
mac_address TEXT,
status TEXT DEFAULT 'active',
notes TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS ports (
id TEXT PRIMARY KEY,
component_id TEXT NOT NULL REFERENCES components(id) ON DELETE CASCADE,
port_number INTEGER NOT NULL,
label TEXT,
port_type TEXT DEFAULT 'RJ45',
connected_to_port_id TEXT REFERENCES ports(id) ON DELETE SET NULL,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
// Migrate: add port_count column if it doesn't exist yet
try { db.exec('ALTER TABLE components ADD COLUMN port_count INTEGER DEFAULT NULL'); } catch (_) {}
try { db.exec('ALTER TABLE components ADD COLUMN sfp_count INTEGER DEFAULT NULL'); } catch (_) {}
// ── Users & Audit tables ─────────────────────────────────────────────────────
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT UNIQUE,
role TEXT NOT NULL DEFAULT 'viewer',
is_active INTEGER NOT NULL DEFAULT 1,
api_key TEXT UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS audit_log (
id TEXT PRIMARY KEY,
user_id TEXT,
username TEXT,
action TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id TEXT,
entity_name TEXT,
changes TEXT,
ip_address TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
);
`);
module.exports = db;
+72
View File
@@ -0,0 +1,72 @@
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3001;
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
// ── Actor middleware ──────────────────────────────────────────────────────────
// The external main app sets x-user-id + x-username after token verification.
// Direct calls can use x-api-key (validated against users.api_key).
const db = require('./db');
app.use((req, _res, next) => {
const userId = req.headers['x-user-id'];
const username = req.headers['x-username'];
const apiKey = req.headers['x-api-key'];
if (userId && username) {
req.actor = { id: userId, username: String(username) };
} else if (apiKey) {
const user = db.prepare('SELECT id, username FROM users WHERE api_key = ? AND is_active = 1').get(apiKey);
req.actor = user ? { id: user.id, username: user.username } : { id: null, username: 'api-key-invalid' };
} else {
req.actor = { id: null, username: 'anonymous' };
}
next();
});
// API routes
app.use('/api/sites', require('./routes/sites'));
app.use('/api/rooms', require('./routes/rooms'));
app.use('/api/racks', require('./routes/racks'));
app.use('/api/components', require('./routes/components'));
app.use('/api/graph', require('./routes/graph'));
app.use('/api/users', require('./routes/users'));
app.use('/api/audit', require('./routes/audit'));
app.use('/api/settings', require('./routes/settings'));
app.get('/api/search', (req, res) => {
const { q } = req.query;
if (!q || q.trim().length < 2) return res.json({ sites: [], rooms: [], racks: [], components: [] });
const term = `%${q.trim()}%`;
const sites = db.prepare('SELECT id, name, location FROM sites WHERE name LIKE ? OR location LIKE ? LIMIT 10').all(term, term);
const rooms = db.prepare('SELECT id, name, site_id FROM rooms WHERE name LIKE ? LIMIT 10').all(term);
const racks = db.prepare('SELECT id, name, room_id FROM racks WHERE name LIKE ? OR model LIKE ? LIMIT 10').all(term, term);
const components = db.prepare(`
SELECT id, name, type, rack_id, model, ip_address FROM components
WHERE name LIKE ? OR model LIKE ? OR ip_address LIKE ? OR serial_number LIKE ? LIMIT 20
`).all(term, term, term, term);
res.json({ sites, rooms, racks, components });
});
// Health check
app.get('/api/health', (_req, res) => res.json({ status: 'ok', time: new Date().toISOString() }));
// Serve built frontend in production
const FRONTEND_DIST = path.join(__dirname, '../../frontend/dist');
app.use(express.static(FRONTEND_DIST));
app.get('*', (_req, res) => {
res.sendFile(path.join(FRONTEND_DIST, 'index.html'), err => {
if (err) res.status(200).json({ message: 'NetworkView API running. Start frontend separately in dev mode.' });
});
});
app.listen(PORT, () => {
console.log(`NetworkView backend running on http://localhost:${PORT}`);
});
+46
View File
@@ -0,0 +1,46 @@
/**
* /api/audit
* Read-only audit log with filtering and pagination.
*/
const express = require('express');
const db = require('../db');
const router = express.Router();
// GET /api/audit?entity_type=&entity_id=&user_id=&action=&limit=50&offset=0
router.get('/', (req, res) => {
const { entity_type, entity_id, user_id, action, limit = 50, offset = 0 } = req.query;
const conditions = [];
const params = [];
if (entity_type) { conditions.push('entity_type = ?'); params.push(entity_type); }
if (entity_id) { conditions.push('entity_id = ?'); params.push(entity_id); }
if (user_id) { conditions.push('user_id = ?'); params.push(user_id); }
if (action) { conditions.push('action = ?'); params.push(action); }
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const lim = Math.min(Number(limit) || 50, 500);
const off = Number(offset) || 0;
const rows = db.prepare(`
SELECT * FROM audit_log ${where}
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`).all(...params, lim, off);
const total = db.prepare(`
SELECT COUNT(*) AS n FROM audit_log ${where}
`).get(...params).n;
res.json({ total, limit: lim, offset: off, entries: rows });
});
// GET /api/audit/:id
router.get('/:id', (req, res) => {
const entry = db.prepare('SELECT * FROM audit_log WHERE id = ?').get(req.params.id);
if (!entry) return res.status(404).json({ error: 'Audit entry not found' });
res.json(entry);
});
module.exports = router;
@@ -0,0 +1,251 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const { logAudit } = require('../audit');
const router = express.Router();
function checkPositionOverlap(rackId, position, heightUnits, excludeId = null) {
const end = position + heightUnits - 1;
const existing = db.prepare(`
SELECT id, name, position, height_units FROM components
WHERE rack_id = ?
AND id != ?
AND position IS NOT NULL
AND position <= ? AND (position + height_units - 1) >= ?
`).all(rackId, excludeId || '', end, position);
return existing;
}
// GET /api/components?rackId=
router.get('/', (req, res) => {
const { rackId } = req.query;
if (!rackId) return res.status(400).json({ error: 'rackId query param required' });
const components = db.prepare(
'SELECT * FROM components WHERE rack_id = ? ORDER BY position ASC NULLS LAST'
).all(rackId);
res.json(components);
});
// GET /api/components/:id
router.get('/:id', (req, res) => {
const component = db.prepare('SELECT * FROM components WHERE id = ?').get(req.params.id);
if (!component) return res.status(404).json({ error: 'Component not found' });
const rawPorts = db.prepare('SELECT * FROM ports WHERE component_id = ? ORDER BY port_number').all(req.params.id);
const ports = rawPorts.map(p => {
if (!p.connected_to_port_id) return p;
const lp = db.prepare('SELECT * FROM ports WHERE id = ?').get(p.connected_to_port_id);
if (!lp) return p;
const lc = db.prepare('SELECT id, name, type FROM components WHERE id = ?').get(lp.component_id);
return { ...p, linked_port: lc ? { id: lp.id, port_number: lp.port_number, label: lp.label, component_id: lc.id, component_name: lc.name, component_type: lc.type } : null };
});
let rack = null, room = null, site = null;
if (component.rack_id) {
rack = db.prepare('SELECT id, name, total_units, room_id FROM racks WHERE id = ?').get(component.rack_id);
if (rack?.room_id) {
room = db.prepare('SELECT id, name, site_id FROM rooms WHERE id = ?').get(rack.room_id);
if (room?.site_id) site = db.prepare('SELECT id, name FROM sites WHERE id = ?').get(room.site_id);
}
}
res.json({ ...component, ports, rack, room, site });
});
// POST /api/components
router.post('/', (req, res) => {
const {
rack_id, name, type, position, height_units,
manufacturer, model, serial_number, asset_tag,
ip_address, mac_address, status, notes
} = req.body;
if (!name) return res.status(400).json({ error: 'name is required' });
if (rack_id) {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(rack_id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
if (position != null) {
const h = height_units || 1;
if (position < 1 || position + h - 1 > rack.total_units) {
return res.status(400).json({ error: `Position ${position} with ${h}U height exceeds rack size (${rack.total_units}U)` });
}
const overlapping = checkPositionOverlap(rack_id, position, h);
if (overlapping.length > 0) {
return res.status(400).json({
error: `Position overlaps with existing component: ${overlapping[0].name}`
});
}
}
}
const { port_count, sfp_count } = req.body;
const id = uuidv4();
db.prepare(`
INSERT INTO components
(id, rack_id, name, type, position, height_units, manufacturer, model,
serial_number, asset_tag, ip_address, mac_address, status, notes, port_count, sfp_count)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id, rack_id || null, name, type || 'other',
position ?? null, height_units || 1,
manufacturer || null, model || null,
serial_number || null, asset_tag || null,
ip_address || null, mac_address || null,
status || 'active', notes || '',
port_count ?? null, sfp_count ?? null
);
res.status(201).json(db.prepare('SELECT * FROM components WHERE id = ?').get(id));
logAudit(req, { action: 'create', entityType: 'component', entityId: id, entityName: name });
});
// PUT /api/components/:id
router.put('/:id', (req, res) => {
const component = db.prepare('SELECT * FROM components WHERE id = ?').get(req.params.id);
if (!component) return res.status(404).json({ error: 'Component not found' });
const {
name, type, position, height_units, manufacturer, model,
serial_number, asset_tag, ip_address, mac_address, status, notes, port_count, sfp_count
} = req.body;
const newPos = position ?? component.position;
const newH = height_units ?? component.height_units;
if (component.rack_id && newPos != null) {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(component.rack_id);
if (newPos < 1 || newPos + newH - 1 > rack.total_units) {
return res.status(400).json({ error: `Position exceeds rack size (${rack.total_units}U)` });
}
const overlapping = checkPositionOverlap(component.rack_id, newPos, newH, req.params.id);
if (overlapping.length > 0) {
return res.status(400).json({ error: `Position overlaps with: ${overlapping[0].name}` });
}
}
db.prepare(`
UPDATE components SET
name = ?, type = ?, position = ?, height_units = ?,
manufacturer = ?, model = ?, serial_number = ?, asset_tag = ?,
ip_address = ?, mac_address = ?, status = ?, notes = ?,
port_count = ?, sfp_count = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(
name ?? component.name,
type ?? component.type,
newPos,
newH,
manufacturer ?? component.manufacturer,
model ?? component.model,
serial_number ?? component.serial_number,
asset_tag ?? component.asset_tag,
ip_address ?? component.ip_address,
mac_address ?? component.mac_address,
status ?? component.status,
notes ?? component.notes,
port_count !== undefined ? port_count : component.port_count,
sfp_count !== undefined ? sfp_count : component.sfp_count,
req.params.id
);
logAudit(req, { action: 'update', entityType: 'component', entityId: component.id, entityName: name ?? component.name });
res.json(db.prepare('SELECT * FROM components WHERE id = ?').get(req.params.id));
});
// DELETE /api/components/:id
router.delete('/:id', (req, res) => {
const component = db.prepare('SELECT * FROM components WHERE id = ?').get(req.params.id);
if (!component) return res.status(404).json({ error: 'Component not found' });
db.prepare('DELETE FROM components WHERE id = ?').run(req.params.id);
logAudit(req, { action: 'delete', entityType: 'component', entityId: component.id, entityName: component.name });
res.json({ ok: true });
});
// --- Ports sub-resource ---
// GET /api/components/:id/ports
router.get('/:id/ports', (req, res) => {
const ports = db.prepare('SELECT * FROM ports WHERE component_id = ? ORDER BY port_number').all(req.params.id);
res.json(ports);
});
// POST /api/components/:id/ports
router.post('/:id/ports', (req, res) => {
const component = db.prepare('SELECT id, type, port_count FROM components WHERE id = ?').get(req.params.id);
if (!component) return res.status(404).json({ error: 'Component not found' });
const { port_number, label, port_type, connected_to_port_id, notes } = req.body;
if (port_number == null) return res.status(400).json({ error: 'port_number is required' });
// Patch-panel specific: no duplicate port numbers, enforce port_count capacity
if (component.type === 'patch_panel') {
const existing = db.prepare('SELECT id FROM ports WHERE component_id = ? AND port_number = ?').get(req.params.id, port_number);
if (existing) return res.status(409).json({ error: `Port ${port_number} already exists on this patch panel.` });
if (component.port_count != null) {
const count = db.prepare('SELECT COUNT(*) as n FROM ports WHERE component_id = ?').get(req.params.id).n;
if (count >= component.port_count)
return res.status(409).json({ error: `Patch panel is full (${component.port_count} ports maximum).` });
if (port_number < 1 || port_number > component.port_count)
return res.status(400).json({ error: `Port number must be between 1 and ${component.port_count}.` });
}
}
const id = uuidv4();
db.prepare(`
INSERT INTO ports (id, component_id, port_number, label, port_type, connected_to_port_id, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(id, req.params.id, port_number, label || null, port_type || 'RJ45', connected_to_port_id || null, notes || null);
// Mirror link on the other side
if (connected_to_port_id) {
db.prepare('UPDATE ports SET connected_to_port_id = ? WHERE id = ?').run(id, connected_to_port_id);
}
res.status(201).json(db.prepare('SELECT * FROM ports WHERE id = ?').get(id));
});
// PUT /api/components/:componentId/ports/:portId
router.put('/:componentId/ports/:portId', (req, res) => {
const port = db.prepare('SELECT * FROM ports WHERE id = ? AND component_id = ?').get(req.params.portId, req.params.componentId);
if (!port) return res.status(404).json({ error: 'Port not found' });
const { label, port_type, connected_to_port_id, notes } = req.body;
const newLinkedId = connected_to_port_id !== undefined ? (connected_to_port_id || null) : port.connected_to_port_id;
const oldLinkedId = port.connected_to_port_id;
// Clear old reverse link if it changed
if (oldLinkedId && oldLinkedId !== newLinkedId) {
db.prepare('UPDATE ports SET connected_to_port_id = NULL WHERE id = ? AND connected_to_port_id = ?').run(oldLinkedId, port.id);
}
db.prepare(`
UPDATE ports SET label = ?, port_type = ?, connected_to_port_id = ?, notes = ? WHERE id = ?
`).run(
label ?? port.label,
port_type ?? port.port_type,
newLinkedId,
notes ?? port.notes,
req.params.portId
);
// Set new reverse link
if (newLinkedId && newLinkedId !== oldLinkedId) {
db.prepare('UPDATE ports SET connected_to_port_id = ? WHERE id = ?').run(port.id, newLinkedId);
}
res.json(db.prepare('SELECT * FROM ports WHERE id = ?').get(req.params.portId));
});
// DELETE /api/components/:componentId/ports/:portId
router.delete('/:componentId/ports/:portId', (req, res) => {
db.prepare('DELETE FROM ports WHERE id = ? AND component_id = ?').run(req.params.portId, req.params.componentId);
res.json({ ok: true });
});
module.exports = router;
+124
View File
@@ -0,0 +1,124 @@
const express = require('express');
const db = require('../db');
const router = express.Router();
/**
* GET /api/graph/:level/:id
* level: site | room | rack
* Returns { nodes, edges } for a force-directed graph view.
*/
router.get('/:level/:id', (req, res) => {
const { level, id } = req.params;
const nodes = [];
const edges = [];
const compIds = [];
if (level === 'site') {
const site = db.prepare('SELECT id, name FROM sites WHERE id = ?').get(id);
if (!site) return res.status(404).json({ error: 'Site not found' });
nodes.push({ id: site.id, label: site.name, type: 'site', url: `/sites/${site.id}` });
const rooms = db.prepare('SELECT id, name FROM rooms WHERE site_id = ?').all(id);
for (const room of rooms) {
nodes.push({ id: room.id, label: room.name, type: 'room', url: `/rooms/${room.id}` });
edges.push({ source: site.id, target: room.id, type: 'hierarchy' });
const racks = db.prepare('SELECT id, name FROM racks WHERE room_id = ?').all(room.id);
for (const rack of racks) {
nodes.push({ id: rack.id, label: rack.name, type: 'rack', url: `/racks/${rack.id}` });
edges.push({ source: room.id, target: rack.id, type: 'hierarchy' });
const components = db.prepare('SELECT id, name, type FROM components WHERE rack_id = ?').all(rack.id);
for (const comp of components) {
nodes.push({ id: comp.id, label: comp.name, type: comp.type, url: `/components/${comp.id}` });
edges.push({ source: rack.id, target: comp.id, type: 'hierarchy' });
compIds.push(comp.id);
}
}
}
} else if (level === 'room') {
const room = db.prepare('SELECT id, name FROM rooms WHERE id = ?').get(id);
if (!room) return res.status(404).json({ error: 'Room not found' });
nodes.push({ id: room.id, label: room.name, type: 'room', url: `/rooms/${room.id}` });
const racks = db.prepare('SELECT id, name FROM racks WHERE room_id = ?').all(id);
for (const rack of racks) {
nodes.push({ id: rack.id, label: rack.name, type: 'rack', url: `/racks/${rack.id}` });
edges.push({ source: room.id, target: rack.id, type: 'hierarchy' });
const components = db.prepare('SELECT id, name, type FROM components WHERE rack_id = ?').all(rack.id);
for (const comp of components) {
nodes.push({ id: comp.id, label: comp.name, type: comp.type, url: `/components/${comp.id}` });
edges.push({ source: rack.id, target: comp.id, type: 'hierarchy' });
compIds.push(comp.id);
}
}
} else if (level === 'rack') {
const rack = db.prepare('SELECT id, name FROM racks WHERE id = ?').get(id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
nodes.push({ id: rack.id, label: rack.name, type: 'rack', url: `/racks/${rack.id}` });
const components = db.prepare('SELECT id, name, type FROM components WHERE rack_id = ?').all(id);
for (const comp of components) {
nodes.push({ id: comp.id, label: comp.name, type: comp.type, url: `/components/${comp.id}` });
edges.push({ source: rack.id, target: comp.id, type: 'hierarchy' });
compIds.push(comp.id);
}
} else {
return res.status(400).json({ error: 'Invalid level. Use site, room, or rack.' });
}
// Port-to-port connections + port-level nodes (leaf level)
if (compIds.length > 1) {
const ph = compIds.map(() => '?').join(',');
const portLinks = db.prepare(`
SELECT
p1.id AS pid1, p1.port_number AS num1, p1.label AS lbl1, p1.component_id AS src,
p2.id AS pid2, p2.port_number AS num2, p2.label AS lbl2, p2.component_id AS tgt
FROM ports p1
JOIN ports p2 ON p1.connected_to_port_id = p2.id
WHERE p1.component_id IN (${ph})
AND p2.component_id IN (${ph})
AND p1.component_id != p2.component_id
`).all(...compIds, ...compIds);
const compEdgeSeen = new Set();
const portNodeSeen = new Set();
const portEdgeSeen = new Set();
for (const link of portLinks) {
// Component-to-component connection (high-level, deduplicated)
const compKey = [link.src, link.tgt].sort().join('|');
if (!compEdgeSeen.has(compKey)) {
compEdgeSeen.add(compKey);
edges.push({ source: link.src, target: link.tgt, type: 'connection' });
}
// Port nodes + hierarchy edges
const pid1 = `port:${link.pid1}`;
const pid2 = `port:${link.pid2}`;
if (!portNodeSeen.has(pid1)) {
portNodeSeen.add(pid1);
nodes.push({ id: pid1, label: link.lbl1 || `P${link.num1}`, type: 'port', url: `/components/${link.src}` });
edges.push({ source: link.src, target: pid1, type: 'hierarchy' });
}
if (!portNodeSeen.has(pid2)) {
portNodeSeen.add(pid2);
nodes.push({ id: pid2, label: link.lbl2 || `P${link.num2}`, type: 'port', url: `/components/${link.tgt}` });
edges.push({ source: link.tgt, target: pid2, type: 'hierarchy' });
}
// Port-to-port connection edge (deepest level)
const portEdgeKey = [pid1, pid2].sort().join('|');
if (!portEdgeSeen.has(portEdgeKey)) {
portEdgeSeen.add(portEdgeKey);
edges.push({ source: pid1, target: pid2, type: 'connection' });
}
}
}
res.json({ nodes, edges });
});
module.exports = router;
+105
View File
@@ -0,0 +1,105 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const { logAudit } = require('../audit');
const router = express.Router();
// GET /api/racks?roomId=
router.get('/', (req, res) => {
const { roomId } = req.query;
if (!roomId) return res.status(400).json({ error: 'roomId query param required' });
const racks = db.prepare(`
SELECT ra.*, COUNT(c.id) as component_count
FROM racks ra
LEFT JOIN components c ON c.rack_id = ra.id
WHERE ra.room_id = ?
GROUP BY ra.id
ORDER BY ra.name
`).all(roomId);
res.json(racks);
});
// GET /api/racks/:id
router.get('/:id', (req, res) => {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(req.params.id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
const components = db.prepare(`
SELECT * FROM components WHERE rack_id = ? ORDER BY position ASC NULLS LAST, name ASC
`).all(req.params.id);
// Attach ports to each component so the graphic view can show active ports
const componentsWithPorts = components.map(c => {
const ports = db.prepare('SELECT * FROM ports WHERE component_id = ? ORDER BY port_number').all(c.id);
return { ...c, ports };
});
let room = null, site = null;
if (rack.room_id) {
room = db.prepare('SELECT id, name, site_id FROM rooms WHERE id = ?').get(rack.room_id);
if (room) site = db.prepare('SELECT id, name FROM sites WHERE id = ?').get(room.site_id);
}
res.json({ ...rack, components: componentsWithPorts, room, site });
});
// POST /api/racks
router.post('/', (req, res) => {
const { room_id, name, total_units, manufacturer, model, notes } = req.body;
if (!name) return res.status(400).json({ error: 'name is required' });
if (room_id) {
const room = db.prepare('SELECT id FROM rooms WHERE id = ?').get(room_id);
if (!room) return res.status(404).json({ error: 'Room not found' });
}
const id = uuidv4();
db.prepare(`
INSERT INTO racks (id, room_id, name, total_units, manufacturer, model, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(id, room_id || null, name, total_units || 42, manufacturer || null, model || null, notes || '');
const created = db.prepare('SELECT * FROM racks WHERE id = ?').get(id);
logAudit(req, { action: 'create', entityType: 'rack', entityId: id, entityName: name });
res.status(201).json(created);
});
// PUT /api/racks/:id
router.put('/:id', (req, res) => {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(req.params.id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
const { name, total_units, manufacturer, model, notes } = req.body;
const changes = {};
if (name != null && name !== rack.name) changes.name = { from: rack.name, to: name };
if (total_units != null && total_units !== rack.total_units) changes.total_units = { from: rack.total_units, to: total_units };
if (manufacturer != null && manufacturer !== rack.manufacturer) changes.manufacturer = { from: rack.manufacturer, to: manufacturer };
if (model != null && model !== rack.model) changes.model = { from: rack.model, to: model };
db.prepare(`
UPDATE racks SET name = ?, total_units = ?, manufacturer = ?, model = ?, notes = ?,
updated_at = CURRENT_TIMESTAMP WHERE id = ?
`).run(
name ?? rack.name,
total_units ?? rack.total_units,
manufacturer ?? rack.manufacturer,
model ?? rack.model,
notes ?? rack.notes,
req.params.id
);
logAudit(req, { action: 'update', entityType: 'rack', entityId: rack.id, entityName: name ?? rack.name, changes });
res.json(db.prepare('SELECT * FROM racks WHERE id = ?').get(req.params.id));
});
// DELETE /api/racks/:id
router.delete('/:id', (req, res) => {
const rack = db.prepare('SELECT * FROM racks WHERE id = ?').get(req.params.id);
if (!rack) return res.status(404).json({ error: 'Rack not found' });
db.prepare('DELETE FROM racks WHERE id = ?').run(req.params.id);
logAudit(req, { action: 'delete', entityType: 'rack', entityId: rack.id, entityName: rack.name });
res.json({ ok: true });
});
module.exports = router;
+84
View File
@@ -0,0 +1,84 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const { logAudit } = require('../audit');
const router = express.Router();
// GET /api/rooms?siteId=
router.get('/', (req, res) => {
const { siteId } = req.query;
if (!siteId) return res.status(400).json({ error: 'siteId query param required' });
const rooms = db.prepare(`
SELECT r.*, COUNT(ra.id) as rack_count
FROM rooms r
LEFT JOIN racks ra ON ra.room_id = r.id
WHERE r.site_id = ?
GROUP BY r.id
ORDER BY r.name
`).all(siteId);
res.json(rooms);
});
// GET /api/rooms/:id
router.get('/:id', (req, res) => {
const room = db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id);
if (!room) return res.status(404).json({ error: 'Room not found' });
const racks = db.prepare(`
SELECT ra.*, COUNT(c.id) as component_count
FROM racks ra
LEFT JOIN components c ON c.rack_id = ra.id
WHERE ra.room_id = ?
GROUP BY ra.id
ORDER BY ra.name
`).all(req.params.id);
const site = db.prepare('SELECT id, name FROM sites WHERE id = ?').get(room.site_id);
res.json({ ...room, racks, site });
});
// POST /api/rooms
router.post('/', (req, res) => {
const { site_id, name, notes } = req.body;
if (!site_id || !name) return res.status(400).json({ error: 'site_id and name are required' });
const site = db.prepare('SELECT id FROM sites WHERE id = ?').get(site_id);
if (!site) return res.status(404).json({ error: 'Site not found' });
const id = uuidv4();
db.prepare('INSERT INTO rooms (id, site_id, name, notes) VALUES (?, ?, ?, ?)').run(
id, site_id, name, notes || ''
);
const created = db.prepare('SELECT * FROM rooms WHERE id = ?').get(id);
logAudit(req, { action: 'create', entityType: 'room', entityId: id, entityName: name });
res.status(201).json(created);
});
// PUT /api/rooms/:id
router.put('/:id', (req, res) => {
const room = db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id);
if (!room) return res.status(404).json({ error: 'Room not found' });
const { name, notes } = req.body;
const changes = {};
if (name != null && name !== room.name) changes.name = { from: room.name, to: name };
db.prepare(`
UPDATE rooms SET name = ?, notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
`).run(name ?? room.name, notes ?? room.notes, req.params.id);
logAudit(req, { action: 'update', entityType: 'room', entityId: room.id, entityName: name ?? room.name, changes });
res.json(db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id));
});
// DELETE /api/rooms/:id
router.delete('/:id', (req, res) => {
const room = db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id);
if (!room) return res.status(404).json({ error: 'Room not found' });
db.prepare('DELETE FROM rooms WHERE id = ?').run(req.params.id);
logAudit(req, { action: 'delete', entityType: 'room', entityId: room.id, entityName: room.name });
res.json({ ok: true });
});
module.exports = router;
@@ -0,0 +1,95 @@
/**
* /api/settings
* Administrative / danger-zone operations.
* All destructive actions require the header X-Confirm: yes
*/
const express = require('express');
const db = require('../db');
const { logAudit } = require('../audit');
const router = express.Router();
// Guard: require explicit confirmation header for destructive ops
function requireConfirm(req, res, next) {
if ((req.headers['x-confirm'] ?? '').toLowerCase() !== 'yes') {
return res.status(400).json({ error: 'Send header X-Confirm: yes to confirm this destructive operation.' });
}
next();
}
// ── GET /api/settings/stats ───────────────────────────────────────────────────
// Overview counts — useful for the settings page dashboard
router.get('/stats', (req, res) => {
const stats = {
sites: db.prepare('SELECT COUNT(*) AS n FROM sites').get().n,
rooms: db.prepare('SELECT COUNT(*) AS n FROM rooms').get().n,
racks: db.prepare('SELECT COUNT(*) AS n FROM racks').get().n,
components: db.prepare('SELECT COUNT(*) AS n FROM components').get().n,
ports: db.prepare('SELECT COUNT(*) AS n FROM ports').get().n,
users: db.prepare('SELECT COUNT(*) AS n FROM users').get().n,
audit_entries: db.prepare('SELECT COUNT(*) AS n FROM audit_log').get().n,
};
res.json(stats);
});
// ── DELETE /api/settings/data/all ────────────────────────────────────────────
// Wipe ALL network data (sites, rooms, racks, components, ports, audit_log).
// Users are kept. Requires X-Confirm: yes.
router.delete('/data/all', requireConfirm, (req, res) => {
db.transaction(() => {
db.exec('DELETE FROM ports');
db.exec('DELETE FROM components');
db.exec('DELETE FROM racks');
db.exec('DELETE FROM rooms');
db.exec('DELETE FROM sites');
db.exec('DELETE FROM audit_log');
})();
logAudit(req, { action: 'delete_all', entityType: 'database', entityName: 'all network data' });
res.json({ ok: true, message: 'All network data deleted.' });
});
// ── DELETE /api/settings/data/audit ──────────────────────────────────────────
// Clear only the audit log.
router.delete('/data/audit', requireConfirm, (req, res) => {
db.exec('DELETE FROM audit_log');
logAudit(req, { action: 'delete_all', entityType: 'audit_log', entityName: 'audit log' });
res.json({ ok: true, message: 'Audit log cleared.' });
});
// ── DELETE /api/settings/sites/:id ───────────────────────────────────────────
router.delete('/sites/:id', requireConfirm, (req, res) => {
const row = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!row) return res.status(404).json({ error: 'Site not found' });
db.prepare('DELETE FROM sites WHERE id = ?').run(row.id);
logAudit(req, { action: 'delete', entityType: 'site', entityId: row.id, entityName: row.name });
res.json({ ok: true });
});
// ── DELETE /api/settings/rooms/:id ───────────────────────────────────────────
router.delete('/rooms/:id', requireConfirm, (req, res) => {
const row = db.prepare('SELECT * FROM rooms WHERE id = ?').get(req.params.id);
if (!row) return res.status(404).json({ error: 'Room not found' });
db.prepare('DELETE FROM rooms WHERE id = ?').run(row.id);
logAudit(req, { action: 'delete', entityType: 'room', entityId: row.id, entityName: row.name });
res.json({ ok: true });
});
// ── DELETE /api/settings/racks/:id ───────────────────────────────────────────
router.delete('/racks/:id', requireConfirm, (req, res) => {
const row = db.prepare('SELECT * FROM racks WHERE id = ?').get(req.params.id);
if (!row) return res.status(404).json({ error: 'Rack not found' });
db.prepare('DELETE FROM racks WHERE id = ?').run(row.id);
logAudit(req, { action: 'delete', entityType: 'rack', entityId: row.id, entityName: row.name });
res.json({ ok: true });
});
// ── DELETE /api/settings/components/:id ──────────────────────────────────────
router.delete('/components/:id', requireConfirm, (req, res) => {
const row = db.prepare('SELECT * FROM components WHERE id = ?').get(req.params.id);
if (!row) return res.status(404).json({ error: 'Component not found' });
db.prepare('DELETE FROM components WHERE id = ?').run(row.id);
logAudit(req, { action: 'delete', entityType: 'component', entityId: row.id, entityName: row.name });
res.json({ ok: true });
});
module.exports = router;
+78
View File
@@ -0,0 +1,78 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const db = require('../db');
const { logAudit } = require('../audit');
const router = express.Router();
// GET /api/sites
router.get('/', (req, res) => {
const sites = db.prepare(`
SELECT s.*, COUNT(r.id) as room_count
FROM sites s
LEFT JOIN rooms r ON r.site_id = s.id
GROUP BY s.id
ORDER BY s.name
`).all();
res.json(sites);
});
// GET /api/sites/:id
router.get('/:id', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).json({ error: 'Site not found' });
const rooms = db.prepare(`
SELECT r.*, COUNT(ra.id) as rack_count
FROM rooms r
LEFT JOIN racks ra ON ra.room_id = r.id
WHERE r.site_id = ?
GROUP BY r.id
ORDER BY r.name
`).all(req.params.id);
res.json({ ...site, rooms });
});
// POST /api/sites
router.post('/', (req, res) => {
const { name, location, notes } = req.body;
if (!name) return res.status(400).json({ error: 'Name is required' });
const id = uuidv4();
db.prepare('INSERT INTO sites (id, name, location, notes) VALUES (?, ?, ?, ?)').run(
id, name, location || null, notes || ''
);
const created = db.prepare('SELECT * FROM sites WHERE id = ?').get(id);
logAudit(req, { action: 'create', entityType: 'site', entityId: id, entityName: name });
res.status(201).json(created);
});
// PUT /api/sites/:id
router.put('/:id', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).json({ error: 'Site not found' });
const { name, location, notes } = req.body;
const changes = {};
if (name != null && name !== site.name) changes.name = { from: site.name, to: name };
if (location != null && location !== site.location) changes.location = { from: site.location, to: location };
db.prepare(`
UPDATE sites SET name = ?, location = ?, notes = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(name ?? site.name, location ?? site.location, notes ?? site.notes, req.params.id);
logAudit(req, { action: 'update', entityType: 'site', entityId: site.id, entityName: name ?? site.name, changes });
res.json(db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id));
});
// DELETE /api/sites/:id
router.delete('/:id', (req, res) => {
const site = db.prepare('SELECT * FROM sites WHERE id = ?').get(req.params.id);
if (!site) return res.status(404).json({ error: 'Site not found' });
db.prepare('DELETE FROM sites WHERE id = ?').run(req.params.id);
logAudit(req, { action: 'delete', entityType: 'site', entityId: site.id, entityName: site.name });
res.json({ ok: true });
});
module.exports = router;
+138
View File
@@ -0,0 +1,138 @@
/**
* /api/users
*
* Manages NetworkView users. Authentication is expected to be handled by
* the external main app; this API accepts an `x-user-id` / `x-username`
* header (set by the gateway after verifying the bearer token) and an
* `x-api-key` header for direct service-to-service calls.
*
* Roles: admin | editor | viewer
*/
const express = require('express');
const { v4: uuidv4 } = require('uuid');
const crypto = require('crypto');
const db = require('../db');
const { logAudit } = require('../audit');
const router = express.Router();
// ── Helpers ──────────────────────────────────────────────────────────────────
const safeUser = u => ({
id: u.id,
username: u.username,
email: u.email,
role: u.role,
is_active: u.is_active,
has_api_key: !!u.api_key,
created_at: u.created_at,
updated_at: u.updated_at,
});
// ── GET /api/users ────────────────────────────────────────────────────────────
router.get('/', (req, res) => {
const users = db.prepare('SELECT * FROM users ORDER BY username').all();
res.json(users.map(safeUser));
});
// ── GET /api/users/:id ────────────────────────────────────────────────────────
router.get('/:id', (req, res) => {
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(safeUser(user));
});
// ── POST /api/users ───────────────────────────────────────────────────────────
router.post('/', (req, res) => {
const { username, email, role = 'viewer' } = req.body;
if (!username) return res.status(400).json({ error: 'username is required' });
if (!['admin', 'editor', 'viewer'].includes(role)) {
return res.status(400).json({ error: 'role must be admin | editor | viewer' });
}
if (db.prepare('SELECT id FROM users WHERE username = ?').get(username)) {
return res.status(409).json({ error: 'Username already exists' });
}
if (email && db.prepare('SELECT id FROM users WHERE email = ?').get(email)) {
return res.status(409).json({ error: 'Email already in use' });
}
const id = uuidv4();
db.prepare(`
INSERT INTO users (id, username, email, role) VALUES (?, ?, ?, ?)
`).run(id, username, email ?? null, role);
const created = db.prepare('SELECT * FROM users WHERE id = ?').get(id);
logAudit(req, { action: 'create', entityType: 'user', entityId: id, entityName: username });
res.status(201).json(safeUser(created));
});
// ── PUT /api/users/:id ────────────────────────────────────────────────────────
router.put('/:id', (req, res) => {
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
const { username, email, role, is_active } = req.body;
if (role && !['admin', 'editor', 'viewer'].includes(role)) {
return res.status(400).json({ error: 'role must be admin | editor | viewer' });
}
if (username && username !== user.username) {
if (db.prepare('SELECT id FROM users WHERE username = ? AND id != ?').get(username, user.id)) {
return res.status(409).json({ error: 'Username already exists' });
}
}
const changes = {};
if (username != null && username !== user.username) changes.username = { from: user.username, to: username };
if (email != null && email !== user.email) changes.email = { from: user.email, to: email };
if (role != null && role !== user.role) changes.role = { from: user.role, to: role };
if (is_active != null && Number(is_active) !== user.is_active) changes.is_active = { from: user.is_active, to: Number(is_active) };
db.prepare(`
UPDATE users
SET username = ?, email = ?, role = ?, is_active = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`).run(
username ?? user.username,
email ?? user.email,
role ?? user.role,
is_active != null ? Number(is_active) : user.is_active,
user.id,
);
logAudit(req, { action: 'update', entityType: 'user', entityId: user.id, entityName: username ?? user.username, changes });
res.json(safeUser(db.prepare('SELECT * FROM users WHERE id = ?').get(user.id)));
});
// ── DELETE /api/users/:id ─────────────────────────────────────────────────────
router.delete('/:id', (req, res) => {
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
db.prepare('DELETE FROM users WHERE id = ?').run(user.id);
logAudit(req, { action: 'delete', entityType: 'user', entityId: user.id, entityName: user.username });
res.json({ ok: true });
});
// ── POST /api/users/:id/api-key (generate / rotate) ─────────────────────────
router.post('/:id/api-key', (req, res) => {
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
const apiKey = `nvk_${crypto.randomBytes(24).toString('hex')}`;
db.prepare('UPDATE users SET api_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(apiKey, user.id);
logAudit(req, { action: 'api_key_rotate', entityType: 'user', entityId: user.id, entityName: user.username });
// Return the key once — after this it is never exposed again
res.json({ api_key: apiKey });
});
// ── DELETE /api/users/:id/api-key (revoke) ──────────────────────────────────
router.delete('/:id/api-key', (req, res) => {
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
db.prepare('UPDATE users SET api_key = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(user.id);
logAudit(req, { action: 'api_key_revoke', entityType: 'user', entityId: user.id, entityName: user.username });
res.json({ ok: true });
});
module.exports = router;