From c8b35aec1addbca2b5691cdfcc9d8f8e74e89eca Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Thu, 4 Jun 2026 10:51:20 +0300 Subject: [PATCH 1/2] feat: add info params (notes, remarks, site, location) to work order schema Accept optional info object with notes, remarks, site, and location on POST /auth/work-orders and PATCH /auth/work-orders/:id. The handler merges these into the thing info alongside the top-level fields. --- .../handlers/work.orders.handlers.test.js | 26 +++++++++++++++++++ .../server/handlers/work.orders.handlers.js | 6 +++-- .../lib/server/schemas/work.orders.schemas.js | 24 +++++++++++++++-- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/tests/unit/handlers/work.orders.handlers.test.js b/tests/unit/handlers/work.orders.handlers.test.js index bfeca37..ba6ae76 100644 --- a/tests/unit/handlers/work.orders.handlers.test.js +++ b/tests/unit/handlers/work.orders.handlers.test.js @@ -48,6 +48,32 @@ test('handlers: createWorkOrder Type 2 resolves part and forwards body as info', t.is(flow.lastPush.params[0].info.partsMoves[0].role, 'diagnosis') }) +test('handlers: createWorkOrder merges info.notes, info.remarks, info.site, info.location into thing info', async (t) => { + const flow = buildSubmitFlow({ parts: [{ id: 'part-1', code: 'PSU-1', type: 'inventory-miner_part-psu', info: { serialNum: 'SN-1' } }] }) + await handlers.createWorkOrder(flow.ctx, { + ...userMeta(), + body: { + type: 1, + deviceType: 'psu', + deviceModel: 'PSU-WM-CB6_V5', + deviceIdentifier: 'SN-1', + info: { + notes: 'batch registration', + remarks: 'test remark', + site: 'Ivinhema', + location: 'Site Warehouse' + } + } + }) + const info = flow.lastPush.params[0].info + t.is(info.notes, 'batch registration') + t.is(info.remarks, 'test remark') + t.is(info.site, 'Ivinhema') + t.is(info.location, 'Site Warehouse') + t.is(info.deviceType, 'psu', 'top-level fields still present') + t.ok(!info.info, 'no nested info.info') +}) + test('handlers: createWorkOrder rejects unknown deviceType with ERR_INVALID_DEVICE_TYPE', async (t) => { const flow = buildSubmitFlow() await t.exception( diff --git a/workers/lib/server/handlers/work.orders.handlers.js b/workers/lib/server/handlers/work.orders.handlers.js index e66e93d..81b7480 100644 --- a/workers/lib/server/handlers/work.orders.handlers.js +++ b/workers/lib/server/handlers/work.orders.handlers.js @@ -35,7 +35,8 @@ async function createWorkOrder (ctx, req) { } const voter = req._info.user.metadata.email - const info = { ...req.body, createdBy: voter, createdAt: Date.now() } + const { info: extraInfo, ...body } = req.body + const info = { ...body, ...extraInfo, createdBy: voter, createdAt: Date.now() } if (type === WORK_ORDER_TYPES.REGULAR) { const part = await _resolvePartByIdentifier(ctx, deviceIdentifier) @@ -73,7 +74,8 @@ async function createWorkOrder (ctx, req) { } async function updateWorkOrder (ctx, req) { - return submitWorkOrderAction(ctx, req, 'updateThing', { id: req.params.id, info: { ...req.body } }) + const { info: extraInfo, ...body } = req.body + return submitWorkOrderAction(ctx, req, 'updateThing', { id: req.params.id, info: { ...body, ...extraInfo } }) } async function closeWorkOrder (ctx, req) { diff --git a/workers/lib/server/schemas/work.orders.schemas.js b/workers/lib/server/schemas/work.orders.schemas.js index 55c300e..f0858e8 100644 --- a/workers/lib/server/schemas/work.orders.schemas.js +++ b/workers/lib/server/schemas/work.orders.schemas.js @@ -22,7 +22,17 @@ const create = { deviceIdentifier: { type: 'string', minLength: 1, maxLength: 200 }, issue: { type: 'string', minLength: 1, maxLength: 2000 }, assignedTo: { type: ['string', 'null'], maxLength: 200 }, - warranty + warranty, + info: { + type: 'object', + additionalProperties: false, + properties: { + notes: { type: 'string', maxLength: 4000 }, + remarks: { type: 'string', maxLength: 4000 }, + site: { type: 'string', maxLength: 200 }, + location: { type: 'string', maxLength: 200 } + } + } }, if: { properties: { type: { const: 2 } } }, then: { required: ['issue'] } @@ -73,7 +83,17 @@ const update = { deviceIdentifier: { type: 'string', minLength: 1, maxLength: 200 }, assignedTo: { type: ['string', 'null'], maxLength: 200 }, finalResult: { type: ['string', 'null'], maxLength: 4000 }, - warranty + warranty, + info: { + type: 'object', + additionalProperties: false, + properties: { + notes: { type: 'string', maxLength: 4000 }, + remarks: { type: 'string', maxLength: 4000 }, + site: { type: 'string', maxLength: 200 }, + location: { type: 'string', maxLength: 200 } + } + } } } } From 68880587dcf895e89569b80ccb721b02eb224fa0 Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Thu, 4 Jun 2026 13:22:51 +0300 Subject: [PATCH 2/2] fix: auto-qualify bare permission names in tokenHasPerms tokenHasPerms received bare resource names (e.g. 'work_order') from route perms, but _permsMatch expects 'resource:level' format. Splitting a bare name by ':' left `required` as undefined, crashing on spread. --- tests/unit/lib/auth.test.js | 28 ++++++++++++++++++++++++++++ workers/lib/auth.js | 6 +++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/unit/lib/auth.test.js b/tests/unit/lib/auth.test.js index 8ed56c4..c77b573 100644 --- a/tests/unit/lib/auth.test.js +++ b/tests/unit/lib/auth.test.js @@ -348,6 +348,34 @@ test('AuthLib - tokenHasPerms with matchAll=false', async (t) => { t.pass() }) +test('AuthLib - tokenHasPerms auto-qualifies bare perm names', async (t) => { + const mockAuth = { + getTokenPerms: function () { + return { superadmin: false, perms: ['work_order:rw', 'actions:rw'] } + }, + conf: { + superAdminPerms: [] + } + } + const authLib = new AuthLib({ + httpc: {}, + httpd: {}, + userService: {}, + auth: mockAuth + }) + + const resultWrite = await authLib.tokenHasPerms('token', true, ['work_order']) + t.is(resultWrite, true, 'bare perm with write=true should resolve to work_order:rw') + + const resultRead = await authLib.tokenHasPerms('token', false, ['work_order']) + t.is(resultRead, true, 'bare perm with write=false should resolve to work_order:r') + + const resultMissing = await authLib.tokenHasPerms('token', true, ['inventory']) + t.is(resultMissing, false, 'bare perm not in user perms should return false') + + t.pass() +}) + test('AuthLib - cleanupTokens', async (t) => { let cleanupTokensCalled = false const mockAuth = { diff --git a/workers/lib/auth.js b/workers/lib/auth.js index 626efc8..aa14975 100644 --- a/workers/lib/auth.js +++ b/workers/lib/auth.js @@ -88,7 +88,11 @@ class AuthLib { return false } - const resolved = requestedPerms.map(perm => this._permsMatch(perms.permissions, perm)) + const level = write ? 'rw' : 'r' + const resolved = requestedPerms.map(perm => { + const qualified = perm.includes(':') ? perm : `${perm}:${level}` + return this._permsMatch(perms.permissions, qualified) + }) return matchAll ? resolved.every(res => res)