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",
|
"twilio": "^3.84.1",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-loki": "^6.1.3",
|
"winston-loki": "^6.1.3",
|
||||||
"zod": "^3.25.42"
|
"zod": "^3.25.42",
|
||||||
|
"zod-validation-error": "^3.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"twilio-run": "^3.5.4",
|
"twilio-run": "^3.5.4",
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import VoiceResponse from 'twilio/lib/twiml/VoiceResponse';
|
|||||||
import { createLogger, format, Logger, transports } from "winston";
|
import { createLogger, format, Logger, transports } from "winston";
|
||||||
import LokiTransport from "winston-loki";
|
import LokiTransport from "winston-loki";
|
||||||
import pTimeout, { TimeoutError } from "promise.timeout";
|
import pTimeout, { TimeoutError } from "promise.timeout";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
export type BaseEvent = { request: { cookies: {}; headers: {}; }; }
|
export type BaseEvent = { request: { cookies: {}; headers: {}; }; }
|
||||||
|
|
||||||
@ -243,6 +245,17 @@ export function withMetrics<T extends DoormanLambdaContext, U extends BaseEvent>
|
|||||||
}
|
}
|
||||||
|
|
||||||
callbackResult = failFastCallbackMethod();
|
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 { Counter } from "prom-client";
|
||||||
import { DoorConfig, getDoorConfigID } from "../../../schema/DoorConfig";
|
import { DoorConfig, getDoorConfigID } from "../../../schema/DoorConfig";
|
||||||
import { createLockStatusWithTimeout, getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
|
import { createLockStatusWithTimeout, getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { setResponseJson } from "../../../utils/responseUtils";
|
||||||
|
|
||||||
export interface AuthRequest extends ServerlessEventObject<{}, UserAgentHeader> {
|
export const AuthRequestSchema = z.object({
|
||||||
door?: string;
|
door: z.string(),
|
||||||
key?: string;
|
key: z.string(),
|
||||||
ip: string;
|
ip: z.string().optional(),
|
||||||
timeout?: string;
|
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();
|
const response = new Twilio.Response();
|
||||||
|
|
||||||
registerMetrics(metricsRegistry);
|
registerMetrics(metricsRegistry);
|
||||||
|
|
||||||
let door = event.door;
|
let req = AuthRequestSchema.parse(event);
|
||||||
let pin = event.key;
|
|
||||||
|
|
||||||
if (!door || !pin) {
|
// now we have validated obj
|
||||||
response.setStatusCode(400);
|
let door = req.door;
|
||||||
return callback(null, response);
|
let pin = req.key;
|
||||||
}
|
|
||||||
|
|
||||||
const db = createDynaBridgeClient(context);
|
const db = createDynaBridgeClient(context);
|
||||||
|
|
||||||
@ -40,7 +44,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
|||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
getMetricFromRegistry<Counter>(metricsRegistry, AuthMetrics.DOOR_CONFIG_NOT_FOUND).inc({ door }, 1);
|
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);
|
return callback(null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +65,9 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!method) {
|
if (!method) {
|
||||||
response.setStatusCode(401);
|
setResponseJson(response, 401, {
|
||||||
|
err: "Invalid PIN",
|
||||||
|
});
|
||||||
return callback(null, response);
|
return callback(null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +81,7 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
|||||||
};
|
};
|
||||||
|
|
||||||
// take timeout from the query string
|
// 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
|
// check lock status if locked, then unlock. If unlocked then lock
|
||||||
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
|
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
|
||||||
@ -81,10 +90,7 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
|||||||
if (isOpen && lock) {
|
if (isOpen && lock) {
|
||||||
const fingerprint = JSON.parse(lock.fingerprint);
|
const fingerprint = JSON.parse(lock.fingerprint);
|
||||||
|
|
||||||
response
|
setResponseJson(response, 200, {
|
||||||
.setStatusCode(200)
|
|
||||||
.appendHeader('Content-Type', 'application/json')
|
|
||||||
.setBody({
|
|
||||||
status: DoorStatus.CLOSED,
|
status: DoorStatus.CLOSED,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
});
|
});
|
||||||
@ -92,10 +98,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
|
|||||||
await db.entities.lockStatus.deleteById(getLockStatusID(door));
|
await db.entities.lockStatus.deleteById(getLockStatusID(door));
|
||||||
} else {
|
} else {
|
||||||
await db.entities.lockStatus.save(createLockStatusWithTimeout(door, timeout, fingerprint));
|
await db.entities.lockStatus.save(createLockStatusWithTimeout(door, timeout, fingerprint));
|
||||||
response
|
|
||||||
.setStatusCode(200)
|
setResponseJson(response, 200, {
|
||||||
.appendHeader('Content-Type', 'application/json')
|
msg: `Opened the door "${door}" for ${timeout}s`
|
||||||
.setBody({ msg: `Opened the door "${door}" for ${timeout}s` });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// destroy the internal client after
|
// 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 { Button, FormField, Icon, Input, SpaceBetween } from "@cloudscape-design/components";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import type { AuthRequest } from "../../../doorman-api/src/functions/api/door/auth";
|
||||||
|
import { fetchUrlEncoded } from "../helpers/FetchHelper";
|
||||||
|
|
||||||
// COPIED FROM SERVER
|
// COPIED FROM SERVER
|
||||||
export enum IAuthMode {
|
export enum IAuthMode {
|
||||||
FIXED_PIN = "Fixed Pin",
|
FIXED_PIN = "Fixed Pin",
|
||||||
@ -18,7 +21,7 @@ export interface IAuthComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCheck }: 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("");
|
const [ error, setError ] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,7 +41,13 @@ export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCh
|
|||||||
return "null";
|
return "null";
|
||||||
});
|
});
|
||||||
|
|
||||||
fetch(`/api/door/auth?key=${key}&rotatingKey=${key}&door=${door}&ip=${ip}`)
|
const body: AuthRequest = {
|
||||||
|
door,
|
||||||
|
ip,
|
||||||
|
key,
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUrlEncoded('/api/door/auth', body)
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
if (res.status !== 200) {
|
if (res.status !== 200) {
|
||||||
setError("Incorrect PIN");
|
setError("Incorrect PIN");
|
||||||
@ -46,7 +55,7 @@ export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCh
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onUnlock();
|
onUnlock();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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());
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user