This commit is contained in:
parent
bc08150533
commit
24ca5f8e0b
20
Dockerfile
20
Dockerfile
@ -1,20 +0,0 @@
|
|||||||
FROM oven/bun
|
|
||||||
|
|
||||||
ADD packages packages
|
|
||||||
ADD bun.lockb bun.lockb
|
|
||||||
ADD package.json package.json
|
|
||||||
ADD tsconfig.json tsconfig.json
|
|
||||||
|
|
||||||
# install all deps
|
|
||||||
RUN bun install
|
|
||||||
|
|
||||||
# client build
|
|
||||||
WORKDIR /home/bun/app/packages/client
|
|
||||||
RUN bun run build
|
|
||||||
|
|
||||||
# move built client to server
|
|
||||||
RUN mv dist ../server/
|
|
||||||
WORKDIR /home/bun/app/packages/server
|
|
||||||
|
|
||||||
# start server
|
|
||||||
CMD bun run ./src/server.ts
|
|
||||||
12
README.md
12
README.md
@ -14,13 +14,23 @@ bun run index.ts
|
|||||||
|
|
||||||
This project was created using `bun init` in bun v1.0.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
This project was created using `bun init` in bun v1.0.3. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||||
|
|
||||||
|
## Deployments
|
||||||
|
|
||||||
|
They are configured to happen in gitea actions for CI/CD. If you need to deploy manually, it should be possible as long as you source the requisite envs first.
|
||||||
|
|
||||||
## to deploy Doorman API / UI
|
## to deploy Doorman API / UI
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run deploy-serverless
|
bun run deploy-serverless
|
||||||
```
|
```
|
||||||
|
|
||||||
## homeassistant integration
|
## to deploy Doorman Buzzer client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run deploy-buzzer-client
|
||||||
|
```
|
||||||
|
|
||||||
|
## homeassistant integration poller
|
||||||
|
|
||||||
in configuration.yaml
|
in configuration.yaml
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
version: "3"
|
|
||||||
|
|
||||||
services:
|
|
||||||
doorman:
|
|
||||||
container_name: doorman
|
|
||||||
image: gitea.chromart.dedyn.io/martin/doorman:latest
|
|
||||||
environment:
|
|
||||||
- CHALLENGE_EXPIRE_MS=105000
|
|
||||||
- BASE_DOMAIN=gitea.chromart.dedyn.io
|
|
||||||
- REDIS_CONNECT_URL=redis://@redis:6379
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
redis:
|
|
||||||
image: redis:latest
|
|
||||||
@ -1,16 +1,20 @@
|
|||||||
{
|
{
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"path": "packages/client"
|
"name": "doorman-ui",
|
||||||
|
"path": "packages/doorman-ui"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "packages/server"
|
"name": "doorman-api",
|
||||||
|
"path": "packages/doorman-api"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"name": "doorman-client",
|
||||||
|
"path": "packages/doorman-client"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "doorman",
|
||||||
"path": "."
|
"path": "."
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "packages/serverless"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
"bun-types": "latest"
|
"bun-types": "latest"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare-client-serverless": "bun --filter 'doorman-client' build && rm -rf packages/serverless/assets/* && mkdir -p packages/serverless/assets/assets && cp -fr packages/client/dist/* packages/serverless/assets/ && cp -f packages/serverless/assets/index.html packages/serverless/assets/assets/index.html",
|
"prepare-ui": "bun --filter 'doorman-ui' build && rm -rf packages/doorman-api/assets/* && mkdir -p packages/doorman-api/assets/assets && cp -fr packages/doorman-ui/dist/* packages/doorman-api/assets/ && cp -f packages/doorman-api/assets/index.html packages/doorman-api/assets/assets/index.html",
|
||||||
"deploy-serverless": "bun run prepare-client-serverless && bun --filter 'serverless' deploy",
|
"deploy-serverless": "bun run prepare-ui && bun --filter 'doorman-api' deploy",
|
||||||
"deploy-buzzer-client": "bun --filter 'buzzer-client' deploy"
|
"deploy-buzzer-client": "bun --filter 'doorman-client' deploy"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
this project deploys the UI and API to twilio functions https://doorman-6741-prod.twil.io
|
this project deploys the UI and API to twilio functions https://doorman-6741-prod.twil.io
|
||||||
|
|
||||||
It uses a cloud redis cache
|
It uses DDB for the backend
|
||||||
|
|
||||||
After the twilio functions I have setup a cloudflare worker at https://doorman.chromart.workers.dev
|
After the twilio functions I have setup a cloudflare worker at https://doorman.chromart.cc to proxy the requests to the twilio lambda
|
||||||
|
|
||||||
The cloudflare worker just proxies requests so the endpoint is a bit nicer
|
The cloudflare worker just proxies requests so the endpoint is a bit nicer
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "serverless",
|
"name": "doorman-api",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "buzzer-client",
|
"name": "doorman-client",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "doorman-client",
|
"name": "doorman-ui",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "doorman-server",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"license": "MIT",
|
|
||||||
"scripts": {
|
|
||||||
"start": "bun --hot run src/server.ts",
|
|
||||||
"build": "bun tsc"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"express-fileupload": "^1.4.0",
|
|
||||||
"express-rate-limit": "^6.10.0",
|
|
||||||
"lnurl": "^0.25.1",
|
|
||||||
"qrcode": "^1.5.3",
|
|
||||||
"redis": "^4.6.8",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"ts-node-dev": "^2.0.0",
|
|
||||||
"typescript": "^5.2.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/express": "^4.17.17",
|
|
||||||
"@types/express-fileupload": "^1.4.1",
|
|
||||||
"@types/express-rate-limit": "^6.0.0",
|
|
||||||
"@types/node": "^20.6.4",
|
|
||||||
"@types/qrcode": "^1.5.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
import type { IAccessControl } from "../../types/IAccessControl";
|
|
||||||
|
|
||||||
export abstract class AbstractDbClient {
|
|
||||||
/**
|
|
||||||
* Checks if the given challenge already exists in the db
|
|
||||||
* @param challenge - challenge to check against db
|
|
||||||
* @returns true if challenge exists in DB
|
|
||||||
*/
|
|
||||||
public abstract doesChallengeExist(challenge: string): Promise<boolean>;
|
|
||||||
|
|
||||||
// access control methods
|
|
||||||
/**
|
|
||||||
* Set an entry in the DB to mark that a particular challenge was completed by a certain key.
|
|
||||||
* A future operation which presents this challenge will be authenticated by the key associated in this operation
|
|
||||||
* @param challenge - the challenge which was completed
|
|
||||||
* @param key - the key which was part of the authentication test
|
|
||||||
*/
|
|
||||||
public abstract markChallengeSuccess(challenge: string, key: string): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls into the DB to see if a particular challege has been completed. If it has, return the relevant access control metadata.
|
|
||||||
* This method will also decrement the access count, and remove it if it falls below the number of remaining access tokens on this challenge
|
|
||||||
* @param challenge - 32 byte hex challenge
|
|
||||||
*/
|
|
||||||
public abstract accessFromChallenge(challenge: string): Promise<IAccessControl | null>;
|
|
||||||
|
|
||||||
public abstract connect(): Promise<any>;
|
|
||||||
|
|
||||||
public abstract put(key: string, value: string): Promise<any>;
|
|
||||||
public abstract exists(key: string): Promise<any>;
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
export class RedisDbClient<A extends RedisModules, B extends RedisFunctions, C extends RedisScripts> extends AbstractDbClient {
|
|
||||||
private client: RedisClientType<A, B, C>;
|
|
||||||
|
|
||||||
private timers: { [challenge: string]: NodeJS.Timeout } = {};
|
|
||||||
|
|
||||||
constructor(onError: (err: any) => void, options?: RedisClientOptions<A, B, C>) {
|
|
||||||
super();
|
|
||||||
this.client = createClient(options);
|
|
||||||
this.client.on("error", onError);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async connect(): Promise<RedisClientType<A, B, C>> {
|
|
||||||
return this.client.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
public doesChallengeExist(challenge: string): Promise<boolean> {
|
|
||||||
return this.client.sIsMember(RedisKeys.CHALLENGES, challenge);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async removeChallenge(challenge: string): Promise<boolean> {
|
|
||||||
let res: number = await this.client.sRem(RedisKeys.CHALLENGES, challenge);
|
|
||||||
clearTimeout(this.timers[challenge]);
|
|
||||||
delete this.timers[challenge];
|
|
||||||
return res > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async markChallengeSuccess(challenge: string, key: string): Promise<void> {
|
|
||||||
await this.client.set(challenge, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async accessFromChallenge(challenge: string): Promise<IAccessControl | null> {
|
|
||||||
let key: string | null = await this.client.getDel(challenge);
|
|
||||||
|
|
||||||
if (key == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
remainingAccess: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async put(key: string, value: string): Promise<any> {
|
|
||||||
return this.client.set(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async exists(key: string): Promise<number> {
|
|
||||||
return this.client.exists(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async get(key: string): Promise<string | null> {
|
|
||||||
return this.client.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async remove(key: string): Promise<string | null> {
|
|
||||||
return this.client.getDel(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getClient() {
|
|
||||||
return this.client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { getEnv } from "../../util/EnvConfigUtil";
|
|
||||||
import { RedisDbClient } from "./RedisDbClient";
|
|
||||||
|
|
||||||
|
|
||||||
let client: RedisDbClient<any, any, any>;
|
|
||||||
|
|
||||||
|
|
||||||
export async function getRedisClient(): Promise<RedisDbClient<any, any, any>> {
|
|
||||||
if (!client) {
|
|
||||||
client = new RedisDbClient((err) => console.error(err), { url: getEnv("REDIS_CONNECT_URL") });
|
|
||||||
await client.connect()
|
|
||||||
}
|
|
||||||
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import type { Request, RequestHandler } from "express";
|
|
||||||
import { getRedisClient } from "../clients/db/RedisDbProvider";
|
|
||||||
import { getAllDoorNames, getAuthModes, getDoorSettingString, getEnv } from "../util/EnvConfigUtil";
|
|
||||||
import { IAuthMode } from "../types/IAuthMode";
|
|
||||||
import { IDoorConfig } from "../types/IDoorConfig";
|
|
||||||
import { doorRotatingKey } from "../types/RedisKeys";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const client = await getRedisClient();
|
|
||||||
|
|
||||||
export const HandleAuthMode: RequestHandler = async (req, res, next) => {
|
|
||||||
const authModes = getAuthModes(req.params.id);
|
|
||||||
|
|
||||||
if (authModes.length === 0) {
|
|
||||||
res.status(404).json({ msg: `Unknown door ${req.params.id}` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkAuth = async (mode: IAuthMode): Promise<boolean> => {
|
|
||||||
switch(mode) {
|
|
||||||
case IAuthMode.FIXED_PIN: return handleFixedPinAuth(req)
|
|
||||||
case IAuthMode.RANDOM_ROTATING_KEY: return await handleRandomRotatingKeyAuth(req)
|
|
||||||
default: return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAuthorized = (await Promise.all(authModes.map((mode) => checkAuth(mode)))).some(b => b);
|
|
||||||
|
|
||||||
if (!isAuthorized) {
|
|
||||||
res.status(401).json({ msg: 'Unauthorized' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFixedPinAuth = (req: Request): boolean => {
|
|
||||||
const fixedPin = getDoorSettingString(req.params.id, IDoorConfig.FIXED_PIN);
|
|
||||||
return fixedPin !== undefined && req.query['key'] === fixedPin;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRandomRotatingKeyAuth = async (req: Request): Promise<boolean> => {
|
|
||||||
const currentPin = await client.get(doorRotatingKey(req.params.id));
|
|
||||||
|
|
||||||
if (currentPin === req.query['rotatingKey']) {
|
|
||||||
await replaceDoorRandomKey(req.params.id);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initializeRandomDoorPins = () => {
|
|
||||||
const doors = getAllDoorNames();
|
|
||||||
doors.forEach(replaceDoorRandomKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const replaceDoorRandomKey = async (door: string) => {
|
|
||||||
const newKey = crypto.randomBytes(20).toString('hex');
|
|
||||||
|
|
||||||
await client.put(doorRotatingKey(door), newKey);
|
|
||||||
|
|
||||||
const message = `New key for door ${door}! Unlock link: ${getEnv("BASE_DOMAIN")}/door/${door}?rotatingKey=${newKey}`;
|
|
||||||
console.log(message);
|
|
||||||
await fetch(getEnv("ROTATING_KEY_NTFY"), { method: "POST", body: message });
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import type { RequestHandler } from "express";
|
|
||||||
import { getDoorSettingTimeLock } from "../util/EnvConfigUtil";
|
|
||||||
import { IDoorStatus } from "../types/IDoorStatus";
|
|
||||||
|
|
||||||
export const TimeLockVerify: RequestHandler = async (req, res, next) => {
|
|
||||||
const timeLock = getDoorSettingTimeLock(req.params.id);
|
|
||||||
|
|
||||||
const timeHr = (new Date()).getHours();
|
|
||||||
|
|
||||||
|
|
||||||
if (timeHr >= timeLock[0] || timeHr <= timeLock[1]) {
|
|
||||||
res.status(410).json({ status: IDoorStatus.TIME_LOCK, msg: 'Sorry! This door is locked at this hour, try again later' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
@ -1,65 +0,0 @@
|
|||||||
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, getEnv } from "../util/EnvConfigUtil";
|
|
||||||
import { IDoorConfig } from "../types/IDoorConfig";
|
|
||||||
import { TimeLockVerify } from "../middlewares/TimeLockMiddleware";
|
|
||||||
import { IDoorStatus } from "../types/IDoorStatus";
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
const client = await getRedisClient();
|
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
|
||||||
res.redirect(`/api/door/${getEnv("DEFAULT_DOOR") || getAllDoorNames()[0]}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id', async(req, res) => {
|
|
||||||
const doorId = req.params.id;
|
|
||||||
const authModes = getAuthModes(doorId);
|
|
||||||
const timeout = getDoorSettingNumber(doorId, IDoorConfig.OPEN_TIMEOUT)|| 60;
|
|
||||||
|
|
||||||
if (authModes.length === 0) {
|
|
||||||
res.status(404).json({ msg: `Door ${doorId} not found` });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = await client.get(doorStatusKey(doorId)) ? IDoorStatus.OPEN: IDoorStatus.CLOSED;
|
|
||||||
|
|
||||||
res.status(200).json({ id: doorId, authModes, timeout, status });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/:id/status', TimeLockVerify, async(req, res) => {
|
|
||||||
const isOpen = await client.get(doorStatusKey(req.params.id));
|
|
||||||
|
|
||||||
if (isOpen) {
|
|
||||||
const fingerprint = JSON.parse(isOpen);
|
|
||||||
res.status(200).json({ status: IDoorStatus.OPEN, fingerprint });
|
|
||||||
|
|
||||||
if (getDoorSettingString(req.params.id, IDoorConfig.CLOSE_AFTER_POLL)) {
|
|
||||||
await client.remove(doorStatusKey(req.params.id));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(401).json({ status: IDoorStatus.CLOSED });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/:id/status', async(req, res) => {
|
|
||||||
await client.remove(doorStatusKey(req.params.id));
|
|
||||||
res.status(200).json({ msg: `Closed the door ${req.params.id}` });
|
|
||||||
});
|
|
||||||
|
|
||||||
router.all('/:id/auth', HandleAuthMode, async(req, res) => {
|
|
||||||
const statusKey = doorStatusKey(req.params.id);
|
|
||||||
|
|
||||||
const fingerprint = (req as any).fingerprint;
|
|
||||||
|
|
||||||
const timeout = getDoorSettingNumber(req.params.id, IDoorConfig.OPEN_TIMEOUT) || 60;
|
|
||||||
|
|
||||||
await client.put(statusKey, JSON.stringify(fingerprint));
|
|
||||||
await client.getClient().expire(statusKey, timeout);
|
|
||||||
res.status(200).json({ msg: `Opened the door "${req.params.id}" for ${timeout}s` });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import DoorRouter from "./routers/DoorRouter";
|
|
||||||
import { initializeRandomDoorPins } from "./middlewares/DoorAuthModes";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const Fingerprint = require('express-fingerprint');
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.set('trust proxy', 1);
|
|
||||||
|
|
||||||
app.use(Fingerprint({
|
|
||||||
parameters: [
|
|
||||||
Fingerprint.useragent,
|
|
||||||
Fingerprint.geoip
|
|
||||||
]
|
|
||||||
}));
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
(req as any).fingerprint.ip = req.ip;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.static("dist"));
|
|
||||||
app.use('/api/door', DoorRouter);
|
|
||||||
|
|
||||||
app.get("*", (req, res) => {
|
|
||||||
res.sendFile(path.join(__dirname, "..", "dist", "index.html"));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(5000, async () => {
|
|
||||||
console.log("listening on port 5000");
|
|
||||||
initializeRandomDoorPins();
|
|
||||||
});
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export interface IAccessControl {
|
|
||||||
key: string;
|
|
||||||
remainingAccess?: number;
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export default interface IAuthCallbackQuery {
|
|
||||||
action: string;
|
|
||||||
tag: string;
|
|
||||||
k1: string;
|
|
||||||
sig: string;
|
|
||||||
key: string
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export enum IAuthMode {
|
|
||||||
FIXED_PIN = "FIXED_PIN",
|
|
||||||
RANDOM_ROTATING_KEY = "RANDOM_ROTATING_KEY",
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
export enum IDoorConfig {
|
|
||||||
AUTH_MODES = "AUTH_MODES",
|
|
||||||
OPEN_TIMEOUT = "OPEN_TIMEOUT",
|
|
||||||
FIXED_PIN = "FIXED_PIN",
|
|
||||||
CLOSE_AFTER_POLL="CLOSE_AFTER_POLL",
|
|
||||||
ALWAYS_LOCKED_TIME="ALWAYS_LOCKED_TIME",
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export interface IDoorResponse {
|
|
||||||
id: string,
|
|
||||||
timeout: number;
|
|
||||||
buzzerCode: string;
|
|
||||||
};
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export enum IDoorStatus {
|
|
||||||
OPEN="OPEN",
|
|
||||||
CLOSED="CLOSED",
|
|
||||||
TIME_LOCK="TIME_LOCK"
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
export enum RedisKeys {
|
|
||||||
CHALLENGES = "challenges",
|
|
||||||
DOORS = "doors"
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doorStatusKey(id: string) {
|
|
||||||
return concatKeys(RedisKeys.DOORS, id, 'open');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function doorRotatingKey(id: string) {
|
|
||||||
return concatKeys(RedisKeys.DOORS, id, 'rotatingKey');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function concatKeys(...keys: String[]) {
|
|
||||||
return keys.join(':');
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
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('-', '_');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAuthModes = (door: string): IAuthMode[] => {
|
|
||||||
const config = getDoorSettingString(door, IDoorConfig.AUTH_MODES);
|
|
||||||
|
|
||||||
if (config) {
|
|
||||||
return JSON.parse(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDoorSettingString = (door: string, setting: IDoorConfig): string | undefined => {
|
|
||||||
return getEnv(`${setting}_${doorToEnv(door)}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDoorSettingNumber = (door: string, setting: IDoorConfig): number => {
|
|
||||||
return parseInt(getDoorSettingString(door, setting) || "0");
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDoorSettingTimeLock = (door: string): number[] => {
|
|
||||||
const config = getDoorSettingString(door, IDoorConfig.ALWAYS_LOCKED_TIME);
|
|
||||||
|
|
||||||
if (config) {
|
|
||||||
try {
|
|
||||||
return config.split(',').map(n => parseInt(n));
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Config ${IDoorConfig.ALWAYS_LOCKED_TIME} for door ${door} is invalid`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// never locked (always -1 < hr < 25)
|
|
||||||
return [25, -1];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAllDoorNames = (): string[] => {
|
|
||||||
const names: string[] = [];
|
|
||||||
|
|
||||||
Object.keys(process.env).forEach(key => {
|
|
||||||
if (key.startsWith(IDoorConfig.AUTH_MODES)) {
|
|
||||||
names.push(key.replace(IDoorConfig.AUTH_MODES + "_", "").toLowerCase());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import rateLimit from "express-rate-limit";
|
|
||||||
|
|
||||||
|
|
||||||
export const uploadDownloadLimiter = rateLimit({
|
|
||||||
windowMs: 5 * 60 * 1000,
|
|
||||||
max: 5,
|
|
||||||
skipFailedRequests: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const challengeLimiter = rateLimit({
|
|
||||||
windowMs: 5 * 60 * 1000,
|
|
||||||
max: 10,
|
|
||||||
skipFailedRequests: true,
|
|
||||||
});
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user