From 231ea8149d8c265d91ac56ec99b99d65f881ed60 Mon Sep 17 00:00:00 2001 From: Martin Dimitrov Date: Sat, 7 Jun 2025 14:15:41 -0700 Subject: [PATCH] add validation to edit route --- bun.lockb | Bin 431136 -> 432280 bytes packages/doorman-api/package.json | 4 +- .../src/functions/api/door/edit.ts | 83 +++++++++++------- packages/doorman-api/src/index.ts | 2 +- packages/doorman-api/src/schema/DoorConfig.ts | 5 +- 5 files changed, 60 insertions(+), 34 deletions(-) diff --git a/bun.lockb b/bun.lockb index 031443ff4ec7c1930a4f2a6106e3aac399472428..21baeaec17b7ff382c388339bc9e310bbe258739 100755 GIT binary patch delta 7130 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 { - door?: string; - approvalId?: string; - newConfig?: string; -} +import { zu } from "zod_utilz"; -export const handler: ServerlessFunctionSignature = async function(context, event, callback) { +// @ts-ignore +import isDeepSubset from "is-deep-subset"; + +export const EditRequestSchema = z.object({ + door: z.string(), + approvalId: z.string().optional(), + newConfig: zu.stringToJSON().optional(), +}); + +export type EditRequest = z.infer; + +export interface EditRequestTwilio extends ServerlessEventObject { } + +export const handler: ServerlessFunctionSignature = withMetrics("edit", async (context, event, callback, metricsRegistry) => { const response = new Twilio.Response(); - if (shouldBlockRequest(event)) { - response.setStatusCode(200); - return callback(null, response); - } + const req = EditRequestSchema.parse(event); - let door = event.door; - let approvalId = event.approvalId; - let newConfigString = event.newConfig; + let door = req.door; + let approvalId = req.approvalId; + let newConfigRaw = req.newConfig; const db = createDynaBridgeClient(context); // approve path - if (door && approvalId) { + if (approvalId) { const newConfig = await db.entities.editDoorConfig.findById(getEditDoorConfigID(door)); if (!newConfig || newConfig.approvalId !== approvalId) { @@ -39,7 +48,7 @@ export const handler: ServerlessFunctionSignature = return callback(null, response); } - db.entities.doorConfig.save(editDoorToDoorConfig(newConfig)); + await db.entities.doorConfig.save(editDoorToDoorConfig(newConfig)); // send update to discord users const updateMessage = `Configuration change \`${approvalId}\` was approved @ Door "${door}"`; @@ -60,16 +69,21 @@ export const handler: ServerlessFunctionSignature = return callback(null, response); } - if (!door || !newConfigString) { - response.setStatusCode(400); + if (!newConfigRaw) { + setResponseJson(response, 400, { + err: "Missing new config", + }); return callback(null, response); } - const newConfig: EditDoorConfigReq = JSON.parse(newConfigString); + const newConfig = EditDoorConfigReqSchema.parse(newConfigRaw); + const config = await db.entities.doorConfig.findById(getDoorConfigID(door)); if (!config) { - response.setStatusCode(404); + setResponseJson(response, 404, { + err: `Door not found ${door}`, + }); return callback(null, response); } @@ -78,14 +92,24 @@ export const handler: ServerlessFunctionSignature = newConfig.pin = config.pin; } + // if nothing changed, we should throw since this is a pointless change + if (isDeepSubset(config, newConfig)) { + setResponseJson(response, 400, { + err: "Nothing changed in the new config", + }); + + return callback(null, response); + } + const editDoorConfig = createEditDoorConfig(door, newConfig); await db.entities.editDoorConfig.save(editDoorConfig); - // newConfig.discordUsers = undefined; - // newConfig.fallbackNumbers = undefined; - // newConfig.status = undefined; + const params: EditRequest = { + door, + approvalId: editDoorConfig.approvalId, + }; - const approvalUrl = `https://doorman.chromart.cc/api/door/edit?door=${door}&approvalId=${editDoorConfig.approvalId as string}`; + const approvalUrl = `https://doorman.chromart.cc/api/door/edit?` + (new URLSearchParams(params as any)).toString(); console.log(approvalUrl); // send update to discord users @@ -101,14 +125,13 @@ export const handler: ServerlessFunctionSignature = await Promise.all(discordPromises); - response - .setStatusCode(200) - .appendHeader('Content-Type', 'application/json') - .setBody({ msg: "Created Configuration change" }); + setResponseJson(response, 200, { + msg: 'Created configuration change', + }); // destroy the internal client after // @ts-ignore db.ddbClient.destroy(); return callback(null, response); -}; +}); diff --git a/packages/doorman-api/src/index.ts b/packages/doorman-api/src/index.ts index 63f30e1..5bf24b5 100644 --- a/packages/doorman-api/src/index.ts +++ b/packages/doorman-api/src/index.ts @@ -12,7 +12,7 @@ const imports = functionFiles.forEach(file => require('./' + path.relative('src' console.log("functions to build:", functionFiles); -const bundledModules = ['dynabridge']; +const bundledModules = ['dynabridge', 'zod_utilz']; const externalModules = Object.keys(require('../package.json').dependencies) .filter(dep => !bundledModules.includes(dep)); diff --git a/packages/doorman-api/src/schema/DoorConfig.ts b/packages/doorman-api/src/schema/DoorConfig.ts index e78177b..7a37030 100644 --- a/packages/doorman-api/src/schema/DoorConfig.ts +++ b/packages/doorman-api/src/schema/DoorConfig.ts @@ -14,7 +14,7 @@ export const DoorConfigSchema = z.object({ buzzerCode: z.string(), discordUsers: z.array(z.string()), fallbackNumbers: z.array(z.string()), - pin: z.string(), + pin: z.string().default(""), pressKey: z.string(), greeting: z.string().optional(), timeout: z.number(), @@ -39,7 +39,8 @@ export const getEditDoorConfigID = (doorName: string): string[] => { export type DoorConfig = z.infer; export type EditDoorConfig = z.infer; -export type EditDoorConfigReq = Omit; +export const EditDoorConfigReqSchema = EditDoorConfigSchema.omit({ "PK": true, "SK": true, "approvalId": true }); +export type EditDoorConfigReq = z.infer; export const DoorConfigEntity: DynaBridgeEntity = { tableName: "doorman",