101 lines
3.8 KiB
JavaScript
101 lines
3.8 KiB
JavaScript
/**
|
|
* Network groups routes — DC-144.
|
|
*
|
|
* CRUD for DashCaddy-side groups (spec §2): a group is an id/name used in
|
|
* person.groups[] and access grants. HARD RULE (spec §2): DashCaddy
|
|
* groups are UI-side ONLY — they never map to Tailscale ACL groups (free
|
|
* plans cap TS groups at 3; compiled ACLs are per-user). Group ids are
|
|
* validated as DashCaddy-local slug strings, enforced in the service.
|
|
*
|
|
* GET /network/groups → list (derived from people store +
|
|
* names map at network-groups.json)
|
|
* POST /network/groups → create { id, name }
|
|
* PATCH /network/groups/:id → rename ({ id?, name? }; re-points
|
|
* person.groups[] + grant refs)
|
|
* DELETE /network/groups/:id → delete + cascade removal from
|
|
* people.groups[] and grants
|
|
*
|
|
* 'Admin' is the well-known group: cannot be renamed or deleted (409).
|
|
*
|
|
* 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 { createGroupsService, GroupError } = require('../src/security/network-groups');
|
|
|
|
module.exports = function networkGroupsRoutesFactory({
|
|
groupsService,
|
|
peopleStore,
|
|
accessStore,
|
|
asyncHandler,
|
|
log = { info() {}, warn() {}, error() {} },
|
|
} = {}) {
|
|
const router = require('express').Router(); // eslint-disable-line global-require
|
|
|
|
const service = groupsService || createGroupsService({ peopleStore, accessStore, log });
|
|
if (!service || typeof service.listGroups !== 'function') {
|
|
if (process.env.NODE_ENV === 'test') {
|
|
log.warn && log.warn('network-groups', 'groupsService missing — routes returning 404 in this environment');
|
|
router.all('*', (_req, res) => res.status(404).json({ success: false, error: '[DC-144] groups unavailable' }));
|
|
return router;
|
|
}
|
|
throw new Error('networkGroupsRoutes requires groupsService (or peopleStore + accessStore)');
|
|
}
|
|
if (!asyncHandler) {
|
|
// Must always be wired in prod (see share.js); test fallback only.
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
throw new Error('networkGroupsRoutes requires asyncHandler');
|
|
}
|
|
asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next); // eslint-disable-line no-param-reassign
|
|
}
|
|
|
|
// Map GroupError (statusCode-carrying, operational) onto the repo's
|
|
// error classes so the shared error handler renders the right status.
|
|
function _map(err) {
|
|
if (err instanceof GroupError) {
|
|
if (err.statusCode === 404) return new NotFoundError('group');
|
|
if (err.statusCode === 409) return new ConflictError(err.message);
|
|
return new ValidationError(err.message, 'group');
|
|
}
|
|
return err;
|
|
}
|
|
|
|
router.get('/', asyncHandler(async (req, res) => {
|
|
const groups = await service.listGroups();
|
|
ok(res, { data: groups, count: groups.length });
|
|
}, 'network-groups-list'));
|
|
|
|
router.post('/', asyncHandler(async (req, res) => {
|
|
try {
|
|
const group = await service.createGroup(req.body || {});
|
|
created(res, { group });
|
|
} catch (err) {
|
|
throw _map(err);
|
|
}
|
|
}, 'network-groups-create'));
|
|
|
|
router.patch('/:id', asyncHandler(async (req, res) => {
|
|
try {
|
|
const group = await service.renameGroup(req.params.id, req.body || {});
|
|
ok(res, { group });
|
|
} catch (err) {
|
|
throw _map(err);
|
|
}
|
|
}, 'network-groups-rename'));
|
|
|
|
router.delete('/:id', asyncHandler(async (req, res) => {
|
|
try {
|
|
const result = await service.deleteGroup(req.params.id);
|
|
ok(res, result);
|
|
} catch (err) {
|
|
throw _map(err);
|
|
}
|
|
}, 'network-groups-delete'));
|
|
|
|
return router;
|
|
};
|