/** * Network resources routes — DC-144. * * CRUD for Private Network resources (spec §2): apps or devices reachable * by name (`plex.internal`, `nas.internal`) — the dst side of the access * map (DC-145) and the compiled ACLs (DC-148). * * GET /network/resources → list * GET /network/resources/:name → one resource * POST /network/resources → create (name slug, port * 1-65535, host non-empty) * PATCH /network/resources/:name → update (name immutable) * DELETE /network/resources/:name → delete * * `sensitive` resources additionally require DashCaddy login + TOTP at * the forward_auth layer (spec §4 step-up); here it just round-trips. * * Mounted on the authed apiRouter — auth middleware is the caller's job * (same as every other route module in this repo). Factory pattern. */ 'use strict'; const { ValidationError, NotFoundError, ConflictError } = require('../src/utilities/errors'); const { ok, created } = require('../src/utils/responses'); const { createResourcesStore, ResourceError } = require('../src/security/network-resources'); module.exports = function networkResourcesRoutesFactory({ resourcesStore, asyncHandler, log = { info() {}, warn() {}, error() {} }, } = {}) { const router = require('express').Router(); // eslint-disable-line global-require const store = resourcesStore || createResourcesStore({ log }); if (!store || typeof store.listResources !== 'function') { if (process.env.NODE_ENV === 'test') { log.warn && log.warn('network-resources', 'resourcesStore missing — routes returning 404 in this environment'); router.all('*', (_req, res) => res.status(404).json({ success: false, error: '[DC-144] resources unavailable' })); return router; } throw new Error('networkResourcesRoutes requires resourcesStore'); } if (!asyncHandler) { // Must always be wired in prod (see share.js); test fallback only. if (process.env.NODE_ENV !== 'test') { throw new Error('networkResourcesRoutes requires asyncHandler'); } asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); // eslint-disable-line no-param-reassign } // Map ResourceError (statusCode-carrying, operational) onto the repo's // error classes so the shared error handler renders the right status. function _map(err) { if (err instanceof ResourceError) { if (err.statusCode === 404) return new NotFoundError('resource'); if (err.statusCode === 409) return new ConflictError(err.message); return new ValidationError(err.message, 'resource'); } return err; } router.get('/', asyncHandler(async (req, res) => { const resources = await store.listResources(); ok(res, { data: resources, count: resources.length }); }, 'network-resources-list')); router.get('/:name', asyncHandler(async (req, res) => { const resource = await store.getResource(req.params.name); if (!resource) throw new NotFoundError('resource'); ok(res, { resource }); }, 'network-resources-get')); router.post('/', asyncHandler(async (req, res) => { try { const resource = await store.createResource(req.body || {}); log.info && log.info('network-resources', 'resource created via API', { name: resource.name }); created(res, { resource }); } catch (err) { throw _map(err); } }, 'network-resources-create')); router.patch('/:name', asyncHandler(async (req, res) => { try { const resource = await store.updateResource(req.params.name, req.body || {}); ok(res, { resource }); } catch (err) { throw _map(err); } }, 'network-resources-update')); router.delete('/:name', asyncHandler(async (req, res) => { try { const result = await store.deleteResource(req.params.name); ok(res, result); } catch (err) { throw _map(err); } }, 'network-resources-delete')); return router; };