setup ts for building

This commit is contained in:
Martin Dimitrov 2024-05-03 14:21:58 -07:00
parent 1a2e128c47
commit 309c70f42e
16 changed files with 60 additions and 128 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -8,6 +8,7 @@
}, },
"scripts": { "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-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" "deploy-serverless": "bun run prepare-client-serverless && bun --filter 'serverless' deploy"
}, },
"peerDependencies": { "peerDependencies": {

View File

@ -4,7 +4,8 @@
"main": "index.js", "main": "index.js",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"start": "bun --hot run src/server.ts" "start": "bun --hot run src/server.ts",
"build": "bun tsc"
}, },
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",

View File

@ -1,14 +1,6 @@
import { IAccessControl } from "../../types/IAccessControl"; import type { IAccessControl } from "../../types/IAccessControl";
export abstract class AbstractDbClient { 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<boolean>;
/** /**
* Checks if the given challenge already exists in the db * Checks if the given challenge already exists in the db
* @param challenge - challenge to check against db * @param challenge - challenge to check against db
@ -16,13 +8,6 @@ export abstract class AbstractDbClient {
*/ */
public abstract doesChallengeExist(challenge: string): Promise<boolean>; public abstract doesChallengeExist(challenge: string): Promise<boolean>;
/**
* 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<boolean>;
// access control methods // access control methods
/** /**
* Set an entry in the DB to mark that a particular challenge was completed by a certain key. * Set an entry in the DB to mark that a particular challenge was completed by a certain key.

View File

@ -1,5 +1,6 @@
import { RedisClientOptions, RedisClientType, RedisFunctions, RedisModules, RedisScripts, createClient } from "redis"; import type { RedisClientOptions, RedisClientType, RedisFunctions, RedisModules, RedisScripts } from "redis";
import { IAccessControl } from "../../types/IAccessControl"; import { createClient } from "redis";
import type { IAccessControl } from "../../types/IAccessControl";
import { AbstractDbClient } from "./AbstractDbClient"; import { AbstractDbClient } from "./AbstractDbClient";
import { RedisKeys } from "../../types/RedisKeys"; import { RedisKeys } from "../../types/RedisKeys";
@ -18,24 +19,6 @@ export class RedisDbClient<A extends RedisModules, B extends RedisFunctions, C e
return this.client.connect(); return this.client.connect();
} }
private scheduleRemoval(challenge: string): void {
this.timers[challenge] = setTimeout(async () => {
console.log("expiring challenge", challenge);
await this.removeChallenge(challenge);
}, Bun.env.CHALLENGE_EXPIRE_MS);
}
public async putChallenge(challenge: string): Promise<boolean> {
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<boolean> { public doesChallengeExist(challenge: string): Promise<boolean> {
return this.client.sIsMember(RedisKeys.CHALLENGES, challenge); return this.client.sIsMember(RedisKeys.CHALLENGES, challenge);
} }

View File

@ -1,3 +1,4 @@
import { getEnv } from "../../util/EnvConfigUtil";
import { RedisDbClient } from "./RedisDbClient"; import { RedisDbClient } from "./RedisDbClient";
@ -6,7 +7,7 @@ let client: RedisDbClient<any, any, any>;
export async function getRedisClient(): Promise<RedisDbClient<any, any, any>> { export async function getRedisClient(): Promise<RedisDbClient<any, any, any>> {
if (!client) { 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() await client.connect()
} }

View File

@ -1,6 +1,6 @@
import { Request, RequestHandler } from "express"; import type { Request, RequestHandler } from "express";
import { getRedisClient } from "../clients/db/RedisDbProvider"; 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 { IAuthMode } from "../types/IAuthMode";
import { IDoorConfig } from "../types/IDoorConfig"; import { IDoorConfig } from "../types/IDoorConfig";
import { doorRotatingKey } from "../types/RedisKeys"; import { doorRotatingKey } from "../types/RedisKeys";
@ -60,7 +60,7 @@ export const replaceDoorRandomKey = async (door: string) => {
await client.put(doorRotatingKey(door), newKey); 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); console.log(message);
await fetch(Bun.env.ROTATING_KEY_NTFY, { method: "POST", body: message }); await fetch(getEnv("ROTATING_KEY_NTFY"), { method: "POST", body: message });
} }

View File

@ -1,4 +1,4 @@
import { RequestHandler } from "express"; import type { RequestHandler } from "express";
import { getDoorSettingTimeLock } from "../util/EnvConfigUtil"; import { getDoorSettingTimeLock } from "../util/EnvConfigUtil";
import { IDoorStatus } from "../types/IDoorStatus"; import { IDoorStatus } from "../types/IDoorStatus";

View File

@ -2,7 +2,7 @@ import express from "express";
import { getRedisClient } from "../clients/db/RedisDbProvider"; import { getRedisClient } from "../clients/db/RedisDbProvider";
import { doorStatusKey } from "../types/RedisKeys"; import { doorStatusKey } from "../types/RedisKeys";
import { HandleAuthMode } from "../middlewares/DoorAuthModes"; 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 { IDoorConfig } from "../types/IDoorConfig";
import { TimeLockVerify } from "../middlewares/TimeLockMiddleware"; import { TimeLockVerify } from "../middlewares/TimeLockMiddleware";
import { IDoorStatus } from "../types/IDoorStatus"; import { IDoorStatus } from "../types/IDoorStatus";
@ -11,7 +11,7 @@ const router = express.Router();
const client = await getRedisClient(); const client = await getRedisClient();
router.get('/', async (req, res) => { 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) => { router.get('/:id', async(req, res) => {

View File

@ -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;

View File

@ -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;
}
}

View File

@ -1,6 +1,10 @@
import { IAuthMode } from "../types/IAuthMode"; import { IAuthMode } from "../types/IAuthMode";
import { IDoorConfig } from "../types/IDoorConfig"; import { IDoorConfig } from "../types/IDoorConfig";
export const getEnv = (key: string): string => {
return process.env[key] || "";
}
const doorToEnv = (door: string): string => { const doorToEnv = (door: string): string => {
return door.toUpperCase().replaceAll(' ', '_').replaceAll('-', '_'); return door.toUpperCase().replaceAll(' ', '_').replaceAll('-', '_');
}; };
@ -16,7 +20,7 @@ export const getAuthModes = (door: string): IAuthMode[] => {
}; };
export const getDoorSettingString = (door: string, setting: IDoorConfig): string | undefined => { 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 => { export const getDoorSettingNumber = (door: string, setting: IDoorConfig): number => {
@ -41,7 +45,7 @@ export const getDoorSettingTimeLock = (door: string): number[] => {
export const getAllDoorNames = (): string[] => { export const getAllDoorNames = (): string[] => {
const names: string[] = []; const names: string[] = [];
Object.keys(Bun.env).forEach(key => { Object.keys(process.env).forEach(key => {
if (key.startsWith(IDoorConfig.AUTH_MODES)) { if (key.startsWith(IDoorConfig.AUTH_MODES)) {
names.push(key.replace(IDoorConfig.AUTH_MODES + "_", "").toLowerCase()); names.push(key.replace(IDoorConfig.AUTH_MODES + "_", "").toLowerCase());
} }

View File

@ -1,6 +0,0 @@
import { RedisClientType } from "redis";
import { RedisKeys } from "../types/RedisKeys";
export function expireChallenges(client: RedisClientType): Promise<any> {
return client.zRemRangeByScore(RedisKeys.CHALLENGES, '-inf', Date.now());
}

View File

@ -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
}
}

View File

@ -11,6 +11,7 @@ lerna-debug.log*
.pnpm-debug.log* .pnpm-debug.log*
assets assets
functions
# Diagnostic reports (https://nodejs.org/api/report.html) # Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View File

@ -5,11 +5,18 @@
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"start": "twilio-run", "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": { "dependencies": {
"twilio": "^3.56", "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": { "devDependencies": {
"twilio-run": "^3.5.4" "twilio-run": "^3.5.4"