Files
homelab-monitor/apps/api/src/routes/devices.ts
T
jhodgkin d614be4747
CI / web (push) Successful in 17s
CI / api (push) Successful in 23s
fix: mac-oui-lookup ESM/CJS interop crash on startup
Named import of a CommonJS module's export crashed the whole process
at boot (SyntaxError, not caught by tsc since it's a runtime module
resolution behavior, not a type error). Verified by actually running
the built output with node this time instead of trusting tsc alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 23:04:52 -06:00

67 lines
2.4 KiB
TypeScript

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 };
}
);
}