Files
Void-Homelab/lib/api/routes/devices.js
root 3ea150bad1 fix: extract softAuth to shared module and apply to icon_sets router
Move the softAuth middleware from devices.js into a new shared
lib/api/soft_auth.js module. Apply router.use(softAuth) and
router.use(errorMiddleware) to icon_sets.js so that POST/DELETE
owner-only routes return 401 (not 500) when no auth is present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 09:10:16 +10:00

77 lines
3.3 KiB
JavaScript

import { Router } from 'express';
import { z } from 'zod';
import { asyncWrap, errorMiddleware } from '../errors.js';
import { requireOwner } from '../cap.js';
import { validate } from '../validate.js';
import * as devices from '../../db/repos/lan_devices.js';
import { isRandomizedMac } from '../../infra/scan.js';
import { softAuth } from '../soft_auth.js';
export const router = Router();
const GROUP_ORDER = ['Smart Home', 'Entertainment', 'Personal', 'Network', 'Flagged'];
router.use(softAuth);
// GET /devices — known devices grouped for the band (open within the app, like /services).
router.get('/', asyncWrap(async (_req, res) => {
const byGrp = new Map();
for (const d of await devices.listKnown()) {
const g = d.grp || 'Flagged';
if (!byGrp.has(g)) byGrp.set(g, []);
byGrp.get(g).push(d);
}
const order = [...GROUP_ORDER, ...[...byGrp.keys()].filter(g => !GROUP_ORDER.includes(g))];
res.json({ groups: order.filter(g => byGrp.has(g)).map(name => ({ name, devices: byGrp.get(name) })) });
}));
// GET /devices/discovered — review queue (owner).
router.get('/discovered', requireOwner, asyncWrap(async (_req, res) => {
res.json(await devices.listDiscovered());
}));
const macParam = z.object({ mac: z.string().regex(/^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i) });
export const iconRef = z.string().regex(/^(set:[a-z0-9-]+:[a-z0-9-]+|brand:[a-z0-9-]+)$/).nullable();
const patchBody = z.object({
name: z.string().max(120).optional(),
grp: z.enum(['Smart Home', 'Entertainment', 'Personal', 'Network', 'Flagged']).optional(),
status: z.enum(['new', 'known', 'ignored']).optional(),
note: z.string().max(500).optional(),
flagged: z.boolean().optional(),
icon: iconRef.optional()
});
// PATCH /devices/:mac — name / edit / promote (owner). This is "add from discovered".
router.patch('/:mac', requireOwner, validate({ params: macParam, body: patchBody }), asyncWrap(async (req, res) => {
const updated = await devices.update(req.params.mac.toLowerCase(), req.body);
if (!updated) return res.status(404).json({ error: { code: 'not_found' } });
res.json(updated);
}));
const addBody = z.object({
mac: z.string().regex(/^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i),
ip: z.string().regex(/^\d{1,3}(\.\d{1,3}){3}$/).optional(),
name: z.string().max(120).optional(),
grp: z.enum(['Smart Home', 'Entertainment', 'Personal', 'Network', 'Flagged']).optional(),
vendor: z.string().max(120).optional()
});
// POST /devices — manually add a device by MAC (e.g. an offline device) (owner).
router.post('/', requireOwner, validate({ body: addBody }), asyncWrap(async (req, res) => {
const mac = req.body.mac.toLowerCase();
res.status(201).json(await devices.addManual({ ...req.body, mac, randomized: isRandomizedMac(mac) }));
}));
// DELETE /devices/:mac (owner).
router.delete('/:mac', requireOwner, validate({ params: macParam }), asyncWrap(async (req, res) => {
if (!(await devices.remove(req.params.mac.toLowerCase()))) return res.status(404).json({ error: { code: 'not_found' } });
res.status(204).end();
}));
// POST /devices/scan — run a scan now (owner).
router.post('/scan', requireOwner, asyncWrap(async (_req, res) => {
const { runDeviceScanCycle } = await import('../../infra/scan_cycle.js');
res.json(await runDeviceScanCycle());
}));
router.use(errorMiddleware);