diff --git a/bun.lockb b/bun.lockb index 031443f..21baeae 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/doorman-api/package.json b/packages/doorman-api/package.json index 1bcf997..dfab9cd 100644 --- a/packages/doorman-api/package.json +++ b/packages/doorman-api/package.json @@ -17,13 +17,15 @@ "discord-oauth2": "^2.12.1", "discord.js": "^14.19.3", "dynabridge": "^0.3.8", + "is-deep-subset": "^0.1.1", "prom-client": "^15.1.3", "promise.timeout": "^1.2.0", "twilio": "^3.84.1", "winston": "^3.17.0", "winston-loki": "^6.1.3", "zod": "^3.25.42", - "zod-validation-error": "^3.4.1" + "zod-validation-error": "^3.4.1", + "zod_utilz": "^0.8.4" }, "devDependencies": { "twilio-run": "^3.5.4", diff --git a/packages/doorman-api/src/functions/api/door/edit.ts b/packages/doorman-api/src/functions/api/door/edit.ts index 98f31bd..3270c43 100644 --- a/packages/doorman-api/src/functions/api/door/edit.ts +++ b/packages/doorman-api/src/functions/api/door/edit.ts @@ -5,33 +5,42 @@ import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types"; import { TwilioContext } from "../../../types/TwilioContext"; -import { shouldBlockRequest, UserAgentHeader } from "../../../utils/blockUserAgent"; +import { UserAgentHeader } from "../../../utils/blockUserAgent"; import { createDynaBridgeClient } from "../../../utils/ddb"; import { sendMessageToUser } from "../../../utils/discord"; -import { createEditDoorConfig, EditDoorConfigReq, editDoorToDoorConfig, getDoorConfigID, getEditDoorConfigID } from "../../../schema/DoorConfig"; +import { createEditDoorConfig, EditDoorConfigReqSchema, editDoorToDoorConfig, getDoorConfigID, getEditDoorConfigID } from "../../../schema/DoorConfig"; +import { z } from "zod"; +import { withMetrics } from "../../../common/DoormanHandler"; +import { setResponseJson } from "../../../utils/responseUtils"; -export interface EditRequest extends ServerlessEventObject<{}, UserAgentHeader> { - 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",