setup ts for building
This commit is contained in:
parent
1a2e128c47
commit
309c70f42e
@ -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": {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 });
|
||||||
}
|
}
|
||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
|||||||
@ -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;
|
|
||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
|
||||||
}
|
|
||||||
28
packages/server/tsconfig.json
Normal file
28
packages/server/tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/serverless/.gitignore
vendored
1
packages/serverless/.gitignore
vendored
@ -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
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user