add common validation path. Refactor auth route to use it
This commit is contained in:
parent
fd3878d590
commit
0979d03a27
@ -22,7 +22,8 @@
|
||||
"twilio": "^3.84.1",
|
||||
"winston": "^3.17.0",
|
||||
"winston-loki": "^6.1.3",
|
||||
"zod": "^3.25.42"
|
||||
"zod": "^3.25.42",
|
||||
"zod-validation-error": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"twilio-run": "^3.5.4",
|
||||
|
||||
@ -10,6 +10,8 @@ import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
|
||||
import { createLogger, format, Logger, transports } from "winston";
|
||||
import LokiTransport from "winston-loki";
|
||||
import pTimeout, { TimeoutError } from "promise.timeout";
|
||||
import { ZodError } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
export type BaseEvent = { request: { cookies: {}; headers: {}; }; }
|
||||
|
||||
@ -243,6 +245,17 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
|
||||
}
|
||||
|
||||
callbackResult = failFastCallbackMethod();
|
||||
} else if (e instanceof ZodError) {
|
||||
// global catch for validation errors
|
||||
const response = new Twilio.Response();
|
||||
response.setStatusCode(400);
|
||||
response.setHeaders({
|
||||
"Content-Type": "application/json"
|
||||
});
|
||||
|
||||
// return nice to read error message from ZOD
|
||||
response.setBody({ err: fromError(e).toString() });
|
||||
callbackResult = [null, response];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,26 +13,30 @@ import { AuthMetrics, registerMetrics } from "../../../metrics/AuthMetrics";
|
||||
import { Counter } from "prom-client";
|
||||
import { DoorConfig, getDoorConfigID } from "../../../schema/DoorConfig";
|
||||
import { createLockStatusWithTimeout, getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
|
||||
import { z } from "zod";
|
||||
import { setResponseJson } from "../../../utils/responseUtils";
|
||||
|
||||
export interface AuthRequest extends ServerlessEventObject<{}, UserAgentHeader> {
|
||||
door?: string;
|
||||
key?: string;
|
||||
ip: string;
|
||||
timeout?: string;
|
||||
}
|
||||
export const AuthRequestSchema = z.object({
|
||||
door: z.string(),
|
||||
key: z.string(),
|
||||
ip: z.string().optional(),
|
||||
timeout: z.number().gt(0, "Timeout cannot be 0").optional(),
|
||||
});
|
||||
|
||||
export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> = withMetrics('auth', async (context, event, callback, metricsRegistry) => {
|
||||
export type AuthRequest = z.infer<typeof AuthRequestSchema>;
|
||||
|
||||
export interface AuthRequestTwilio extends ServerlessEventObject<AuthRequest, UserAgentHeader> { };
|
||||
|
||||
export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequestTwilio> = withMetrics('auth', async (context, event, callback, metricsRegistry) => {
|
||||
const response = new Twilio.Response();
|
||||
|
||||
registerMetrics(metricsRegistry);
|
||||
|
||||
let door = event.door;
|
||||
let pin = event.key;
|
||||
let req = AuthRequestSchema.parse(event);
|
||||
|
||||
if (!door || !pin) {
|
||||
response.setStatusCode(400);
|
||||
return callback(null, response);
|
||||
}
|
||||
// now we have validated obj
|
||||
let door = req.door;
|
||||
let pin = req.key;
|
||||
|
||||
const db = createDynaBridgeClient(context);
|
||||
|
||||
@ -40,7 +44,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
||||
|
||||
if (!config) {
|
||||
getMetricFromRegistry<Counter>(metricsRegistry, AuthMetrics.DOOR_CONFIG_NOT_FOUND).inc({ door }, 1);
|
||||
response.setStatusCode(404);
|
||||
setResponseJson(response, 404, {
|
||||
err: `Door ${door} not found`,
|
||||
});
|
||||
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
@ -58,7 +65,9 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
||||
}
|
||||
|
||||
if (!method) {
|
||||
response.setStatusCode(401);
|
||||
setResponseJson(response, 401, {
|
||||
err: "Invalid PIN",
|
||||
});
|
||||
return callback(null, response);
|
||||
}
|
||||
|
||||
@ -72,7 +81,7 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
||||
};
|
||||
|
||||
// take timeout from the query string
|
||||
const timeout = event.timeout ? parseInt(event.timeout) : config.timeout;
|
||||
const timeout = event.timeout || config.timeout;
|
||||
|
||||
// check lock status if locked, then unlock. If unlocked then lock
|
||||
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
|
||||
@ -81,21 +90,18 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
||||
if (isOpen && lock) {
|
||||
const fingerprint = JSON.parse(lock.fingerprint);
|
||||
|
||||
response
|
||||
.setStatusCode(200)
|
||||
.appendHeader('Content-Type', 'application/json')
|
||||
.setBody({
|
||||
status: DoorStatus.CLOSED,
|
||||
fingerprint,
|
||||
});
|
||||
setResponseJson(response, 200, {
|
||||
status: DoorStatus.CLOSED,
|
||||
fingerprint,
|
||||
});
|
||||
|
||||
await db.entities.lockStatus.deleteById(getLockStatusID(door));
|
||||
} else {
|
||||
await db.entities.lockStatus.save(createLockStatusWithTimeout(door, timeout, fingerprint));
|
||||
response
|
||||
.setStatusCode(200)
|
||||
.appendHeader('Content-Type', 'application/json')
|
||||
.setBody({ msg: `Opened the door "${door}" for ${timeout}s` });
|
||||
|
||||
setResponseJson(response, 200, {
|
||||
msg: `Opened the door "${door}" for ${timeout}s`
|
||||
});
|
||||
}
|
||||
|
||||
// destroy the internal client after
|
||||
|
||||
17
packages/doorman-api/src/utils/responseUtils.ts
Normal file
17
packages/doorman-api/src/utils/responseUtils.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { TwilioResponse } from "@twilio-labs/serverless-runtime-types/types"
|
||||
|
||||
/**
|
||||
* Fill a twilio response object with JSON and set the appropriate headers
|
||||
* @param response
|
||||
* @param statusCode
|
||||
* @param json
|
||||
* @returns twilio response
|
||||
*/
|
||||
export const setResponseJson = (response: TwilioResponse, statusCode: number, json: any): TwilioResponse => {
|
||||
response
|
||||
.setStatusCode(statusCode)
|
||||
.appendHeader('Content-Type', 'application/json')
|
||||
.setBody(json);
|
||||
|
||||
return response;
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
import { Button, FormField, Icon, Input, SpaceBetween } from "@cloudscape-design/components";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { AuthRequest } from "../../../doorman-api/src/functions/api/door/auth";
|
||||
import { fetchUrlEncoded } from "../helpers/FetchHelper";
|
||||
|
||||
// COPIED FROM SERVER
|
||||
export enum IAuthMode {
|
||||
FIXED_PIN = "Fixed Pin",
|
||||
@ -18,7 +21,7 @@ export interface IAuthComponentProps {
|
||||
}
|
||||
|
||||
export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCheck }: IAuthComponentProps) => {
|
||||
const [ key, setKey ] = useState(secret);
|
||||
const [ key, setKey ] = useState(secret || "");
|
||||
const [ error, setError ] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
@ -38,15 +41,21 @@ export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCh
|
||||
return "null";
|
||||
});
|
||||
|
||||
fetch(`/api/door/auth?key=${key}&rotatingKey=${key}&door=${door}&ip=${ip}`)
|
||||
.then(async res => {
|
||||
if (res.status !== 200) {
|
||||
setError("Incorrect PIN");
|
||||
onError && onError(await res.json());
|
||||
return;
|
||||
}
|
||||
onUnlock();
|
||||
})
|
||||
const body: AuthRequest = {
|
||||
door,
|
||||
ip,
|
||||
key,
|
||||
};
|
||||
|
||||
fetchUrlEncoded('/api/door/auth', body)
|
||||
.then(async res => {
|
||||
if (res.status !== 200) {
|
||||
setError("Incorrect PIN");
|
||||
onError && onError(await res.json());
|
||||
return;
|
||||
}
|
||||
onUnlock();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
5
packages/doorman-ui/src/helpers/FetchHelper.ts
Normal file
5
packages/doorman-ui/src/helpers/FetchHelper.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const fetchUrlEncoded = (url: string, body: Record<string, any>): Promise<Response> => {
|
||||
const params = new URLSearchParams(body);
|
||||
|
||||
return fetch(url + '?' + params.toString());
|
||||
}
|
||||
@ -183,7 +183,7 @@ export function DoorPage() {
|
||||
authMode={IAuthMode.FIXED_PIN}
|
||||
runCheck={submitPin}
|
||||
door={door}
|
||||
secret={secret}
|
||||
secret={secret}
|
||||
onUnlock={() => {
|
||||
setStep(2);
|
||||
dismissInProgressAlerts();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user