diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7d8e6ab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotenv.enableAutocloaking": false +} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index ca15788..ea00172 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 44370a9..bcb95ad 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ }, "scripts": { "prepare-client-serverless": "bun --filter 'doorman-client' build && cp -fr packages/client/dist/* packages/serverless/assets/ && cp -f packages/serverless/assets/index.html packages/serverless/assets/assets/index.html", - "prepare-server-serverless": "bun --filter 'doorman-server' build && cp -fr packages/server/build/* packages/serverless/functions/ && mv packages/serverless/functions/server.js packages/serverless/functions/api.js", "deploy-serverless": "bun run prepare-client-serverless && bun --filter 'serverless' deploy" }, "peerDependencies": { "typescript": "^5.0.0" }, "dependencies": { + "@types/twilio": "^3.19.3", "crypto": "^1.0.1", "express-fingerprint": "^1.2.2", "hono": "^4.3.0", diff --git a/packages/client/src/components/AuthComponent.tsx b/packages/client/src/components/AuthComponent.tsx index d14e4bb..a0e3a2e 100644 --- a/packages/client/src/components/AuthComponent.tsx +++ b/packages/client/src/components/AuthComponent.tsx @@ -28,7 +28,7 @@ export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCh }, [runCheck]) const onSubmit = () => { - fetch(`/api/door/${door}/auth?key=${key}&rotatingKey=${key}`) + fetch(`/api/door/auth?key=${key}&rotatingKey=${key}&door=${door}`) .then(async res => { if (res.status !== 200) { setError("Incorrect PIN"); diff --git a/packages/client/src/pages/DoorPage.tsx b/packages/client/src/pages/DoorPage.tsx index ec439ee..67c50ca 100644 --- a/packages/client/src/pages/DoorPage.tsx +++ b/packages/client/src/pages/DoorPage.tsx @@ -7,7 +7,7 @@ import type { IDoorResponse } from "../../../server/src/types/IDoorResponse"; import { CountdownBar } from "../components/CountdownBar"; export async function loader({ params }: any) { - const response = await fetch(params.door ? `/api/door/${params.door}`: `/api/door`).then(res => res.json()); + const response = await fetch(params.door ? `/api/door/info?door=${params.door}`: `/api/door/info`).then(res => res.json()); console.log(response); @@ -74,7 +74,7 @@ export function DoorPage() { } const timer = setInterval(async () => { - const response = await fetch(`/api/door/${door}`).then(res => res.json()); + const response = await fetch(`/api/door/info?door=${door}`).then(res => res.json()); // polling assumes that the door was opened and whatever closed it was the buzzer system... // ie. state transition from OPEN to CLOSED before timeout means that twilio opened the door diff --git a/packages/serverless/.gitignore b/packages/serverless/.gitignore index a798209..655b8ab 100644 --- a/packages/serverless/.gitignore +++ b/packages/serverless/.gitignore @@ -11,7 +11,6 @@ lerna-debug.log* .pnpm-debug.log* assets -functions # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json diff --git a/packages/serverless/functions/api/door/auth.js b/packages/serverless/functions/api/door/auth.js new file mode 100644 index 0000000..51afa1b --- /dev/null +++ b/packages/serverless/functions/api/door/auth.js @@ -0,0 +1,56 @@ +/** + * Try to unlock the door with auth mode + */ + +const redis = require('redis'); + +function doorStatusKey(id) { + return concatKeys("doors", id, 'open'); +} + +function concatKeys(...keys) { + return keys.join(':'); +} + +exports.handler = function(context, event, callback) { + const response = new Twilio.Response(); + + let door = event.door; + let pin = event.key; + + if (!door || !pin) { + response.setStatusCode(400); + return callback(null, response); + } + + door = door.toUpperCase(); + + if (!context['FIXED_PIN_' + door]) { + response.setStatusCode(404); + return callback(null, response); + } + + let correctPin = context['FIXED_PIN' + door]; + + if (correctPin !== pin) { + response.setStatusCode(401); + return callback(null, response); + } + + let client = new redis.RedisDbClient((err) => console.error(err), { url: context.REDIS_CONNECT_URL }); + client.connect() + .then(async () => { + const statusKey = doorStatusKey(door); + const fingerprint = { method: "PIN" }; + const timeout = context['OPEN_TIMEOUT_' + door] || 60; + + await client.put(statusKey, JSON.stringify(fingerprint)); + await client.getClient().expire(statusKey, timeout); + response + .setStatusCode(200) + .appendHeader('Content-Type', 'application/json') + .setBody({ msg: `Opened the door "${door}" for ${timeout}s` }); + + return callback(null, response); + }); +}; \ No newline at end of file diff --git a/packages/serverless/functions/api/door/info.js b/packages/serverless/functions/api/door/info.js new file mode 100644 index 0000000..873e235 --- /dev/null +++ b/packages/serverless/functions/api/door/info.js @@ -0,0 +1,44 @@ +/** + * Try to get door info + */ + +const redis = require('redis'); + +function doorStatusKey(id) { + return concatKeys("doors", id, 'open'); +} + +function concatKeys(...keys) { + return keys.join(':'); +} + +exports.handler = function(context, event, callback) { + const response = new Twilio.Response(); + + let door = event.door || context.DEFAULT_DOOR; + + if (!door) { + response.setStatusCode(400); + return callback(null, response); + } + + door = door.toUpperCase(); + + const timeout = context['OPEN_TIMEOUT_' + door] || 60; + + + res.status(200).json(); + + let client = new RedisDbClient((err) => console.error(err), { url: context.REDIS_CONNECT_URL }); + client.connect() + .then(async () => { + const status = await client.get(doorStatusKey(door)) ? "OPEN": "CLOSED"; + + response + .setStatusCode(200) + .appendHeader('Content-Type', 'application/json') + .setBody({ id: doorId, timeout, status }); + + return callback(null, response); + }); +}; \ No newline at end of file diff --git a/packages/serverless/functions/api/door/status.js b/packages/serverless/functions/api/door/status.js new file mode 100644 index 0000000..b15e13d --- /dev/null +++ b/packages/serverless/functions/api/door/status.js @@ -0,0 +1,54 @@ +/** + * Try to update door status + */ + +const redis = require('redis'); + +function doorStatusKey(id) { + return concatKeys("doors", id, 'open'); +} + +function concatKeys(...keys) { + return keys.join(':'); +} + +exports.handler = function(context, event, callback) { + const response = new Twilio.Response(); + + let door = event.door; + + if (!door) { + response.setStatusCode(400); + return callback(null, response); + } + + let client = new RedisDbClient((err) => console.error(err), { url: context.REDIS_CONNECT_URL }); + client.connect() + .then(async () => { + const isOpen = await client.get(doorStatusKey(door)); + + if (isOpen) { + const fingerprint = JSON.parse(isOpen); + + response + .setStatusCode(200) + .appendHeader('Content-Type', 'application/json') + .setBody({ + status: "OPEN", + fingerprint, + }); + await client.remove(doorStatusKey(req.params.id)); + + return callback(null, response); + } + + response + .setStatusCode(401) + .appendHeader('Content-Type', 'application/json') + .setBody({ + status: "CLOSED", + }); + + return callback(null, response); + }); +}; \ No newline at end of file diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 08789df..f8d3c83 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -11,10 +11,6 @@ "dependencies": { "twilio": "^3.56", "@twilio/runtime-handler": "1.3.0", - "@codegenie/serverless-express": "^4.14.1", - "express": "^4.18.2", - "express-fileupload": "^1.4.0", - "express-rate-limit": "^6.10.0", "qrcode": "^1.5.3", "redis": "^4.6.8" },