DC-143: block/unblock/remove person + revoke device orchestration + tests
This commit is contained in:
4 files changed
+962
No files matched your search
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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;
|
||||
};
|
||||
Reference in new issue
Block a user