diff --git a/bun.lockb b/bun.lockb
index 2d74fb8..267862f 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/packages/doorman-api/functions/api/door/edit.js b/packages/doorman-api/functions/api/door/edit.js
new file mode 100644
index 0000000..2ed179b
--- /dev/null
+++ b/packages/doorman-api/functions/api/door/edit.js
@@ -0,0 +1,103 @@
+
+/**
+ * Edit API for doors
+ */
+
+exports.handler = async function(context, event, callback) {
+ const response = new Twilio.Response();
+
+ let door = event.door;
+ let approvalId = event.approvalId;
+ let newConfig = event.newConfig;
+
+ const ddbPath = Runtime.getFunctions()['common/ddb'].path;
+ const discordPath = Runtime.getFunctions()['common/discord'].path;
+ const ddb = require(ddbPath);
+ const discord = require(discordPath);
+ const client = ddb.createDDBClient(context);
+
+ // approve path
+ if (door && approvalId) {
+ const newConfig = await client.send(ddb.getDoorConfigUpdateCommand(door));
+
+ if (!newConfig || newConfig.Item.approvalId.S !== approvalId) {
+ response.setStatusCode(400);
+ return callback(null, response);
+ }
+
+ await client.send(ddb.replaceDoorConfigWithUpdateItem(newConfig));
+
+ const hostUrl = event.request.headers.host;
+ const scheme = hostUrl.startsWith("localhost") ? "http://": "https://";
+
+ // send update to discord users
+ const updateMessage = `Configuration change \`${approvalId}\` was approved @ Door "${door}"`;
+
+ const discordPromises = newConfig.Item.discordUsers.SS.map((user) => {
+ return discord.sendMessageToUser(
+ context,
+ user,
+ updateMessage,
+ ).catch(e => console.error(e))
+ });
+
+ await Promise.all(discordPromises);
+
+ response.setStatusCode(200);
+ return callback(null, response);
+ }
+
+ if (!door || !newConfig) {
+ response.setStatusCode(400);
+ return callback(null, response);
+ }
+
+ newConfig = JSON.parse(event.newConfig);
+
+ const config = await client.send(ddb.getDoorConfigCommand(door));
+
+ if (!config.Item) {
+ response.setStatusCode(404);
+ return callback(null, response);
+ }
+
+ // set to old PIN if it is missing
+ if (newConfig.pin === "") {
+ newConfig.pin = config.Item.pin.S;
+ }
+
+ const input = ddb.putDoorUpdateConfigCommand(door, newConfig);
+
+ const update = await client.send(input);
+
+ newConfig.discordUser = undefined;
+ newConfig.fallbackNumber = undefined;
+ newConfig.status = undefined;
+
+ const hostUrl = event.request.headers.host;
+ const scheme = hostUrl.startsWith("localhost") ? "http://": "https://";
+
+ const approvalUrl = `${scheme + hostUrl}/api/door/edit?door=${door}&approvalId=${input.input.Item.approvalId.S}`;
+ console.log(approvalUrl);
+
+ // send update to discord users
+ const approvalMessage = `Configuration change requested @ Door "${door}" [click to approve it](${approvalUrl})\`\`\`${JSON.stringify(newConfig, null, 4)}\`\`\``;
+
+ const discordPromises = config.Item.discordUsers.SS.map((user) => {
+ return discord.sendMessageToUser(
+ context,
+ user,
+ approvalMessage,
+ ).catch(e => console.error(e))
+ });
+
+ await Promise.all(discordPromises);
+
+ response
+ .setStatusCode(200)
+ .appendHeader('Content-Type', 'application/json')
+ .setBody({ msg: update });
+
+ await client.destroy();
+ return callback(null, response);
+};
diff --git a/packages/doorman-api/functions/api/door/info.js b/packages/doorman-api/functions/api/door/info.js
index 694d92c..3580dd1 100644
--- a/packages/doorman-api/functions/api/door/info.js
+++ b/packages/doorman-api/functions/api/door/info.js
@@ -41,6 +41,7 @@ exports.handler = async function(context, event, callback) {
.setBody({ err: "This buzzer is not registered properly" });
} else {
if (buzzer) {
+ // respond to twilio CLIENT
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
@@ -56,10 +57,20 @@ exports.handler = async function(context, event, callback) {
.then(async (lock) => {
const status = ddb.isLockOpen(lock) ? "OPEN": "CLOSED";
+ // respond to UI
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
- .setBody({ id: door, timeout: config.Item.timeout.N, status, buzzerCode: config.Item.buzzerCode.S });
+ .setBody({
+ id: door,
+ timeout: config.Item.timeout.N,
+ buzzer: config.Item.buzzer.S,
+ status,
+ buzzerCode: config.Item.buzzerCode.S,
+ fallbackNumbers: config.Item.fallbackNumbers.SS,
+ pressKey: config.Item.pressKey.S,
+ discordUsers: config.Item?.discordUsers?.SS || [],
+ });
}).catch((e) => {
console.log(e);
diff --git a/packages/doorman-api/functions/common/ddb.private.js b/packages/doorman-api/functions/common/ddb.private.js
index 62bf239..632fa5d 100644
--- a/packages/doorman-api/functions/common/ddb.private.js
+++ b/packages/doorman-api/functions/common/ddb.private.js
@@ -1,4 +1,5 @@
-const { DynamoDBClient, GetItemCommand, DeleteItemCommand, PutItemCommand } = require("@aws-sdk/client-dynamodb");
+const { randomUUID } = require("crypto");
+const { DynamoDBClient, GetItemCommand, DeleteItemCommand, PutItemCommand, UpdateItemCommand } = require("@aws-sdk/client-dynamodb");
exports.createDDBClient = (context) => {
return new DynamoDBClient({
@@ -91,3 +92,86 @@ exports.setLockStatusCommand = (door, timeoutSeconds, fingerprintObj) => {
}
});
};
+
+exports.putDoorUpdateConfigCommand = (door, config) => {
+ 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(),
+ }
+ }
+ });
+};
+
+exports.getDoorConfigCommand = (door) => {
+ return new GetItemCommand({
+ TableName: "doorman",
+ Key: {
+ "PK": {
+ S: `door-${door}`,
+ },
+ "SK": {
+ S: "config-update",
+ },
+ },
+ });
+};
+
+exports.getDoorConfigUpdateCommand = (door) => {
+ return new GetItemCommand({
+ TableName: "doorman",
+ Key: {
+ "PK": {
+ S: `door-${door}`,
+ },
+ "SK": {
+ S: "config-update",
+ },
+ },
+ });
+};
+
+exports.replaceDoorConfigWithUpdateItem = (newConfigItem) => {
+ const newItem = {
+ ...newConfigItem.Item,
+ SK: { S: "config" },
+ };
+
+ delete newItem.approvalId;
+
+ return new PutItemCommand({
+ TableName: "doorman",
+ Item: newItem,
+ });
+};
diff --git a/packages/doorman-ui/src/components/InputTokenGroup.tsx b/packages/doorman-ui/src/components/InputTokenGroup.tsx
new file mode 100644
index 0000000..5440005
--- /dev/null
+++ b/packages/doorman-ui/src/components/InputTokenGroup.tsx
@@ -0,0 +1,27 @@
+import { Button, ColumnLayout, Input, SpaceBetween, TokenGroup, TokenGroupProps } from "@cloudscape-design/components";
+
+export type InputTokenGroupProps = {
+ type?: React.HTMLInputTypeAttribute;
+ registerInput: any,
+ tokenGroupProps: TokenGroupProps,
+ onAdd: () => void,
+ onDismiss: (i: number) => void,
+};
+
+export const InputTokenGroup = (props: InputTokenGroupProps) => {
+ return (
+
+
+
+
+
+ props.onDismiss(e.detail.itemIndex)}
+ {...props.tokenGroupProps}
+ />
+
+ );
+};
diff --git a/packages/doorman-ui/src/index.tsx b/packages/doorman-ui/src/index.tsx
index 69df590..4695693 100644
--- a/packages/doorman-ui/src/index.tsx
+++ b/packages/doorman-ui/src/index.tsx
@@ -4,7 +4,8 @@ import App from './App';
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { DoorPage, loader as doorpageloader } from './pages/DoorPage';
import { ErrorPage } from './pages/ErrorPage';
-
+import { QueryRouter } from './routers/QueryRouter';
+import { EditPage } from './pages/EditPage';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
@@ -19,7 +20,13 @@ const router = createBrowserRouter([
{
path: "",
loader: doorpageloader,
- element:
+ element:
+ ,
+ door:
+ }}
+ />
}
]
}
@@ -30,4 +37,3 @@ root.render(
);
-
diff --git a/packages/doorman-ui/src/pages/DoorPage.tsx b/packages/doorman-ui/src/pages/DoorPage.tsx
index 18af4b1..13d69a7 100644
--- a/packages/doorman-ui/src/pages/DoorPage.tsx
+++ b/packages/doorman-ui/src/pages/DoorPage.tsx
@@ -1,6 +1,6 @@
-import React, { ReactElement, ReactNode, useEffect, useState } from "react";
+import { ReactNode, useEffect, useState } from "react";
import { useLoaderData, useNavigate, useSearchParams } from "react-router-dom";
-import { AppLayout, BreadcrumbGroup, Container, Flashbar, FlashbarProps, Header, SideNavigation, SpaceBetween, TextContent, Wizard } from "@cloudscape-design/components";
+import { AppLayout, BreadcrumbGroup, Container, Flashbar, FlashbarProps, Header, SpaceBetween, TextContent, Wizard } from "@cloudscape-design/components";
import { AuthComponent, IAuthMode } from "../components/AuthComponent";
import OtpInput from 'react-otp-input';
import { CountdownBar } from "../components/CountdownBar";
@@ -9,27 +9,21 @@ import { DoorResponse } from "../types/DoorResponse";
export async function loader({ params, request }: any) {
const door = new URL(request.url).searchParams.get('door');
+ if (!door) {
+ return {};
+ }
+
const response = await fetch(`/api/door/info?door=${door}`).then(res => res.json());
console.log(response);
- if (response.msg) {
+ if (response.err) {
throw new Error("Not a valid door");
}
return response as DoorResponse;
}
-interface SelectOption {
- label?: string;
- value?: string;
-}
-
-const selectOptions: SelectOption[] = [
- { label: "backup", value: "backup" },
- { label: "key", value: "key" },
-];
-
export function DoorPage() {
const doorResponse = useLoaderData() as DoorResponse;
const navigate = useNavigate();
@@ -106,7 +100,7 @@ export function DoorPage() {
@@ -272,4 +266,4 @@ export function DoorPage() {
}
/>
);
-}
+};
diff --git a/packages/doorman-ui/src/pages/EditPage.tsx b/packages/doorman-ui/src/pages/EditPage.tsx
new file mode 100644
index 0000000..00b7592
--- /dev/null
+++ b/packages/doorman-ui/src/pages/EditPage.tsx
@@ -0,0 +1,145 @@
+import { AppLayout, BreadcrumbGroup, Button, Container, Form, FormField, Header, Link, SpaceBetween } from "@cloudscape-design/components";
+import { useLoaderData, useSearchParams } from "react-router-dom";
+import { DoorResponse } from "../types/DoorResponse";
+import { useForm } from "react-hook-form";
+import { InputTokenGroup } from "../components/InputTokenGroup";
+
+export type DoorEditForm = DoorResponse & { pin: string, fallbackNumber: string, discordUser: string, greeting: string };
+
+export const EditPage = () => {
+ const doorResponse = useLoaderData() as DoorResponse;
+ const door = doorResponse.id;
+ const { register, setValue, setError, getValues, watch, formState, clearErrors } = useForm({
+ defaultValues: doorResponse,
+ mode: "all",
+ });
+
+ const fallbackNumbers = watch("fallbackNumbers");
+ const discordUsers = watch("discordUsers");
+
+ const fallbackNumbersError = formState.errors.fallbackNumbers?.message;
+ const discordUsersError = formState.errors.discordUsers?.message;
+
+ return (
+
+ }
+ content={
+
+ }
+ />
+ );
+};
diff --git a/packages/doorman-ui/src/routers/QueryRouter.tsx b/packages/doorman-ui/src/routers/QueryRouter.tsx
new file mode 100644
index 0000000..3c5198f
--- /dev/null
+++ b/packages/doorman-ui/src/routers/QueryRouter.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { ReactNode } from "react";
+import { useLocation, useSearchParams } from "react-router-dom";
+
+export interface IQueryRouterProps {
+ mapping: Record;
+};
+
+export const QueryRouter = ({ mapping }: IQueryRouterProps) => {
+ const [params] = useSearchParams();
+ let element = null;
+
+ for (const key of params.keys()) {
+ if (mapping[key]) {
+ element = mapping[key];
+ break;
+ }
+ }
+ if (element === null) {
+ throw new Error("missing mapping");
+ }
+
+ return element;
+};
diff --git a/packages/doorman-ui/src/types/Action.ts b/packages/doorman-ui/src/types/Action.ts
deleted file mode 100644
index 8c29eaf..0000000
--- a/packages/doorman-ui/src/types/Action.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export enum Action {
- DOWNLOAD = "download",
- UPLOAD = "upload",
- DELETE = "delete",
-}
\ No newline at end of file
diff --git a/packages/doorman-ui/src/types/DoorResponse.ts b/packages/doorman-ui/src/types/DoorResponse.ts
index 7e58947..271d934 100644
--- a/packages/doorman-ui/src/types/DoorResponse.ts
+++ b/packages/doorman-ui/src/types/DoorResponse.ts
@@ -2,4 +2,8 @@ export interface DoorResponse {
id: string,
timeout: number;
buzzerCode: string;
+ fallbackNumbers: string[];
+ pressKey: string;
+ discordUsers: string[];
+ buzzer: string;
};