feat(devices): manually add a device by MAC (offline pre-register) → 2.1.3
'+ Add by MAC' in the band header → POST /api/devices → lan_devices.addManual (status=known, present=false; enriched on next scan). Repo + API + frontend tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ 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 * as agents from '../../db/repos/agents.js';
|
||||
import { timingSafeStrEqual } from '../../auth/safe_compare.js';
|
||||
import { accessOwnerEmail } from '../../auth/cf_access.js';
|
||||
@@ -67,6 +68,19 @@ router.patch('/:mac', requireOwner, validate({ params: macParam, body: patchBody
|
||||
res.json(updated);
|
||||
}));
|
||||
|
||||
const addBody = z.object({
|
||||
mac: z.string().regex(/^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/i),
|
||||
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' } });
|
||||
|
||||
@@ -19,6 +19,21 @@ export async function get(mac) {
|
||||
return r || null;
|
||||
}
|
||||
|
||||
// Manually add a device by MAC (e.g. an offline device whose MAC you know). Lands
|
||||
// as status='known', present=false. Idempotent — re-adding updates name/grp/vendor.
|
||||
export async function addManual({ mac, name = null, grp = 'Flagged', vendor = null, randomized = false }) {
|
||||
const { rows: [r] } = await pool.query(
|
||||
`INSERT INTO lan_devices (mac, name, grp, vendor, randomized, status, present, first_seen, last_seen)
|
||||
VALUES ($1,$2,$3,$4,$5,'known',false,now(),now())
|
||||
ON CONFLICT (mac) DO UPDATE SET
|
||||
name = EXCLUDED.name, grp = EXCLUDED.grp,
|
||||
vendor = COALESCE(NULLIF(EXCLUDED.vendor,''), lan_devices.vendor),
|
||||
status = 'known'
|
||||
RETURNING ${COLS}`,
|
||||
[mac, name, grp, vendor, !!randomized]);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Insert unseen MACs as status='new'; for existing, refresh ip/vendor/last_seen/present
|
||||
// WITHOUT touching owner-curated name/grp/status/flagged.
|
||||
export async function upsertScan(rows) {
|
||||
|
||||
Reference in New Issue
Block a user