108 lines
3.9 KiB
JavaScript
108 lines
3.9 KiB
JavaScript
/**
|
|
* Network lifecycle routes — DC-143 (specs/PRIVATE-NETWORK-SPEC.md §5).
|
|
*
|
|
* Standalone router (same factory pattern as routes/network-people.js);
|
|
* auth middleware is the caller's job:
|
|
*
|
|
* POST /network/people/:id/block → block person (deny network, expire
|
|
* device keys, kill sessions, purge
|
|
* grants with snapshot for restore)
|
|
* POST /network/people/:id/unblock → honest unblock (restore grants +
|
|
* status; Tailscale devices must sign
|
|
* in again — response says so)
|
|
* POST /network/people/:id/remove → remove person + provider devices +
|
|
* grants + sessions
|
|
* POST /network/devices/:id/revoke → revoke one device (body: {personId})
|
|
*
|
|
* Errors: LastAdminError → 409, ProviderNotActiveError → 503, other
|
|
* ProviderError → 502, NotFound → 404, ValidationError → 400.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { ProviderError, ProviderNotActiveError } = require('../src/network/errors');
|
|
const { LastAdminError } = require('../src/security/network-people-store');
|
|
const { ok } = require('../src/utils/responses');
|
|
|
|
function _statusFor(err) {
|
|
if (err instanceof LastAdminError) return 409;
|
|
if (err instanceof ProviderNotActiveError) return 503;
|
|
if (err instanceof ProviderError) return 502;
|
|
if (err && err.statusCode) return err.statusCode;
|
|
if (err && err.name === 'ValidationError') return 400;
|
|
if (err && err.name === 'NotFoundError') return 404;
|
|
return 500;
|
|
}
|
|
|
|
module.exports = function networkLifecycleRoutesFactory({
|
|
lifecycle,
|
|
asyncHandler,
|
|
log = { info() {}, warn() {}, error() {} },
|
|
} = {}) {
|
|
const router = require('express').Router(); // eslint-disable-line global-require
|
|
|
|
if (!lifecycle) {
|
|
throw new Error('networkLifecycleRoutes requires a lifecycle service');
|
|
}
|
|
if (!asyncHandler) {
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
throw new Error('networkLifecycleRoutes requires asyncHandler');
|
|
}
|
|
/* eslint-disable no-param-reassign */
|
|
asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
|
|
/* eslint-enable no-param-reassign */
|
|
}
|
|
|
|
const wrap = (fn) => asyncHandler(async (req, res) => {
|
|
try {
|
|
await fn(req, res);
|
|
} catch (err) {
|
|
log.warn && log.warn('network-lifecycle', 'route error', {
|
|
path: req.path,
|
|
err: err && err.message,
|
|
code: err && err.code,
|
|
});
|
|
const status = _statusFor(err);
|
|
res.status(status).json({
|
|
success: false,
|
|
error: err && err.message ? err.message : 'lifecycle failure',
|
|
code: (err && err.code) || null,
|
|
});
|
|
}
|
|
});
|
|
|
|
router.post('/people/:id/block', wrap(async (req, res) => {
|
|
const result = await lifecycle.blockPerson(req.params.id);
|
|
ok(res, { success: true, ...result });
|
|
}));
|
|
|
|
router.post('/people/:id/unblock', wrap(async (req, res) => {
|
|
const result = await lifecycle.unblockPerson(req.params.id);
|
|
// Honest-unblock (spec §5): the note is part of the contract —
|
|
// Tailscale users must be told their devices need to sign in again;
|
|
// WireGuard users are told peers reconnect immediately.
|
|
ok(res, { success: true, ...result });
|
|
}));
|
|
|
|
router.post('/people/:id/remove', wrap(async (req, res) => {
|
|
const result = await lifecycle.removePerson(req.params.id);
|
|
ok(res, { success: true, ...result });
|
|
}));
|
|
|
|
router.post('/devices/:id/revoke', wrap(async (req, res) => {
|
|
const personId = req.body && req.body.personId;
|
|
if (!personId || typeof personId !== 'string') {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: 'personId is required in the body',
|
|
code: 'bad_input',
|
|
});
|
|
return;
|
|
}
|
|
const result = await lifecycle.revokeDevice(personId, req.params.id);
|
|
ok(res, { success: true, ...result });
|
|
}));
|
|
|
|
return router;
|
|
};
|