add validation to edit route
All checks were successful
Build and push image for doorman-homeassistant / docker (push) Successful in 31s
Build and push Doorman UI / API / docker (push) Successful in 2m15s
Build and push image for doorman-homeassistant / deploy-gitainer (push) Successful in 9s

This commit is contained in:
Martin Dimitrov 2025-06-07 14:15:41 -07:00
parent 0979d03a27
commit 231ea8149d
5 changed files with 60 additions and 34 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -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",

View File

@ -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<TwilioContext, EditRequest> = 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<typeof EditRequestSchema>;
export interface EditRequestTwilio extends ServerlessEventObject<EditRequest, UserAgentHeader> { }
export const handler: ServerlessFunctionSignature<TwilioContext, EditRequestTwilio> = 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<TwilioContext, EditRequest> =
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<TwilioContext, EditRequest> =
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<TwilioContext, EditRequest> =
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<TwilioContext, EditRequest> =
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);
};
});

View File

@ -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));

View File

@ -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<typeof DoorConfigSchema>;
export type EditDoorConfig = z.infer<typeof EditDoorConfigSchema>;
export type EditDoorConfigReq = Omit<EditDoorConfig, "PK" | "SK" | "approvalId">;
export const EditDoorConfigReqSchema = EditDoorConfigSchema.omit({ "PK": true, "SK": true, "approvalId": true });
export type EditDoorConfigReq = z.infer<typeof EditDoorConfigReqSchema>;
export const DoorConfigEntity: DynaBridgeEntity<DoorConfig> = {
tableName: "doorman",