import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; import type Database from "better-sqlite3"; // mac-oui-lookup is CommonJS; Node's ESM interop doesn't reliably expose its // named exports (crashed the whole process on startup with // "Named export 'getVendor' not found" despite working fine under // require()) -- import the default and destructure instead. import macOuiLookup from "mac-oui-lookup"; const { getVendor } = macOuiLookup; import { getRecentDevices, setDeviceLabel, clearDeviceLabel, type DeviceRow } from "../db/index.js"; async function requireAuth(req: FastifyRequest, reply: FastifyReply) { if (!req.session.username) { reply.code(401).send({ error: "not authenticated" }); } } // Manual label wins (deliberately set by the admin), then the static // config/hosts.yaml known list, then the passively-resolved mDNS hostname — // which is informational only and does NOT make a device "known" on its own, // since nobody has actually vetted it yet. function toApiDevice(r: DeviceRow) { const name = r.manual_label ?? r.known_name ?? null; return { ip: r.ip, mac: r.mac, name, known: r.manual_label !== null || r.known_name !== null, vendor: getVendor(r.mac), mdnsHostname: r.mdns_hostname, firstSeen: r.first_seen, lastSeen: r.last_seen, }; } const MAC_RE = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i; export function registerDeviceRoutes(app: FastifyInstance, db: Database.Database): void { app.get("/api/devices", { preHandler: requireAuth }, async () => { const rows = getRecentDevices(db); return { devices: rows.map(toApiDevice) }; }); app.put<{ Params: { mac: string }; Body: { label: string } }>( "/api/devices/:mac/label", { preHandler: requireAuth }, async (req, reply) => { const mac = req.params.mac.toLowerCase(); const label = req.body?.label?.trim(); if (!MAC_RE.test(mac)) return reply.code(400).send({ error: "invalid MAC address" }); if (!label) return reply.code(400).send({ error: "label must not be empty" }); setDeviceLabel(db, mac, label); return { ok: true }; } ); app.delete<{ Params: { mac: string } }>( "/api/devices/:mac/label", { preHandler: requireAuth }, async (req, reply) => { const mac = req.params.mac.toLowerCase(); if (!MAC_RE.test(mac)) return reply.code(400).send({ error: "invalid MAC address" }); clearDeviceLabel(db, mac); return { ok: true }; } ); }