mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
144 lines
4.5 KiB
JavaScript
144 lines
4.5 KiB
JavaScript
// Identity Admin Service
|
|
// Purpose: Exposes lockdown admin socket operations for inspecting and editing the central identity database.
|
|
// Scope: Owns transport-level authorization and delegates every database mutation to identityService.
|
|
const io = require('../../globals/io');
|
|
const logger = require('../../globals/logger').child('identityAdminService');
|
|
const { getRole } = require('../roleService');
|
|
const {
|
|
listUsersForAdmin,
|
|
getUserForAdmin,
|
|
addUserSignal,
|
|
removeUserSignal,
|
|
setVerified,
|
|
setDeterrence,
|
|
setMuted,
|
|
setUserPermission,
|
|
listRegisteredPermissions,
|
|
setFeatureState,
|
|
deleteFeatureState,
|
|
} = require('../identityService');
|
|
|
|
const MAX_FEATURE_STATE_JSON_BYTES = 64 * 1024;
|
|
|
|
function isLockdownAdminSocket(socket) {
|
|
const role = getRole(socket);
|
|
return role === 'lockdown';
|
|
}
|
|
|
|
function requireLockdownAdmin(socket) {
|
|
if (!isLockdownAdminSocket(socket)) {
|
|
throw new Error('Lockdown admin required.');
|
|
}
|
|
}
|
|
|
|
function normalizeFeaturePayload(namespace, value) {
|
|
const ns = String(namespace || '').trim();
|
|
if (!ns) throw new Error('Feature namespace required.');
|
|
if (!/^[a-zA-Z0-9_.:-]{1,80}$/.test(ns)) {
|
|
throw new Error('Feature namespace contains invalid characters.');
|
|
}
|
|
|
|
/*
|
|
Feature state is intentionally JSON-shaped. Strings are not accepted as the
|
|
stored value because they make the admin editor ambiguous: a textarea JSON
|
|
string should represent a real JSON object/array, not an escaped blob.
|
|
*/
|
|
if (!value || typeof value !== 'object') {
|
|
throw new Error('Feature state must be a JSON object or array.');
|
|
}
|
|
const encoded = JSON.stringify(value);
|
|
if (Buffer.byteLength(encoded, 'utf8') > MAX_FEATURE_STATE_JSON_BYTES) {
|
|
throw new Error('Feature state is too large.');
|
|
}
|
|
return { namespace: ns, value };
|
|
}
|
|
|
|
function ackHandler(socket, eventName, handler) {
|
|
socket.on(eventName, (payload = {}, cb = () => {}) => {
|
|
try {
|
|
requireLockdownAdmin(socket);
|
|
cb({ success: true, ...handler(payload || {}) });
|
|
} catch (err) {
|
|
logger.warn('Identity admin request failed', {
|
|
eventName,
|
|
socketId: socket.id,
|
|
role: getRole(socket),
|
|
error: err.message,
|
|
});
|
|
cb({ error: err.message });
|
|
}
|
|
});
|
|
}
|
|
|
|
io.on('connection', (socket) => {
|
|
ackHandler(socket, 'identityAdmin:listUsers', () => ({
|
|
users: listUsersForAdmin(),
|
|
permissions: listRegisteredPermissions(),
|
|
}));
|
|
|
|
ackHandler(socket, 'identityAdmin:listPermissions', () => ({
|
|
permissions: listRegisteredPermissions(),
|
|
}));
|
|
|
|
ackHandler(socket, 'identityAdmin:getUser', ({ userId }) => {
|
|
const user = getUserForAdmin(userId);
|
|
if (!user) throw new Error('User not found.');
|
|
return { user };
|
|
});
|
|
|
|
ackHandler(socket, 'identityAdmin:addSignal', ({ userId, type, value }) => ({
|
|
user: addUserSignal(userId, type, value),
|
|
}));
|
|
|
|
ackHandler(socket, 'identityAdmin:removeSignal', ({ userId, type, value }) => ({
|
|
user: removeUserSignal(userId, type, value),
|
|
}));
|
|
|
|
ackHandler(socket, 'identityAdmin:setVerified', ({ userId, enabled }) => ({
|
|
user: getUserForAdmin(setVerified(userId, {
|
|
enabled: Boolean(enabled),
|
|
actor: socket?.data?.user?.username || socket.id,
|
|
at: Date.now(),
|
|
}).id),
|
|
}));
|
|
|
|
ackHandler(socket, 'identityAdmin:setDeterrence', ({ userId, enabled, reason }) => ({
|
|
user: getUserForAdmin(setDeterrence(userId, {
|
|
enabled: Boolean(enabled),
|
|
reason: String(reason || '').trim() || null,
|
|
actor: socket?.data?.user?.username || socket.id,
|
|
at: Date.now(),
|
|
}).id),
|
|
}));
|
|
|
|
ackHandler(socket, 'identityAdmin:setMuted', ({ userId, enabled }) => ({
|
|
user: getUserForAdmin(setMuted(userId, {
|
|
enabled: Boolean(enabled),
|
|
actor: socket?.data?.user?.username || socket.id,
|
|
at: Date.now(),
|
|
}).id),
|
|
}));
|
|
|
|
ackHandler(socket, 'identityAdmin:setPermission', ({ userId, permissionKey, enabled }) => ({
|
|
user: getUserForAdmin(setUserPermission(userId, permissionKey, {
|
|
enabled: Boolean(enabled),
|
|
actor: socket?.data?.user?.username || socket.id,
|
|
at: Date.now(),
|
|
}).id),
|
|
}));
|
|
|
|
ackHandler(socket, 'identityAdmin:updateFeatureState', ({ userId, namespace, value }) => {
|
|
const normalized = normalizeFeaturePayload(namespace, value);
|
|
setFeatureState(userId, normalized.namespace, normalized.value);
|
|
return { user: getUserForAdmin(userId) };
|
|
});
|
|
|
|
ackHandler(socket, 'identityAdmin:deleteFeatureState', ({ userId, namespace }) => ({
|
|
user: deleteFeatureState(userId, namespace),
|
|
}));
|
|
});
|
|
|
|
module.exports = {
|
|
isLockdownAdminSocket,
|
|
};
|