Compare commits
2 Commits
fd3878d590
...
231ea8149d
| Author | SHA1 | Date | |
|---|---|---|---|
| 231ea8149d | |||
| 0979d03a27 |
@ -17,12 +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": "^3.25.42",
|
||||
"zod-validation-error": "^3.4.1",
|
||||
"zod_utilz": "^0.8.4"
|
||||
},
|
||||
"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
|
||||
|
||||
@ -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);
|
||||
};
|
||||
});
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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",
|
||||
|
||||
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