diff --git a/bun.lockb b/bun.lockb index b1e5e8d..f99ad27 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f16e265..4563846 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "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": { diff --git a/packages/server/package.json b/packages/server/package.json index 289a56e..ddeefff 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -4,7 +4,8 @@ "main": "index.js", "license": "MIT", "scripts": { - "start": "bun --hot run src/server.ts" + "start": "bun --hot run src/server.ts", + "build": "bun tsc" }, "dependencies": { "express": "^4.18.2", diff --git a/packages/server/src/clients/db/AbstractDbClient.ts b/packages/server/src/clients/db/AbstractDbClient.ts index 7b5311e..1d7237f 100644 --- a/packages/server/src/clients/db/AbstractDbClient.ts +++ b/packages/server/src/clients/db/AbstractDbClient.ts @@ -1,14 +1,6 @@ -import { IAccessControl } from "../../types/IAccessControl"; +import type { IAccessControl } from "../../types/IAccessControl"; export abstract class AbstractDbClient { - // set operations for challenges - /** - * Add a challenge in progress. It will expire from the DB after a configured amount of time - * @param challenge - 32 random bytes in hex - * @returns true if the operation completed successfully - */ - public abstract putChallenge(challenge: string): Promise; - /** * Checks if the given challenge already exists in the db * @param challenge - challenge to check against db @@ -16,13 +8,6 @@ export abstract class AbstractDbClient { */ public abstract doesChallengeExist(challenge: string): Promise; - /** - * Remove the challenge from the DB manually - * @param challenge - * @returns true if removed successfully, false if not in DB. - */ - public abstract removeChallenge(challenge: string): Promise; - // access control methods /** * Set an entry in the DB to mark that a particular challenge was completed by a certain key. diff --git a/packages/server/src/clients/db/RedisDbClient.ts b/packages/server/src/clients/db/RedisDbClient.ts index d82fc5a..9a738a2 100644 --- a/packages/server/src/clients/db/RedisDbClient.ts +++ b/packages/server/src/clients/db/RedisDbClient.ts @@ -1,5 +1,6 @@ -import { RedisClientOptions, RedisClientType, RedisFunctions, RedisModules, RedisScripts, createClient } from "redis"; -import { IAccessControl } from "../../types/IAccessControl"; +import type { RedisClientOptions, RedisClientType, RedisFunctions, RedisModules, RedisScripts } from "redis"; +import { createClient } from "redis"; +import type { IAccessControl } from "../../types/IAccessControl"; import { AbstractDbClient } from "./AbstractDbClient"; import { RedisKeys } from "../../types/RedisKeys"; @@ -18,24 +19,6 @@ export class RedisDbClient { - console.log("expiring challenge", challenge); - await this.removeChallenge(challenge); - }, Bun.env.CHALLENGE_EXPIRE_MS); - } - - public async putChallenge(challenge: string): Promise { - let res: number = await this.client.sAdd(RedisKeys.CHALLENGES, challenge); - - // if the sAdd succeeded - if (res > 0) { - this.scheduleRemoval(challenge); - } - - return res > 0; - } - public doesChallengeExist(challenge: string): Promise { return this.client.sIsMember(RedisKeys.CHALLENGES, challenge); } diff --git a/packages/server/src/clients/db/RedisDbProvider.ts b/packages/server/src/clients/db/RedisDbProvider.ts index a13db90..7306b1b 100644 --- a/packages/server/src/clients/db/RedisDbProvider.ts +++ b/packages/server/src/clients/db/RedisDbProvider.ts @@ -1,3 +1,4 @@ +import { getEnv } from "../../util/EnvConfigUtil"; import { RedisDbClient } from "./RedisDbClient"; @@ -6,7 +7,7 @@ let client: RedisDbClient; export async function getRedisClient(): Promise> { if (!client) { - client = new RedisDbClient((err) => console.error(err), { url: Bun.env.REDIS_CONNECT_URL }); + client = new RedisDbClient((err) => console.error(err), { url: getEnv("REDIS_CONNECT_URL") }); await client.connect() } diff --git a/packages/server/src/middlewares/DoorAuthModes.ts b/packages/server/src/middlewares/DoorAuthModes.ts index 8fa4d2f..235598a 100644 --- a/packages/server/src/middlewares/DoorAuthModes.ts +++ b/packages/server/src/middlewares/DoorAuthModes.ts @@ -1,6 +1,6 @@ -import { Request, RequestHandler } from "express"; +import type { Request, RequestHandler } from "express"; import { getRedisClient } from "../clients/db/RedisDbProvider"; -import { getAllDoorNames, getAuthModes, getDoorSettingString } from "../util/EnvConfigUtil"; +import { getAllDoorNames, getAuthModes, getDoorSettingString, getEnv } from "../util/EnvConfigUtil"; import { IAuthMode } from "../types/IAuthMode"; import { IDoorConfig } from "../types/IDoorConfig"; import { doorRotatingKey } from "../types/RedisKeys"; @@ -60,7 +60,7 @@ export const replaceDoorRandomKey = async (door: string) => { await client.put(doorRotatingKey(door), newKey); - const message = `New key for door ${door}! Unlock link: ${Bun.env.BASE_DOMAIN}/door/${door}?rotatingKey=${newKey}`; + const message = `New key for door ${door}! Unlock link: ${getEnv("BASE_DOMAIN")}/door/${door}?rotatingKey=${newKey}`; console.log(message); - await fetch(Bun.env.ROTATING_KEY_NTFY, { method: "POST", body: message }); + await fetch(getEnv("ROTATING_KEY_NTFY"), { method: "POST", body: message }); } \ No newline at end of file diff --git a/packages/server/src/middlewares/TimeLockMiddleware.ts b/packages/server/src/middlewares/TimeLockMiddleware.ts index b2b0471..78b8da8 100644 --- a/packages/server/src/middlewares/TimeLockMiddleware.ts +++ b/packages/server/src/middlewares/TimeLockMiddleware.ts @@ -1,4 +1,4 @@ -import { RequestHandler } from "express"; +import type { RequestHandler } from "express"; import { getDoorSettingTimeLock } from "../util/EnvConfigUtil"; import { IDoorStatus } from "../types/IDoorStatus"; diff --git a/packages/server/src/routers/DoorRouter.ts b/packages/server/src/routers/DoorRouter.ts index 0732097..f23229a 100644 --- a/packages/server/src/routers/DoorRouter.ts +++ b/packages/server/src/routers/DoorRouter.ts @@ -2,7 +2,7 @@ import express from "express"; import { getRedisClient } from "../clients/db/RedisDbProvider"; import { doorStatusKey } from "../types/RedisKeys"; import { HandleAuthMode } from "../middlewares/DoorAuthModes"; -import { getAllDoorNames, getAuthModes, getDoorSettingNumber, getDoorSettingString } from "../util/EnvConfigUtil"; +import { getAllDoorNames, getAuthModes, getDoorSettingNumber, getDoorSettingString, getEnv } from "../util/EnvConfigUtil"; import { IDoorConfig } from "../types/IDoorConfig"; import { TimeLockVerify } from "../middlewares/TimeLockMiddleware"; import { IDoorStatus } from "../types/IDoorStatus"; @@ -11,7 +11,7 @@ const router = express.Router(); const client = await getRedisClient(); router.get('/', async (req, res) => { - res.redirect(`/api/door/${Bun.env.DEFAULT_DOOR || getAllDoorNames()[0]}`); + res.redirect(`/api/door/${getEnv("DEFAULT_DOOR") || getAllDoorNames()[0]}`); }); router.get('/:id', async(req, res) => { diff --git a/packages/server/src/routers/LnurlRouter.ts b/packages/server/src/routers/LnurlRouter.ts deleted file mode 100644 index c768b23..0000000 --- a/packages/server/src/routers/LnurlRouter.ts +++ /dev/null @@ -1,62 +0,0 @@ -import express from "express"; -import { getRedisClient } from "../clients/db/RedisDbProvider"; -import crypto from "crypto"; - -// @ts-ignore -import lnurl, { verifyAuthorizationSignature } from "lnurl"; - -const router = express.Router(); -const client = await getRedisClient(); - -router.get("/login", async (req, res) => { - let challenge: string = ""; - - // challenge must be unique - do { - challenge = crypto.randomBytes(32).toString('hex'); - } while (await client.doesChallengeExist(challenge)); - - const { subdomain } = req.query; - - if (!subdomain) { - res.status(400).json({ message: "Missing subdomain" }); - return; - } - - // MUST BE https for clearnet - const url = `https://${subdomain}.${Bun.env.BASE_DOMAIN}/api/lnurl/validate?action=auth&tag=login&k1=${challenge}`; - const encoded = lnurl.encode(url); - - // add the challenge to db - await client.putChallenge(challenge); - - res.json({ challenge, lnurl: encoded }); -}); - -router.get('/validate', async (req, res) => { - // @ts-ignore - const { k1, key, sig } = req.query as IAuthCallbackQuery; - - if (!(await client.doesChallengeExist(k1))) { - console.log("challenge doesn't exist"); - res.status(404).json({ status: "ERROR", reason: "challenge not found" }); - return; - } - - const signatureValid: boolean = verifyAuthorizationSignature(sig, k1, key); - - if (signatureValid) { - console.log("signature is valid"); - - await client.removeChallenge(k1); - await client.markChallengeSuccess(k1, key); - - res.status(200).json({ status: "ok" }); - return; - } - - console.log("Signature mismatch"); - res.status(401).json({ status: "ERROR", reason: "Signature mismatch" }); -}); - -export default router; \ No newline at end of file diff --git a/packages/server/src/types/Environment.ts b/packages/server/src/types/Environment.ts deleted file mode 100644 index 4037de3..0000000 --- a/packages/server/src/types/Environment.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare module "bun" { - interface Env { - CHALLENGE_EXPIRE_MS: number; - BASE_DOMAIN: string; - REDIS_CONNECT_URL: string; // `redis[s]://[[username][:password]@][host][:port][/db-number]` - DOOR_OPEN_TIMEOUT: number; - DOOR_FIXED_PIN: string; - ROTATING_KEY_NTFY: string; - DEFAULT_DOOR: string; - } -} \ No newline at end of file diff --git a/packages/server/src/util/EnvConfigUtil.ts b/packages/server/src/util/EnvConfigUtil.ts index fc36460..45cd1cc 100644 --- a/packages/server/src/util/EnvConfigUtil.ts +++ b/packages/server/src/util/EnvConfigUtil.ts @@ -1,6 +1,10 @@ import { IAuthMode } from "../types/IAuthMode"; import { IDoorConfig } from "../types/IDoorConfig"; +export const getEnv = (key: string): string => { + return process.env[key] || ""; +} + const doorToEnv = (door: string): string => { return door.toUpperCase().replaceAll(' ', '_').replaceAll('-', '_'); }; @@ -16,7 +20,7 @@ export const getAuthModes = (door: string): IAuthMode[] => { }; export const getDoorSettingString = (door: string, setting: IDoorConfig): string | undefined => { - return Bun.env[`${setting}_${doorToEnv(door)}`]; + return getEnv(`${setting}_${doorToEnv(door)}`); }; export const getDoorSettingNumber = (door: string, setting: IDoorConfig): number => { @@ -41,7 +45,7 @@ export const getDoorSettingTimeLock = (door: string): number[] => { export const getAllDoorNames = (): string[] => { const names: string[] = []; - Object.keys(Bun.env).forEach(key => { + Object.keys(process.env).forEach(key => { if (key.startsWith(IDoorConfig.AUTH_MODES)) { names.push(key.replace(IDoorConfig.AUTH_MODES + "_", "").toLowerCase()); } diff --git a/packages/server/src/util/ExpireChallenges.ts b/packages/server/src/util/ExpireChallenges.ts deleted file mode 100644 index 8cf289e..0000000 --- a/packages/server/src/util/ExpireChallenges.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { RedisClientType } from "redis"; -import { RedisKeys } from "../types/RedisKeys"; - -export function expireChallenges(client: RedisClientType): Promise { - return client.zRemRangeByScore(RedisKeys.CHALLENGES, '-inf', Date.now()); -} \ No newline at end of file diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json new file mode 100644 index 0000000..8899e03 --- /dev/null +++ b/packages/server/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "outDir": "build", + + // Bundler mode + "moduleResolution": "bundler", + // "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": false, + + // Best practices + "strict": false, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // // Some stricter flags + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noPropertyAccessFromIndexSignature": true + } +} \ No newline at end of file diff --git a/packages/serverless/.gitignore b/packages/serverless/.gitignore index 655b8ab..a798209 100644 --- a/packages/serverless/.gitignore +++ b/packages/serverless/.gitignore @@ -11,6 +11,7 @@ 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/package.json b/packages/serverless/package.json index 7897b4e..08789df 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -5,11 +5,18 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "twilio-run", - "deploy": "twilio-run deploy --service-name react-twilio-serverless" + "deploy": "twilio-run deploy --service-name react-twilio-serverless", + "clean": "rm -rf assets/* functions/*" }, "dependencies": { "twilio": "^3.56", - "@twilio/runtime-handler": "1.3.0" + "@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" }, "devDependencies": { "twilio-run": "^3.5.4"