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