Compare commits
No commits in common. "231ea8149d8c265d91ac56ec99b99d65f881ed60" and "fd3878d590ae39f370fe44a2442831b7279cdb4a" have entirely different histories.
231ea8149d
...
fd3878d590
@ -17,15 +17,12 @@
|
|||||||
"discord-oauth2": "^2.12.1",
|
"discord-oauth2": "^2.12.1",
|
||||||
"discord.js": "^14.19.3",
|
"discord.js": "^14.19.3",
|
||||||
"dynabridge": "^0.3.8",
|
"dynabridge": "^0.3.8",
|
||||||
"is-deep-subset": "^0.1.1",
|
|
||||||
"prom-client": "^15.1.3",
|
"prom-client": "^15.1.3",
|
||||||
"promise.timeout": "^1.2.0",
|
"promise.timeout": "^1.2.0",
|
||||||
"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",
|
|
||||||
"zod_utilz": "^0.8.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"twilio-run": "^3.5.4",
|
"twilio-run": "^3.5.4",
|
||||||
|
|||||||
@ -10,8 +10,6 @@ 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: {}; }; }
|
||||||
|
|
||||||
@ -245,17 +243,6 @@ 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,30 +13,26 @@ 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 const AuthRequestSchema = z.object({
|
export interface AuthRequest extends ServerlessEventObject<{}, UserAgentHeader> {
|
||||||
door: z.string(),
|
door?: string;
|
||||||
key: z.string(),
|
key?: string;
|
||||||
ip: z.string().optional(),
|
ip: string;
|
||||||
timeout: z.number().gt(0, "Timeout cannot be 0").optional(),
|
timeout?: string;
|
||||||
});
|
}
|
||||||
|
|
||||||
export type AuthRequest = z.infer<typeof AuthRequestSchema>;
|
export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> = withMetrics('auth', async (context, event, callback, metricsRegistry) => {
|
||||||
|
|
||||||
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 req = AuthRequestSchema.parse(event);
|
let door = event.door;
|
||||||
|
let pin = event.key;
|
||||||
|
|
||||||
// now we have validated obj
|
if (!door || !pin) {
|
||||||
let door = req.door;
|
response.setStatusCode(400);
|
||||||
let pin = req.key;
|
return callback(null, response);
|
||||||
|
}
|
||||||
|
|
||||||
const db = createDynaBridgeClient(context);
|
const db = createDynaBridgeClient(context);
|
||||||
|
|
||||||
@ -44,10 +40,7 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequestTwil
|
|||||||
|
|
||||||
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);
|
||||||
setResponseJson(response, 404, {
|
response.setStatusCode(404);
|
||||||
err: `Door ${door} not found`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return callback(null, response);
|
return callback(null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,9 +58,7 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequestTwil
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!method) {
|
if (!method) {
|
||||||
setResponseJson(response, 401, {
|
response.setStatusCode(401);
|
||||||
err: "Invalid PIN",
|
|
||||||
});
|
|
||||||
return callback(null, response);
|
return callback(null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,7 +72,7 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequestTwil
|
|||||||
};
|
};
|
||||||
|
|
||||||
// take timeout from the query string
|
// take timeout from the query string
|
||||||
const timeout = event.timeout || config.timeout;
|
const timeout = event.timeout ? parseInt(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));
|
||||||
@ -90,7 +81,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequestTwil
|
|||||||
if (isOpen && lock) {
|
if (isOpen && lock) {
|
||||||
const fingerprint = JSON.parse(lock.fingerprint);
|
const fingerprint = JSON.parse(lock.fingerprint);
|
||||||
|
|
||||||
setResponseJson(response, 200, {
|
response
|
||||||
|
.setStatusCode(200)
|
||||||
|
.appendHeader('Content-Type', 'application/json')
|
||||||
|
.setBody({
|
||||||
status: DoorStatus.CLOSED,
|
status: DoorStatus.CLOSED,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
});
|
});
|
||||||
@ -98,10 +92,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequestTwil
|
|||||||
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
|
||||||
setResponseJson(response, 200, {
|
.setStatusCode(200)
|
||||||
msg: `Opened the door "${door}" for ${timeout}s`
|
.appendHeader('Content-Type', 'application/json')
|
||||||
});
|
.setBody({ msg: `Opened the door "${door}" for ${timeout}s` });
|
||||||
}
|
}
|
||||||
|
|
||||||
// destroy the internal client after
|
// destroy the internal client after
|
||||||
|
|||||||
@ -5,42 +5,33 @@
|
|||||||
|
|
||||||
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
|
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
|
||||||
import { TwilioContext } from "../../../types/TwilioContext";
|
import { TwilioContext } from "../../../types/TwilioContext";
|
||||||
import { UserAgentHeader } from "../../../utils/blockUserAgent";
|
import { shouldBlockRequest, UserAgentHeader } from "../../../utils/blockUserAgent";
|
||||||
import { createDynaBridgeClient } from "../../../utils/ddb";
|
import { createDynaBridgeClient } from "../../../utils/ddb";
|
||||||
import { sendMessageToUser } from "../../../utils/discord";
|
import { sendMessageToUser } from "../../../utils/discord";
|
||||||
import { createEditDoorConfig, EditDoorConfigReqSchema, editDoorToDoorConfig, getDoorConfigID, getEditDoorConfigID } from "../../../schema/DoorConfig";
|
import { createEditDoorConfig, EditDoorConfigReq, editDoorToDoorConfig, getDoorConfigID, getEditDoorConfigID } from "../../../schema/DoorConfig";
|
||||||
import { z } from "zod";
|
|
||||||
import { withMetrics } from "../../../common/DoormanHandler";
|
|
||||||
import { setResponseJson } from "../../../utils/responseUtils";
|
|
||||||
|
|
||||||
import { zu } from "zod_utilz";
|
export interface EditRequest extends ServerlessEventObject<{}, UserAgentHeader> {
|
||||||
|
door?: string;
|
||||||
|
approvalId?: string;
|
||||||
|
newConfig?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> = async function(context, event, callback) {
|
||||||
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();
|
const response = new Twilio.Response();
|
||||||
|
|
||||||
const req = EditRequestSchema.parse(event);
|
if (shouldBlockRequest(event)) {
|
||||||
|
response.setStatusCode(200);
|
||||||
|
return callback(null, response);
|
||||||
|
}
|
||||||
|
|
||||||
let door = req.door;
|
let door = event.door;
|
||||||
let approvalId = req.approvalId;
|
let approvalId = event.approvalId;
|
||||||
let newConfigRaw = req.newConfig;
|
let newConfigString = event.newConfig;
|
||||||
|
|
||||||
const db = createDynaBridgeClient(context);
|
const db = createDynaBridgeClient(context);
|
||||||
|
|
||||||
// approve path
|
// approve path
|
||||||
if (approvalId) {
|
if (door && approvalId) {
|
||||||
const newConfig = await db.entities.editDoorConfig.findById(getEditDoorConfigID(door));
|
const newConfig = await db.entities.editDoorConfig.findById(getEditDoorConfigID(door));
|
||||||
|
|
||||||
if (!newConfig || newConfig.approvalId !== approvalId) {
|
if (!newConfig || newConfig.approvalId !== approvalId) {
|
||||||
@ -48,7 +39,7 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequestTwil
|
|||||||
return callback(null, response);
|
return callback(null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.entities.doorConfig.save(editDoorToDoorConfig(newConfig));
|
db.entities.doorConfig.save(editDoorToDoorConfig(newConfig));
|
||||||
|
|
||||||
// send update to discord users
|
// send update to discord users
|
||||||
const updateMessage = `Configuration change \`${approvalId}\` was approved @ Door "${door}"`;
|
const updateMessage = `Configuration change \`${approvalId}\` was approved @ Door "${door}"`;
|
||||||
@ -69,21 +60,16 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequestTwil
|
|||||||
return callback(null, response);
|
return callback(null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newConfigRaw) {
|
if (!door || !newConfigString) {
|
||||||
setResponseJson(response, 400, {
|
response.setStatusCode(400);
|
||||||
err: "Missing new config",
|
|
||||||
});
|
|
||||||
return callback(null, response);
|
return callback(null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
const newConfig = EditDoorConfigReqSchema.parse(newConfigRaw);
|
const newConfig: EditDoorConfigReq = JSON.parse(newConfigString);
|
||||||
|
|
||||||
const config = await db.entities.doorConfig.findById(getDoorConfigID(door));
|
const config = await db.entities.doorConfig.findById(getDoorConfigID(door));
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
setResponseJson(response, 404, {
|
response.setStatusCode(404);
|
||||||
err: `Door not found ${door}`,
|
|
||||||
});
|
|
||||||
return callback(null, response);
|
return callback(null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,24 +78,14 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequestTwil
|
|||||||
newConfig.pin = config.pin;
|
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);
|
const editDoorConfig = createEditDoorConfig(door, newConfig);
|
||||||
await db.entities.editDoorConfig.save(editDoorConfig);
|
await db.entities.editDoorConfig.save(editDoorConfig);
|
||||||
|
|
||||||
const params: EditRequest = {
|
// newConfig.discordUsers = undefined;
|
||||||
door,
|
// newConfig.fallbackNumbers = undefined;
|
||||||
approvalId: editDoorConfig.approvalId,
|
// newConfig.status = undefined;
|
||||||
};
|
|
||||||
|
|
||||||
const approvalUrl = `https://doorman.chromart.cc/api/door/edit?` + (new URLSearchParams(params as any)).toString();
|
const approvalUrl = `https://doorman.chromart.cc/api/door/edit?door=${door}&approvalId=${editDoorConfig.approvalId as string}`;
|
||||||
console.log(approvalUrl);
|
console.log(approvalUrl);
|
||||||
|
|
||||||
// send update to discord users
|
// send update to discord users
|
||||||
@ -125,13 +101,14 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequestTwil
|
|||||||
|
|
||||||
await Promise.all(discordPromises);
|
await Promise.all(discordPromises);
|
||||||
|
|
||||||
setResponseJson(response, 200, {
|
response
|
||||||
msg: 'Created configuration change',
|
.setStatusCode(200)
|
||||||
});
|
.appendHeader('Content-Type', 'application/json')
|
||||||
|
.setBody({ msg: "Created Configuration change" });
|
||||||
|
|
||||||
// destroy the internal client after
|
// destroy the internal client after
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
db.ddbClient.destroy();
|
db.ddbClient.destroy();
|
||||||
|
|
||||||
return callback(null, response);
|
return callback(null, response);
|
||||||
});
|
};
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const imports = functionFiles.forEach(file => require('./' + path.relative('src'
|
|||||||
|
|
||||||
console.log("functions to build:", functionFiles);
|
console.log("functions to build:", functionFiles);
|
||||||
|
|
||||||
const bundledModules = ['dynabridge', 'zod_utilz'];
|
const bundledModules = ['dynabridge'];
|
||||||
|
|
||||||
const externalModules = Object.keys(require('../package.json').dependencies)
|
const externalModules = Object.keys(require('../package.json').dependencies)
|
||||||
.filter(dep => !bundledModules.includes(dep));
|
.filter(dep => !bundledModules.includes(dep));
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export const DoorConfigSchema = z.object({
|
|||||||
buzzerCode: z.string(),
|
buzzerCode: z.string(),
|
||||||
discordUsers: z.array(z.string()),
|
discordUsers: z.array(z.string()),
|
||||||
fallbackNumbers: z.array(z.string()),
|
fallbackNumbers: z.array(z.string()),
|
||||||
pin: z.string().default(""),
|
pin: z.string(),
|
||||||
pressKey: z.string(),
|
pressKey: z.string(),
|
||||||
greeting: z.string().optional(),
|
greeting: z.string().optional(),
|
||||||
timeout: z.number(),
|
timeout: z.number(),
|
||||||
@ -39,8 +39,7 @@ export const getEditDoorConfigID = (doorName: string): string[] => {
|
|||||||
export type DoorConfig = z.infer<typeof DoorConfigSchema>;
|
export type DoorConfig = z.infer<typeof DoorConfigSchema>;
|
||||||
export type EditDoorConfig = z.infer<typeof EditDoorConfigSchema>;
|
export type EditDoorConfig = z.infer<typeof EditDoorConfigSchema>;
|
||||||
|
|
||||||
export const EditDoorConfigReqSchema = EditDoorConfigSchema.omit({ "PK": true, "SK": true, "approvalId": true });
|
export type EditDoorConfigReq = Omit<EditDoorConfig, "PK" | "SK" | "approvalId">;
|
||||||
export type EditDoorConfigReq = z.infer<typeof EditDoorConfigReqSchema>;
|
|
||||||
|
|
||||||
export const DoorConfigEntity: DynaBridgeEntity<DoorConfig> = {
|
export const DoorConfigEntity: DynaBridgeEntity<DoorConfig> = {
|
||||||
tableName: "doorman",
|
tableName: "doorman",
|
||||||
|
|||||||
@ -1,17 +0,0 @@
|
|||||||
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,9 +1,6 @@
|
|||||||
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",
|
||||||
@ -21,7 +18,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(() => {
|
||||||
@ -41,13 +38,7 @@ export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCh
|
|||||||
return "null";
|
return "null";
|
||||||
});
|
});
|
||||||
|
|
||||||
const body: AuthRequest = {
|
fetch(`/api/door/auth?key=${key}&rotatingKey=${key}&door=${door}&ip=${ip}`)
|
||||||
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");
|
||||||
@ -55,7 +46,7 @@ export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCh
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onUnlock();
|
onUnlock();
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
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