d614be4747
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>
67 lines
2.4 KiB
TypeScript
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 };
|
|
}
|
|
);
|
|
}
|