Compare commits

..

No commits in common. "2f45005a8a37eefad643b7f6fd2eaf92c14125a3" and "73e98c0508b26ee6c0ebc9432d6b88351a1ff884" have entirely different histories.

13 changed files with 337 additions and 203 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -5,14 +5,14 @@
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { UserAgentHeader } from "../../../utils/blockUserAgent";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { clearLockStatusCommand, createDDBClient, ddbItemToJSON, getDoorConfigCommand, getLockStatusCommand, isLockOpen, setLockStatusCommand } from "../../../utils/ddb";
import { DoorConfig } from "../../../types/DoorConfig";
import { AuthMethod } from "../../../types/AuthMethod";
import { Lock } from "../../../types/Lock";
import { DoorStatus } from "../../../types/DoorStatus";
import { getMetricFromRegistry, withMetrics } from "../../../common/DoormanHandler";
import { AuthMetrics, registerMetrics } from "../../../metrics/AuthMetrics";
import { Counter } from "prom-client";
import { DoorConfig, getDoorConfigID } from "../../../schema/DoorConfig";
import { createLockStatusWithTimeout, getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
export interface AuthRequest extends ServerlessEventObject<{}, UserAgentHeader> {
door?: string;
@ -34,9 +34,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
return callback(null, response);
}
const db = createDynaBridgeClient(context);
const client = createDDBClient(context);
const config: DoorConfig | undefined = await db.entities.doorConfig.findById(getDoorConfigID(door));
const ddbConfig = await client.send(getDoorConfigCommand(door));
const config: DoorConfig = ddbItemToJSON<DoorConfig>(ddbConfig);
if (!config) {
getMetricFromRegistry<Counter>(metricsRegistry, AuthMetrics.DOOR_CONFIG_NOT_FOUND).inc({ door }, 1);
@ -75,32 +76,41 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
const timeout = event.timeout ? parseInt(event.timeout) : config.timeout;
// check lock status if locked, then unlock. If unlocked then lock
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
const isOpen = isLockOpen(lock);
await client.send(getLockStatusCommand(door))
.then(async (lockDdb) => {
const isOpen = isLockOpen(lockDdb);
if (isOpen && lock) {
const fingerprint = JSON.parse(lock.fingerprint);
if (isOpen) {
const lock: Lock = ddbItemToJSON<Lock>(lockDdb);
const fingerprint = JSON.parse(lock.fingerprint);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
fingerprint,
});
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
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` });
}
await client.send(clearLockStatusCommand(lockDdb));
return;
}
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
await client.send(setLockStatusCommand(door, timeout, fingerprint))
.then(async (item) => {
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: `Opened the door "${door}" for ${timeout}s` });
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
});
});
await client.destroy();
return callback(null, response);
});

View File

@ -6,9 +6,9 @@
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { shouldBlockRequest, UserAgentHeader } from "../../../utils/blockUserAgent";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { createDDBClient, ddbItemToJSON, getDoorConfigCommand, getDoorConfigUpdateCommand, putDoorUpdateConfigCommand, replaceDoorConfigWithUpdateItem } from "../../../utils/ddb";
import { DoorConfig, EditDoorConfig } from "../../../types/DoorConfig";
import { sendMessageToUser } from "../../../utils/discord";
import { createEditDoorConfig, EditDoorConfigReq, editDoorToDoorConfig, getDoorConfigID, getEditDoorConfigID } from "../../../schema/DoorConfig";
export interface EditRequest extends ServerlessEventObject<{}, UserAgentHeader> {
door?: string;
@ -28,18 +28,19 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> =
let approvalId = event.approvalId;
let newConfigString = event.newConfig;
const db = createDynaBridgeClient(context);
const client = createDDBClient(context);
// approve path
if (door && approvalId) {
const newConfig = await db.entities.editDoorConfig.findById(getEditDoorConfigID(door));
const newConfigDdb = await client.send(getDoorConfigUpdateCommand(door));
const newConfig = ddbItemToJSON<EditDoorConfig>(newConfigDdb);
if (!newConfig || newConfig.approvalId !== approvalId) {
response.setStatusCode(400);
return callback(null, response);
}
db.entities.doorConfig.save(editDoorToDoorConfig(newConfig));
await client.send(replaceDoorConfigWithUpdateItem(newConfigDdb as any));
// send update to discord users
const updateMessage = `Configuration change \`${approvalId}\` was approved @ Door "${door}"`;
@ -63,8 +64,10 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> =
return callback(null, response);
}
const newConfig: EditDoorConfigReq = JSON.parse(newConfigString);
const config = await db.entities.doorConfig.findById(getDoorConfigID(door));
const newConfig: EditDoorConfig = JSON.parse(newConfigString);
const configDdb = await client.send(getDoorConfigCommand(door));
const config = ddbItemToJSON<DoorConfig>(configDdb);
if (!config) {
response.setStatusCode(404);
@ -76,14 +79,15 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> =
newConfig.pin = config.pin;
}
const editDoorConfig = createEditDoorConfig(door, newConfig);
await db.entities.editDoorConfig.save(editDoorConfig);
const input = putDoorUpdateConfigCommand(door, newConfig);
const update = await client.send(input);
// newConfig.discordUsers = undefined;
// newConfig.fallbackNumbers = undefined;
// newConfig.status = undefined;
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?door=${door}&approvalId=${input?.input?.Item?.approvalId.S as string}`;
console.log(approvalUrl);
// send update to discord users
@ -102,11 +106,8 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> =
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: "Created Configuration change" });
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
.setBody({ msg: update });
await client.destroy();
return callback(null, response);
};

View File

@ -4,11 +4,10 @@
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { createDDBClient, createDynaBridgeClient, ddbItemToJSON, getDoorAliasCommand, getDoorConfigCommand, getLockStatusCommand, isLockOpen } from "../../../utils/ddb";
import { DoorStatus } from "../../../types/DoorStatus";
import { getDoorConfigID } from "../../../schema/DoorConfig";
import { getDoorAliasID } from "../../../schema/DoorAlias";
import { getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
import { DoorConfig } from "../../../types/DoorConfig";
import { DOOR_CONFIG_SK, getDoorConfigID } from "../../../schema/DoorConfig";
export interface InfoRequest extends ServerlessEventObject {
door?: string;
@ -39,19 +38,20 @@ export const handler: ServerlessFunctionSignature<TwilioContext, InfoRequest> =
return callback(null, response);
}
const client = createDDBClient(context);
const db = createDynaBridgeClient(context);
if (buzzer) {
door = await db.entities.doorAlias.findById(getDoorAliasID(buzzer))
door = await client.send(getDoorAliasCommand(buzzer))
.then(async (alias) => {
if (!alias) {
if (!alias.Item) {
response
.setStatusCode(404)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: "This buzzer is not registered" });
return undefined;
}
return alias.name;
return alias.Item.name.S;
});
}
@ -74,34 +74,39 @@ export const handler: ServerlessFunctionSignature<TwilioContext, InfoRequest> =
door,
fallbackNumbers: config.fallbackNumbers,
pressKey: config.pressKey,
discordUsers: config.discordUsers,
discordUsers: config.discordUsers || [],
});
} else {
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
const status = isLockOpen(lock) ? DoorStatus.OPEN: DoorStatus.CLOSED;
await client.send(getLockStatusCommand(door))
.then(async (lock) => {
const status = isLockOpen(lock) ? DoorStatus.OPEN: DoorStatus.CLOSED;
// 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 || "",
// 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 || "",
});
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
});
}
}
}
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
await client.destroy();
return callback(null, response);
};

View File

@ -5,9 +5,8 @@
import { ServerlessFunctionSignature, ServerlessEventObject } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { shouldBlockRequest } from "../../../utils/blockUserAgent";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { clearLockStatusCommand, createDDBClient, getLockStatusCommand, isLockOpen } from "../../../utils/ddb";
import { DoorStatus } from "../../../types/DoorStatus";
import { getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
export interface StatusRequest extends ServerlessEventObject {
door: string;
@ -33,34 +32,42 @@ export const handler: ServerlessFunctionSignature<TwilioContext, StatusRequest>
return callback(null, response);
}
const db = createDynaBridgeClient(context);
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
const client = createDDBClient(context);
const isOpen = isLockOpen(lock);
await client.send(getLockStatusCommand(door))
.then(async (lock) => {
const isOpen = isLockOpen(lock);
if (isOpen && lock) {
const fingerprint = JSON.parse(lock.fingerprint);
if (isOpen) {
const fingerprint = JSON.parse(lock?.Item?.fingerprint.S as string);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.OPEN,
fingerprint,
});
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.OPEN,
fingerprint,
});
await db.entities.lockStatus.deleteById(getLockStatusID(door));
} else {
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
await client.send(clearLockStatusCommand(lock));
return;
}
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
});
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
});
}
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
await client.destroy();
return callback(null, response);
};

View File

@ -1,22 +0,0 @@
import { z } from "zod";
import { DynaBridgeEntity } from 'dynabridge';
export const DOOR_ALIAS_SK = "alias";
export const DoorAliasSchema = z.object({
// keys
PK: z.string(), // phone number (buzzer number)
SK: z.literal(DOOR_ALIAS_SK).default(DOOR_ALIAS_SK),
name: z.string(),
});
export const getDoorAliasID = (buzzer: string): string[] => {
return [buzzer, DOOR_ALIAS_SK];
}
export type DoorAlias = z.infer<typeof DoorAliasSchema>;
export const DoorAliasEntity: DynaBridgeEntity<DoorAlias> = {
tableName: "doorman",
id: ["PK", "SK"],
};

View File

@ -1,6 +1,5 @@
import { z } from "zod";
import { DynaBridgeEntity } from 'dynabridge';
import { randomUUID } from "crypto";
import { DynaBridge, DynaBridgeEntity } from 'dynabridge';
export const DOOR_CONFIG_SK = "config";
export const EDIT_DOOR_CONFIG_SK = "config-update";
@ -16,7 +15,7 @@ export const DoorConfigSchema = z.object({
fallbackNumbers: z.array(z.string()),
pin: z.string(),
pressKey: z.string(),
greeting: z.string().optional(),
greeting: z.string(),
timeout: z.number(),
});
@ -37,8 +36,6 @@ 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 DoorConfigEntity: DynaBridgeEntity<DoorConfig> = {
tableName: "doorman",
id: ["PK", "SK"],
@ -48,23 +45,3 @@ export const EditDoorConfigEntity: DynaBridgeEntity<EditDoorConfig> = {
tableName: "doorman",
id: ["PK", "SK"],
};
export const editDoorToDoorConfig = (updateDoor: EditDoorConfig): DoorConfig => {
const newItem: any = {
...updateDoor,
SK: DOOR_CONFIG_SK,
};
delete newItem.approvalId;
return newItem;
}
export const createEditDoorConfig = (door: string, newConfig: EditDoorConfigReq): EditDoorConfig => {
return {
...newConfig,
PK: "door-" + door,
SK: EDIT_DOOR_CONFIG_SK,
approvalId: randomUUID().toString(),
};
}

View File

@ -1,40 +0,0 @@
import { z } from "zod";
import { DynaBridgeEntity } from 'dynabridge';
export const LOCK_STATUS_SK = "lock";
export const LockStatusSchema = z.object({
// keys
PK: z.string(), // door name
SK: z.literal(LOCK_STATUS_SK).default(LOCK_STATUS_SK),
TTL: z.number(),
fingerprint: z.string(),
});
export const getLockStatusID = (door: string): string[] => {
return [door, LOCK_STATUS_SK];
}
export type LockStatus = z.infer<typeof LockStatusSchema>;
export const LockStatusEntity: DynaBridgeEntity<LockStatus> = {
tableName: "doorman",
id: ["PK", "SK"],
};
export const isLockOpen = (lock?: LockStatus) => {
// ttl is a UTC ms time for how long it is unlocked
const ttl = lock?.TTL || 0;
return parseInt("" + ttl) > Date.now();
};
export const createLockStatusWithTimeout = (door: string, timeoutSeconds: number, fingerprint: any): LockStatus => {
return {
PK: door,
SK: LOCK_STATUS_SK,
TTL: Date.now() + timeoutSeconds * 1000,
fingerprint: JSON.stringify(fingerprint),
};
};

View File

@ -0,0 +1,14 @@
export interface DoorConfig {
buzzer: string;
buzzerCode: string;
discordUsers: string[];
fallbackNumbers: string[];
pin: string;
pressKey: string;
timeout: number;
greeting: string;
}
export interface EditDoorConfig extends DoorConfig {
approvalId: string;
}

View File

@ -0,0 +1,3 @@
export interface Lock {
fingerprint: any;
}

View File

@ -1,16 +1,25 @@
import { randomUUID } from "crypto";
import { DynamoDBClient, GetItemCommand, DeleteItemCommand, PutItemCommand, UpdateItemCommand, GetItemOutput } from "@aws-sdk/client-dynamodb";
import { TwilioContext } from "../types/TwilioContext";
import { DoorConfig } from "../types/DoorConfig";
import { DynaBridge } from "dynabridge";
import { DoorConfigEntity, EditDoorConfigEntity } from "../schema/DoorConfig";
import { DoorAliasEntity } from "../schema/DoorAlias";
import { LockStatusEntity } from "../schema/LockStatus";
export const createDDBClient = (context: TwilioContext) => {
return new DynamoDBClient({
region: "us-east-1" ,
credentials: {
accessKeyId: context.AWS_ACCESS_KEY,
secretAccessKey: context.AWS_SECRET_ACCESS_KEY,
},
});
};
export const createDynaBridgeClient = (context: TwilioContext) => {
// register all entities here
return new DynaBridge({
doorConfig: DoorConfigEntity,
editDoorConfig: EditDoorConfigEntity,
doorAlias: DoorAliasEntity,
lockStatus: LockStatusEntity,
}, {
serialize: (entity) => entity,
deserialize: (entity) => {
@ -33,3 +42,170 @@ export const createDynaBridgeClient = (context: TwilioContext) => {
},
});
};
export const getLockStatusCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: door,
},
"SK": {
S: "lock",
}
},
});
};
export const getDoorAliasCommand = (buzzerNumber: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: buzzerNumber,
},
"SK": {
S: "alias",
}
},
});
};
export const getDoorConfigCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: `door-${door}`,
},
"SK": {
S: "config",
},
},
});
};
export const isLockOpen = (lock: GetItemOutput) => {
// ttl is a UTC ms time for how long it is unlocked
const ttl = lock.Item?.TTL?.N || 0;
return parseInt("" + ttl) > Date.now();
};
export const clearLockStatusCommand = (lock: GetItemOutput) => {
return new DeleteItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: lock?.Item?.PK.S as string,
},
"SK": {
S: "lock",
}
}
});
};
export const setLockStatusCommand = (door: string, timeoutSeconds: number, fingerprintObj: any) => {
return new PutItemCommand({
TableName: "doorman",
Item: {
"PK": {
S: door,
},
"SK": {
S: "lock",
},
"TTL": {
N: `${Date.now() + timeoutSeconds * 1000}`,
},
"fingerprint": {
S: JSON.stringify(fingerprintObj),
}
}
});
};
export const putDoorUpdateConfigCommand = (door: string, config: DoorConfig) => {
return new PutItemCommand({
TableName: "doorman",
Item: {
"PK": {
S: "door-" + door,
},
"SK": {
S: "config-update",
},
"buzzer": {
S: config.buzzer,
},
"buzzerCode": {
S: config.buzzerCode,
},
"discordUsers": {
SS: config.discordUsers,
},
"fallbackNumbers": {
SS: config.fallbackNumbers,
},
"pin": {
S: config.pin,
},
"pressKey": {
S: config.pressKey,
},
"timeout": {
N: `${config.timeout}`,
},
"greeting": {
S: config.greeting,
},
"approvalId": {
S: randomUUID().toString(),
}
}
});
};
export function ddbItemToJSON<T extends Record<string, any>>(ddbItem: GetItemOutput): T {
let obj: any = {};
if (!ddbItem.Item) {
return obj;
}
const Item = ddbItem.Item;
Object.keys(Item).forEach(key => {
obj[key] = Object.values(Item[key])[0];
});
return obj;
};
export const getDoorConfigUpdateCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: `door-${door}`,
},
"SK": {
S: "config-update",
},
},
});
};
export const replaceDoorConfigWithUpdateItem = (newConfigItem: GetItemOutput & { Item: { approvalId?: { S: string } }}) => {
const newItem = {
...newConfigItem.Item,
SK: { S: "config" },
};
delete newItem.approvalId;
return new PutItemCommand({
TableName: "doorman",
Item: newItem,
});
};

View File

@ -15,7 +15,7 @@
"@twilio/runtime-handler": "1.3.0",
"node-fetch": "^2.7.0",
"prom-client": "^15.1.3",
"prometheus-remote-write": "^0.5.1",
"prometheus-remote-write": "^0.5.0",
"promise.timeout": "^1.2.0",
"twilio": "^3.84.1",
"winston": "^3.17.0",
@ -23,7 +23,7 @@
},
"devDependencies": {
"@types/bun": "latest",
"concurrently": "^9.1.2",
"concurrently": "^9.1.0",
"twilio-run": "^3.5.4"
},
"engines": {

View File

@ -1,10 +1,8 @@
import { serve } from "bun";
import { Hono } from "hono";
import { prettyJSON } from "hono/pretty-json";
import { createDynaBridgeClient } from "../../doorman-api/src/utils/ddb";
import { createDDBClient, getDoorConfigCommand, getLockStatusCommand, isLockOpen } from "../../doorman-api/src/utils/ddb";
import { DoorStatus } from "../../doorman-api/src/types/DoorStatus";
import { getLockStatusID, isLockOpen } from "../../doorman-api/src/schema/LockStatus";
import { getDoorConfigID } from "../../doorman-api/src/schema/DoorConfig";
const app = new Hono();
@ -18,22 +16,27 @@ app.get("/api/door/info", async (c) => {
}, 400);
}
const db = createDynaBridgeClient(Bun.env as any);
const client = createDDBClient(Bun.env as any);
const config = await client.send(getDoorConfigCommand(door));
const config = await db.entities.doorConfig.findById(getDoorConfigID(door));
if (!config) {
if (!config.Item) {
return c.json({
err: "This buzzer is not registered properly",
}, 404);
}
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
const status = isLockOpen(lock) ? DoorStatus.OPEN: "CLOSED";
return c.json({
id: door,
status,
});
return await client.send(getLockStatusCommand(door))
.then(async (lock) => {
const status = isLockOpen(lock) ? DoorStatus.OPEN: "CLOSED";
return c.json({
id: door,
status,
});
}).catch((e) => {
console.log(e);
return c.json({
err: e,
}, 500);
});
});
serve({