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.
|
||||
|
||||
## 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
|
||||
|
||||
```bash
|
||||
bun run deploy-serverless
|
||||
```
|
||||
|
||||
## homeassistant integration
|
||||
## to deploy Doorman Buzzer client
|
||||
|
||||
```bash
|
||||
bun run deploy-buzzer-client
|
||||
```
|
||||
|
||||
## homeassistant integration poller
|
||||
|
||||
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": [
|
||||
{
|
||||
"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": "packages/serverless"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"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",
|
||||
"deploy-serverless": "bun run prepare-client-serverless && bun --filter 'serverless' deploy",
|
||||
"deploy-buzzer-client": "bun --filter 'buzzer-client' deploy"
|
||||
"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-ui && bun --filter 'doorman-api' deploy",
|
||||
"deploy-buzzer-client": "bun --filter 'doorman-client' deploy"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
|
||||
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
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "serverless",
|
||||
"name": "doorman-api",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "buzzer-client",
|
||||
"name": "doorman-client",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "doorman-client",
|
||||
"name": "doorman-ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"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