From e17bb267fe40c3d5d78eb590f1b2931a026d2ab8 Mon Sep 17 00:00:00 2001 From: Martin Dimitrov Date: Sat, 7 Jun 2025 16:15:33 -0700 Subject: [PATCH] update info route --- .../src/functions/api/door/info.ts | 105 ++++++++---------- packages/doorman-ui/src/pages/DoorPage.tsx | 9 +- 2 files changed, 55 insertions(+), 59 deletions(-) diff --git a/packages/doorman-api/src/functions/api/door/info.ts b/packages/doorman-api/src/functions/api/door/info.ts index 66b1e56..40324f0 100644 --- a/packages/doorman-api/src/functions/api/door/info.ts +++ b/packages/doorman-api/src/functions/api/door/info.ts @@ -6,38 +6,40 @@ import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs import { TwilioContext } from "../../../types/TwilioContext"; import { createDynaBridgeClient } from "../../../utils/ddb"; import { DoorStatus } from "../../../types/DoorStatus"; -import { getDoorConfigID } from "../../../schema/DoorConfig"; +import { DoorConfigSchema, getDoorConfigID } from "../../../schema/DoorConfig"; import { getDoorAliasID } from "../../../schema/DoorAlias"; import { getLockStatusID, isLockOpen } from "../../../schema/LockStatus"; +import { withMetrics } from "../../../common/DoormanHandler"; +import { z } from "zod"; +import { UserAgentHeader } from "../../../utils/blockUserAgent"; +import { setResponseJson } from "../../../utils/responseUtils"; -export interface InfoRequest extends ServerlessEventObject { - door?: string; - buzzer?: string; +export const InfoRequestSchema = z.object({ + door: z.string().optional(), + buzzer: z.string().optional(), +}) +.partial() +.refine(data => data.buzzer || data.door, 'Buzzer or door must be provided'); - // TODO: change these to be multiple - discordUser: string; - msg: string; - json: string; -} +export type InfoRequest = z.infer; +export interface InfoRequestTwilio extends ServerlessEventObject { }; -export interface InfoResponseClient { - buzzer: string; - door: string; - pressKey: string; - fallbackNumbers: string[]; - discordUsers: string[]; -} +export const InfoResponseClientSchema = DoorConfigSchema + .omit({ PK: true, SK: true, pin: true }) + .extend({ door: z.string() }); -export const handler: ServerlessFunctionSignature = async function(context, event, callback) { +export type InfoResponseClient = z.infer; + +export const InfoResponseUISchema = InfoResponseClientSchema.extend({ id: z.string(), status: z.nativeEnum(DoorStatus) }); + +export type InfoResponseUI = z.infer; + +export const handler: ServerlessFunctionSignature = withMetrics("info", async (context, event, callback, metricsRegistry) => { const response = new Twilio.Response(); + const req = InfoRequestSchema.parse(event); - let door = event.door; - const buzzer = event.buzzer?.slice(-10); - - if (!door && !buzzer) { - response.setStatusCode(400); - return callback(null, response); - } + let door = req.door; + const buzzer = req.buzzer?.slice(-10); const db = createDynaBridgeClient(context); @@ -45,10 +47,9 @@ export const handler: ServerlessFunctionSignature = door = await db.entities.doorAlias.findById(getDoorAliasID(buzzer)) .then(async (alias) => { if (!alias) { - response - .setStatusCode(404) - .appendHeader('Content-Type', 'application/json') - .setBody({ err: "This buzzer is not registered" }); + setResponseJson(response, 404, { + err: "This buzzer is not registered", + }); return undefined; } return alias.name; @@ -59,42 +60,32 @@ export const handler: ServerlessFunctionSignature = const config = await db.entities.doorConfig.findById(getDoorConfigID(door)); if (!config) { - response - .setStatusCode(404) - .appendHeader('Content-Type', 'application/json') - .setBody({ err: "This buzzer is not registered properly" }); + setResponseJson(response, 404, { + err: "This buzzer is not registered", + }); } else { if (buzzer) { // respond to twilio CLIENT - response - .setStatusCode(200) - .appendHeader('Content-Type', 'application/json') - .setBody({ - buzzer, - door, - fallbackNumbers: config.fallbackNumbers, - pressKey: config.pressKey, - discordUsers: config.discordUsers, - }); + const body: InfoResponseClient = InfoResponseClientSchema.parse({ + ...config, + buzzer, + door, + }); + + setResponseJson(response, 200, body); } else { const lock = await db.entities.lockStatus.findById(getLockStatusID(door)); const status = isLockOpen(lock) ? DoorStatus.OPEN: DoorStatus.CLOSED; + + const body: InfoResponseUI = InfoResponseUISchema.parse({ + ...config, + id: door, + door, + status, + }) // respond to UI - response - .setStatusCode(200) - .appendHeader('Content-Type', 'application/json') - .setBody({ - id: door, - timeout: config.timeout, - buzzer: config.buzzer, - status, - buzzerCode: config.buzzerCode, - fallbackNumbers: config.fallbackNumbers, - pressKey: config.pressKey, - discordUsers: config.discordUsers, - greeting: config.greeting || "", - }); + setResponseJson(response, 200, body); } } } @@ -104,4 +95,4 @@ export const handler: ServerlessFunctionSignature = db.ddbClient.destroy(); return callback(null, response); -}; \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/doorman-ui/src/pages/DoorPage.tsx b/packages/doorman-ui/src/pages/DoorPage.tsx index 8875761..a6fd23c 100644 --- a/packages/doorman-ui/src/pages/DoorPage.tsx +++ b/packages/doorman-ui/src/pages/DoorPage.tsx @@ -5,6 +5,7 @@ import { AuthComponent, IAuthMode } from "../components/AuthComponent"; import OtpInput from 'react-otp-input'; import { CountdownBar } from "../components/CountdownBar"; import { DoorResponse } from "../types/DoorResponse"; +import { fetchUrlEncoded } from "../helpers/FetchHelper"; export async function loader({ params, request }: any) { const door = new URL(request.url).searchParams.get('door'); @@ -13,7 +14,9 @@ export async function loader({ params, request }: any) { return {}; } - const response = await fetch(`/api/door/info?door=${door}`).then(res => res.json()); + const response = await fetchUrlEncoded('/api/door/info', { + door, + }).then(res => res.json()); console.log(response); @@ -70,7 +73,9 @@ export function DoorPage() { } const timer = setInterval(async () => { - const response = await fetch(`/api/door/info?door=${door}`).then(res => res.json()); + const response = await fetchUrlEncoded('/api/door/info', { + door, + }).then(res => res.json()); // polling assumes that the door was opened and whatever closed it was the buzzer system... // ie. state transition from OPEN to CLOSED before timeout means that twilio opened the door