mielk-api 1.3.10 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http/httpResponseStatus/HttpResponseStatus.d.ts +4 -0
- package/dist/http/httpResponseStatus/HttpResponseStatus.js +4 -0
- package/dist/internal/messaging/messageTags.d.ts +4 -1
- package/dist/internal/messaging/messageTags.js +4 -1
- package/dist/middlewares/cors/privateCors.js +1 -0
- package/dist/middlewares/cors/publicCors.js +1 -0
- package/dist/middlewares/index.d.ts +6 -2
- package/dist/middlewares/index.js +4 -2
- package/dist/middlewares/redisLocks/redisLocker.d.ts +7 -0
- package/dist/middlewares/redisLocks/redisLocker.js +46 -0
- package/dist/middlewares/redisLocks/redisLockerConfigFactory.d.ts +4 -0
- package/dist/middlewares/redisLocks/redisLockerConfigFactory.js +17 -0
- package/dist/middlewares/redisLocks/types.d.ts +14 -0
- package/dist/middlewares/redisLocks/types.js +1 -0
- package/dist/rateLimit/checkRateLimit.d.ts +2 -0
- package/dist/rateLimit/checkRateLimit.js +33 -0
- package/dist/rateLimit/createRateLimiter.d.ts +4 -0
- package/dist/rateLimit/createRateLimiter.js +49 -0
- package/dist/rateLimit/index.d.ts +8 -0
- package/dist/rateLimit/index.js +5 -0
- package/dist/rateLimit/rateLimitConfigFactory.d.ts +7 -0
- package/dist/rateLimit/rateLimitConfigFactory.js +31 -0
- package/dist/rateLimit/redisKeysFactory.d.ts +14 -0
- package/dist/rateLimit/redisKeysFactory.js +30 -0
- package/dist/rateLimit/types.d.ts +35 -0
- package/dist/rateLimit/types.js +1 -0
- package/dist/routing/express.d.ts +3 -2
- package/dist/routing/express.js +16 -4
- package/package.json +8 -3
- package/dist/middlewares/rateLimit/rateLimit.d.ts +0 -8
- package/dist/middlewares/rateLimit/rateLimit.js +0 -18
|
@@ -2,10 +2,14 @@ import { HttpStatus } from "../types/HttpStatus.js";
|
|
|
2
2
|
export declare const HttpResponseStatus: {
|
|
3
3
|
OK: HttpStatus;
|
|
4
4
|
CREATED: HttpStatus;
|
|
5
|
+
ACCEPTED: HttpStatus;
|
|
6
|
+
NO_CONTENT: HttpStatus;
|
|
5
7
|
BAD_REQUEST: HttpStatus;
|
|
6
8
|
UNAUTHORIZED: HttpStatus;
|
|
7
9
|
FORBIDDEN: HttpStatus;
|
|
8
10
|
NOT_FOUND: HttpStatus;
|
|
9
11
|
CONFLICT: HttpStatus;
|
|
12
|
+
LOCKED: HttpStatus;
|
|
13
|
+
TOO_MANY_REQUESTS: HttpStatus;
|
|
10
14
|
SERVER_ERROR: HttpStatus;
|
|
11
15
|
};
|
|
@@ -4,10 +4,14 @@ const msg = Msg.apiStatus;
|
|
|
4
4
|
export const HttpResponseStatus = {
|
|
5
5
|
OK: createApiStatus(true, 200, msg.ok),
|
|
6
6
|
CREATED: createApiStatus(true, 201, msg.created),
|
|
7
|
+
ACCEPTED: createApiStatus(true, 202, msg.created),
|
|
8
|
+
NO_CONTENT: createApiStatus(true, 204, msg.noContent),
|
|
7
9
|
BAD_REQUEST: createApiStatus(false, 400, msg.badRequest),
|
|
8
10
|
UNAUTHORIZED: createApiStatus(false, 401, msg.unauthorized),
|
|
9
11
|
FORBIDDEN: createApiStatus(false, 403, msg.forbidden),
|
|
10
12
|
NOT_FOUND: createApiStatus(false, 404, msg.notFound),
|
|
11
13
|
CONFLICT: createApiStatus(false, 409, msg.conflict),
|
|
14
|
+
LOCKED: createApiStatus(false, 423, msg.locked),
|
|
15
|
+
TOO_MANY_REQUESTS: createApiStatus(false, 429, msg.tooManyRequests),
|
|
12
16
|
SERVER_ERROR: createApiStatus(false, 500, msg.serverError),
|
|
13
17
|
};
|
|
@@ -2,17 +2,20 @@ export declare const Msg: {
|
|
|
2
2
|
apiStatus: {
|
|
3
3
|
ok: string;
|
|
4
4
|
created: string;
|
|
5
|
+
accepted: string;
|
|
6
|
+
noContent: string;
|
|
5
7
|
badRequest: string;
|
|
6
8
|
unauthorized: string;
|
|
7
9
|
forbidden: string;
|
|
8
10
|
notFound: string;
|
|
9
11
|
conflict: string;
|
|
12
|
+
locked: string;
|
|
13
|
+
tooManyRequests: string;
|
|
10
14
|
serverError: string;
|
|
11
15
|
};
|
|
12
16
|
connection: {
|
|
13
17
|
corsBlocked: string;
|
|
14
18
|
notInitialized: string;
|
|
15
|
-
rateLimitExceededMessage: string;
|
|
16
19
|
sshOptionsNotSpecified: string;
|
|
17
20
|
sshTunnelFailed: string;
|
|
18
21
|
postgreConnectionError: string;
|
|
@@ -5,17 +5,20 @@ export const Msg = {
|
|
|
5
5
|
apiStatus: {
|
|
6
6
|
ok: `${___HTTP_STATUS___}:ok`,
|
|
7
7
|
created: `${___HTTP_STATUS___}:created`,
|
|
8
|
+
accepted: `${___HTTP_STATUS___}:accepted`,
|
|
9
|
+
noContent: `${___HTTP_STATUS___}:noContent`,
|
|
8
10
|
badRequest: `${___HTTP_STATUS___}:badRequest`,
|
|
9
11
|
unauthorized: `${___HTTP_STATUS___}:unauthorized`,
|
|
10
12
|
forbidden: `${___HTTP_STATUS___}:forbidden`,
|
|
11
13
|
notFound: `${___HTTP_STATUS___}:notFound`,
|
|
12
14
|
conflict: `${___HTTP_STATUS___}:conflict`,
|
|
15
|
+
locked: `${___HTTP_STATUS___}:locked`, // Account is temporarily locked. Try again later or reset your password. OR Your account has been temporarily locked due to multiple failed login attempts. Please reset your password to regain access.
|
|
16
|
+
tooManyRequests: `${___HTTP_STATUS___}:tooManyRequests`,
|
|
13
17
|
serverError: `${___HTTP_STATUS___}:serverError`,
|
|
14
18
|
},
|
|
15
19
|
connection: {
|
|
16
20
|
corsBlocked: `${___CONNECTION___}:corsBlocked`,
|
|
17
21
|
notInitialized: `${___CONNECTION___}:notInitialized`,
|
|
18
|
-
rateLimitExceededMessage: `${___CONNECTION___}:rateLimitExceededMessage`,
|
|
19
22
|
sshOptionsNotSpecified: `${___CONNECTION___}:sshOptionsNotSpecified`,
|
|
20
23
|
sshTunnelFailed: `${___CONNECTION___}:sshTunnelFailed`, // ❌ SSH tunnel failed
|
|
21
24
|
postgreConnectionError: `${___CONNECTION___}:postgreConnectionError`, // Unexpected PostgreSQL error
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { CorsConfig, initCors } from './cors/cors.js';
|
|
2
2
|
import { privateCors } from './cors/privateCors.js';
|
|
3
3
|
import { publicCors } from './cors/publicCors.js';
|
|
4
|
-
import { RateLimitConfig, setAppRateLimit, getRateLimit } from './rateLimit/rateLimit.js';
|
|
5
4
|
import { apiKeyAuthorization } from './requestAuth/auth.middleware.js';
|
|
6
5
|
import { validateQueryParams, validateBodyJson } from './zod/validate.js';
|
|
6
|
+
import { createRedisLocker, setRedisLock, removeRedisLock, checkIfLockedInRedis } from './redisLocks/redisLocker.js';
|
|
7
|
+
import { RedisLockerConfigFactory } from './redisLocks/redisLockerConfigFactory.js';
|
|
8
|
+
import { RedisLocker, RedisLockerConfig, RedisLockerConfigParams } from './redisLocks/types.js';
|
|
7
9
|
export { CorsConfig, initCors, privateCors, publicCors };
|
|
8
|
-
export { RateLimitConfig, setAppRateLimit, getRateLimit };
|
|
9
10
|
export { apiKeyAuthorization };
|
|
10
11
|
export { validateQueryParams, validateBodyJson };
|
|
12
|
+
export { createRedisLocker, setRedisLock, removeRedisLock as removeLock, checkIfLockedInRedis as isLocked };
|
|
13
|
+
export { RedisLockerConfigFactory };
|
|
14
|
+
export { RedisLocker, RedisLockerConfig, RedisLockerConfigParams };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { initCors } from './cors/cors.js';
|
|
2
2
|
import { privateCors } from './cors/privateCors.js';
|
|
3
3
|
import { publicCors } from './cors/publicCors.js';
|
|
4
|
-
import { setAppRateLimit, getRateLimit } from './rateLimit/rateLimit.js';
|
|
5
4
|
import { apiKeyAuthorization } from './requestAuth/auth.middleware.js';
|
|
6
5
|
import { validateQueryParams, validateBodyJson } from './zod/validate.js';
|
|
6
|
+
import { createRedisLocker, setRedisLock, removeRedisLock, checkIfLockedInRedis } from './redisLocks/redisLocker.js';
|
|
7
|
+
import { RedisLockerConfigFactory } from './redisLocks/redisLockerConfigFactory.js';
|
|
7
8
|
export { initCors, privateCors, publicCors };
|
|
8
|
-
export { setAppRateLimit, getRateLimit };
|
|
9
9
|
export { apiKeyAuthorization };
|
|
10
10
|
export { validateQueryParams, validateBodyJson };
|
|
11
|
+
export { createRedisLocker, setRedisLock, removeRedisLock as removeLock, checkIfLockedInRedis as isLocked };
|
|
12
|
+
export { RedisLockerConfigFactory };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RedisLocker, RedisLockerConfig } from "./types.js";
|
|
2
|
+
import { Request } from "express";
|
|
3
|
+
export declare const createRedisLocker: (config: RedisLockerConfig) => RedisLocker;
|
|
4
|
+
export declare const generateRedisLockKey: (config: RedisLockerConfig, req: Request) => string;
|
|
5
|
+
export declare const setRedisLock: (key: string, ttlSeconds: number) => Promise<void>;
|
|
6
|
+
export declare const removeRedisLock: (key: string) => Promise<void>;
|
|
7
|
+
export declare const checkIfLockedInRedis: (key: string) => Promise<boolean>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { getRedisClient } from "mielk-fn/redis";
|
|
11
|
+
import { failure, HttpResponseStatus } from "../../http/index.js";
|
|
12
|
+
import { Msg } from "../../internal/messaging/messageTags.js";
|
|
13
|
+
export const createRedisLocker = (config) => {
|
|
14
|
+
return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
|
|
15
|
+
const { message } = config;
|
|
16
|
+
const key = generateRedisLockKey(config, req);
|
|
17
|
+
const isLocked = yield checkIfLockedInRedis(key);
|
|
18
|
+
if (isLocked) {
|
|
19
|
+
return failure(res, HttpResponseStatus.LOCKED, message || Msg.apiStatus.locked);
|
|
20
|
+
}
|
|
21
|
+
return next();
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
export const generateRedisLockKey = (config, req) => {
|
|
25
|
+
const { keyPrefix, keySuffixes } = config;
|
|
26
|
+
return `${keyPrefix}:${keySuffixes(req).join(':')}`;
|
|
27
|
+
};
|
|
28
|
+
export const setRedisLock = (key, ttlSeconds) => __awaiter(void 0, void 0, void 0, function* () {
|
|
29
|
+
const redisClient = yield getRedisClient();
|
|
30
|
+
yield redisClient.set(key, JSON.stringify({ locked: true }), {
|
|
31
|
+
expiration: {
|
|
32
|
+
type: 'EX',
|
|
33
|
+
value: ttlSeconds
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
console.log('RL key:', key, ttlSeconds);
|
|
37
|
+
});
|
|
38
|
+
export const removeRedisLock = (key) => __awaiter(void 0, void 0, void 0, function* () {
|
|
39
|
+
const redisClient = yield getRedisClient();
|
|
40
|
+
yield redisClient.del(key);
|
|
41
|
+
});
|
|
42
|
+
export const checkIfLockedInRedis = (key) => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
const redisClient = yield getRedisClient();
|
|
44
|
+
const value = yield redisClient.get(key);
|
|
45
|
+
return value !== null;
|
|
46
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RedisKeyFactory } from "../../rateLimit/redisKeysFactory.js";
|
|
2
|
+
import { Msg } from "../../internal/messaging/messageTags.js";
|
|
3
|
+
const defaultRedisLockerConfig = {
|
|
4
|
+
keyPrefix: RedisKeyFactory.prefixes.lockEmail,
|
|
5
|
+
keySuffixes: RedisKeyFactory.suffixes.mail,
|
|
6
|
+
ttlSeconds: 3 * 60,
|
|
7
|
+
message: Msg.apiStatus.locked,
|
|
8
|
+
};
|
|
9
|
+
const createRedisLockerConfig = (params) => {
|
|
10
|
+
return Object.assign(Object.assign({}, defaultRedisLockerConfig), params);
|
|
11
|
+
};
|
|
12
|
+
const mail = (params) => {
|
|
13
|
+
return createRedisLockerConfig(Object.assign({}, (params || {})));
|
|
14
|
+
};
|
|
15
|
+
export const RedisLockerConfigFactory = {
|
|
16
|
+
mail,
|
|
17
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
export type RedisLocker = (req: Request, res: Response, next: NextFunction) => Promise<void | Response<Record<string, any>, Record<string, any>>>;
|
|
3
|
+
export interface RedisLockerConfigParams {
|
|
4
|
+
keyPrefix?: string;
|
|
5
|
+
keySuffixes?: (req: Request) => string[];
|
|
6
|
+
ttlSeconds?: number;
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RedisLockerConfig {
|
|
10
|
+
keyPrefix: string;
|
|
11
|
+
keySuffixes: (req: Request) => string[];
|
|
12
|
+
ttlSeconds: number;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { getRedisClient } from 'mielk-fn/redis';
|
|
11
|
+
import { setRedisLock } from '../middlewares/redisLocks/redisLocker.js';
|
|
12
|
+
export const checkRateLimit = (rateLimit) => __awaiter(void 0, void 0, void 0, function* () {
|
|
13
|
+
const { key, limit, windowSec, redisLock } = rateLimit;
|
|
14
|
+
const client = yield getRedisClient();
|
|
15
|
+
const currentCounter = yield client.incr(key);
|
|
16
|
+
if (currentCounter === 1) {
|
|
17
|
+
yield client.expire(key, windowSec);
|
|
18
|
+
}
|
|
19
|
+
const ttl = yield client.ttl(key);
|
|
20
|
+
const allowed = currentCounter <= limit;
|
|
21
|
+
if (!allowed && redisLock) {
|
|
22
|
+
const { key, ttlSeconds } = redisLock;
|
|
23
|
+
console.log(`Setting Redis lock for ${key} | ${ttlSeconds} seconds`);
|
|
24
|
+
setRedisLock(key, ttlSeconds);
|
|
25
|
+
}
|
|
26
|
+
console.log('RL key:', key, 'current:', currentCounter, 'ttl:', ttl);
|
|
27
|
+
return {
|
|
28
|
+
currentCounter,
|
|
29
|
+
limit,
|
|
30
|
+
ttl,
|
|
31
|
+
allowed
|
|
32
|
+
};
|
|
33
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { RateLimitConfig, RateLimitConfigParams } from './types.js';
|
|
3
|
+
export declare const createGlobalRateLimiter: (config?: RateLimitConfigParams) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<Record<string, any>, Record<string, any>>>;
|
|
4
|
+
export declare const createRateLimiter: (config: RateLimitConfig) => (req: Request, res: Response, next: NextFunction) => Promise<void | Response<Record<string, any>, Record<string, any>>>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { checkRateLimit } from './checkRateLimit.js';
|
|
11
|
+
import { failure, HttpResponseStatus } from '../http/index.js';
|
|
12
|
+
import { Msg } from '../internal/messaging/messageTags.js';
|
|
13
|
+
import { RateLimitConfigFactory } from './rateLimitConfigFactory.js';
|
|
14
|
+
import { generateRedisLockKey } from '../middlewares/redisLocks/redisLocker.js';
|
|
15
|
+
export const createGlobalRateLimiter = (config) => {
|
|
16
|
+
return createRateLimiter(RateLimitConfigFactory.globalIp(config));
|
|
17
|
+
};
|
|
18
|
+
export const createRateLimiter = (config) => {
|
|
19
|
+
return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
|
+
const { message } = config;
|
|
21
|
+
const rateLimit = createRateLimitFromConfig(config, req);
|
|
22
|
+
const { limit, currentCounter, ttl, allowed } = yield checkRateLimit(rateLimit);
|
|
23
|
+
res.setHeader('X-RateLimit-Limit', limit);
|
|
24
|
+
res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - currentCounter));
|
|
25
|
+
res.setHeader('X-RateLimit-Reset', ttl);
|
|
26
|
+
console.log('IP:', req.ip);
|
|
27
|
+
console.log('Forwarded:', req.headers['x-forwarded-for']);
|
|
28
|
+
if (!allowed) {
|
|
29
|
+
return failure(res, HttpResponseStatus.TOO_MANY_REQUESTS, message || Msg.apiStatus.tooManyRequests);
|
|
30
|
+
}
|
|
31
|
+
return next();
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const createRateLimitFromConfig = (config, req) => {
|
|
35
|
+
const { limit, windowSec, message, redisLockerConfig } = config;
|
|
36
|
+
const key = generateKeyFromConfig(config, req);
|
|
37
|
+
const rateLimit = { key, limit, windowSec, message };
|
|
38
|
+
if (redisLockerConfig) {
|
|
39
|
+
const { ttlSeconds } = redisLockerConfig;
|
|
40
|
+
const key = generateRedisLockKey(redisLockerConfig, req);
|
|
41
|
+
const redisLock = { key, ttlSeconds };
|
|
42
|
+
rateLimit.redisLock = redisLock;
|
|
43
|
+
}
|
|
44
|
+
return rateLimit;
|
|
45
|
+
};
|
|
46
|
+
const generateKeyFromConfig = (config, req) => {
|
|
47
|
+
const { keyPrefix, keySuffixes } = config;
|
|
48
|
+
return `${keyPrefix}:${keySuffixes(req).join(':')}`;
|
|
49
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { checkRateLimit } from './checkRateLimit.js';
|
|
2
|
+
import { createRateLimiter } from './createRateLimiter.js';
|
|
3
|
+
import { RateLimitConfigFactory } from './rateLimitConfigFactory.js';
|
|
4
|
+
import { RedisKeyFactory } from './redisKeysFactory.js';
|
|
5
|
+
import { type RateLimiter, RateLimitConfig, RateLimit, RateLimitCheck } from './types.js';
|
|
6
|
+
export { checkRateLimit, createRateLimiter, RateLimitConfigFactory, RedisKeyFactory };
|
|
7
|
+
export { RateLimitConfig, RateLimit, RateLimitCheck };
|
|
8
|
+
export type { RateLimiter };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { checkRateLimit } from './checkRateLimit.js';
|
|
2
|
+
import { createRateLimiter } from './createRateLimiter.js';
|
|
3
|
+
import { RateLimitConfigFactory } from './rateLimitConfigFactory.js';
|
|
4
|
+
import { RedisKeyFactory } from './redisKeysFactory.js';
|
|
5
|
+
export { checkRateLimit, createRateLimiter, RateLimitConfigFactory, RedisKeyFactory };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { RateLimitConfig, RateLimitConfigParams } from "./types.js";
|
|
2
|
+
export declare const RateLimitConfigFactory: {
|
|
3
|
+
globalIp: (params?: RateLimitConfigParams) => RateLimitConfig;
|
|
4
|
+
loginIp: (params?: RateLimitConfigParams) => RateLimitConfig;
|
|
5
|
+
loginIpMail: (params?: RateLimitConfigParams) => RateLimitConfig;
|
|
6
|
+
loginMail: (params?: RateLimitConfigParams) => RateLimitConfig;
|
|
7
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RedisKeyFactory } from "./redisKeysFactory.js";
|
|
2
|
+
import { Msg } from "../internal/messaging/messageTags.js";
|
|
3
|
+
import { RedisLockerConfigFactory } from "../middlewares/index.js";
|
|
4
|
+
const defaultRateLimitConfig = {
|
|
5
|
+
keyPrefix: RedisKeyFactory.prefixes.login,
|
|
6
|
+
keySuffixes: RedisKeyFactory.suffixes.ip,
|
|
7
|
+
windowSec: 15 * 60,
|
|
8
|
+
limit: 5,
|
|
9
|
+
message: Msg.apiStatus.tooManyRequests,
|
|
10
|
+
};
|
|
11
|
+
const createRateLimitConfig = (params) => {
|
|
12
|
+
return Object.assign(Object.assign({}, defaultRateLimitConfig), params);
|
|
13
|
+
};
|
|
14
|
+
const globalIp = (params) => {
|
|
15
|
+
return createRateLimitConfig(Object.assign({ keyPrefix: RedisKeyFactory.prefixes.global, windowSec: 60, limit: 2 }, (params || {})));
|
|
16
|
+
};
|
|
17
|
+
const loginIp = (params) => {
|
|
18
|
+
return createRateLimitConfig(Object.assign({}, (params || {})));
|
|
19
|
+
};
|
|
20
|
+
const loginIpMail = (params) => {
|
|
21
|
+
return createRateLimitConfig(Object.assign({ keySuffixes: RedisKeyFactory.suffixes.ipMail }, (params || {})));
|
|
22
|
+
};
|
|
23
|
+
const loginMail = (params) => {
|
|
24
|
+
return createRateLimitConfig(Object.assign({ keySuffixes: RedisKeyFactory.suffixes.mail, redisLockerConfig: RedisLockerConfigFactory.mail() }, (params || {})));
|
|
25
|
+
};
|
|
26
|
+
export const RateLimitConfigFactory = {
|
|
27
|
+
globalIp,
|
|
28
|
+
loginIp,
|
|
29
|
+
loginIpMail,
|
|
30
|
+
loginMail
|
|
31
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Request } from 'express';
|
|
2
|
+
export declare const RedisKeyFactory: {
|
|
3
|
+
prefixes: {
|
|
4
|
+
readonly global: "global";
|
|
5
|
+
readonly login: "login";
|
|
6
|
+
readonly lockEmail: "lock:email";
|
|
7
|
+
readonly refresh: "refresh";
|
|
8
|
+
};
|
|
9
|
+
suffixes: {
|
|
10
|
+
readonly ip: (req: Request) => string[];
|
|
11
|
+
readonly ipMail: (req: Request) => string[];
|
|
12
|
+
readonly mail: (req: Request) => string[];
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const prefixes = {
|
|
2
|
+
global: 'global',
|
|
3
|
+
login: 'login',
|
|
4
|
+
lockEmail: 'lock:email',
|
|
5
|
+
refresh: 'refresh',
|
|
6
|
+
};
|
|
7
|
+
const getIpSuffixCallback = (req) => {
|
|
8
|
+
const ip = req.ip || 'undefined';
|
|
9
|
+
return [ip];
|
|
10
|
+
};
|
|
11
|
+
const getEmailSuffixCallback = (req) => {
|
|
12
|
+
var _a;
|
|
13
|
+
const email = (((_a = req.body) === null || _a === void 0 ? void 0 : _a.email) || 'unknown').toLowerCase();
|
|
14
|
+
return [email];
|
|
15
|
+
};
|
|
16
|
+
const getIpEmailSuffixCallback = (req) => {
|
|
17
|
+
var _a;
|
|
18
|
+
const email = (((_a = req.body) === null || _a === void 0 ? void 0 : _a.email) || 'unknown').toLowerCase();
|
|
19
|
+
const ip = req.ip || 'unknown';
|
|
20
|
+
return [ip, email];
|
|
21
|
+
};
|
|
22
|
+
const suffixCallbacks = {
|
|
23
|
+
'ip': getIpSuffixCallback,
|
|
24
|
+
'ipMail': getIpEmailSuffixCallback,
|
|
25
|
+
'mail': getEmailSuffixCallback,
|
|
26
|
+
};
|
|
27
|
+
export const RedisKeyFactory = {
|
|
28
|
+
prefixes: prefixes,
|
|
29
|
+
suffixes: suffixCallbacks
|
|
30
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { RedisLockerConfig } from "../middlewares/index.js";
|
|
3
|
+
export type RateLimiter = (req: Request, res: Response, next: NextFunction) => Promise<void | Response<Record<string, any>, Record<string, any>>>;
|
|
4
|
+
export type RateLimitConfigParams = {
|
|
5
|
+
keyPrefix?: string;
|
|
6
|
+
keySuffixes?: (req: Request) => string[];
|
|
7
|
+
windowSec?: number;
|
|
8
|
+
limit?: number;
|
|
9
|
+
message?: string;
|
|
10
|
+
redisLockerConfig?: RedisLockerConfig | null;
|
|
11
|
+
};
|
|
12
|
+
export interface RateLimitConfig {
|
|
13
|
+
keyPrefix: string;
|
|
14
|
+
keySuffixes: (req: Request) => string[];
|
|
15
|
+
limit: number;
|
|
16
|
+
windowSec: number;
|
|
17
|
+
message?: string;
|
|
18
|
+
redisLockerConfig?: RedisLockerConfig | null;
|
|
19
|
+
}
|
|
20
|
+
export interface RateLimit {
|
|
21
|
+
key: string;
|
|
22
|
+
limit: number;
|
|
23
|
+
windowSec: number;
|
|
24
|
+
message?: string;
|
|
25
|
+
redisLock?: {
|
|
26
|
+
key: string;
|
|
27
|
+
ttlSeconds: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface RateLimitCheck {
|
|
31
|
+
currentCounter: number;
|
|
32
|
+
limit: number;
|
|
33
|
+
ttl: number;
|
|
34
|
+
allowed: boolean;
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { PostgreDbConfig } from '../db/pg/index.js';
|
|
2
2
|
import { MsSqlDbConfig } from '../db/mssql/index.js';
|
|
3
|
-
import { CorsConfig
|
|
3
|
+
import { CorsConfig } from '../middlewares/index.js';
|
|
4
4
|
import { DbProvider } from './DbProvider.js';
|
|
5
|
+
import { RateLimitConfigParams } from '../rateLimit/types.js';
|
|
5
6
|
export type DbConfig = PostgreDbConfig | MsSqlDbConfig;
|
|
6
|
-
export declare const createExpressApp: (isProd: boolean, dbConfig: DbConfig, corsConfig: CorsConfig, rateLimitConfig?:
|
|
7
|
+
export declare const createExpressApp: (isProd: boolean, dbConfig: DbConfig, corsConfig: CorsConfig, rateLimitConfig?: RateLimitConfigParams) => Promise<import("express-serve-static-core").Express>;
|
|
7
8
|
export declare const isProd: () => boolean;
|
|
8
9
|
export declare const getDbProvider: () => DbProvider;
|
package/dist/routing/express.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
1
10
|
import express from 'express';
|
|
2
11
|
import { initDb as postgreInitDb } from '../db/pg/index.js';
|
|
3
12
|
import { initDb as msSqlInitDb } from '../db/mssql/index.js';
|
|
4
|
-
import { initCors
|
|
13
|
+
import { initCors } from '../middlewares/index.js';
|
|
14
|
+
import { createGlobalRateLimiter } from '../rateLimit/createRateLimiter.js';
|
|
5
15
|
const env = {
|
|
6
16
|
isProd: true,
|
|
7
17
|
provider: undefined
|
|
8
18
|
};
|
|
9
|
-
export const createExpressApp = (isProd, dbConfig, corsConfig, rateLimitConfig) => {
|
|
19
|
+
export const createExpressApp = (isProd, dbConfig, corsConfig, rateLimitConfig) => __awaiter(void 0, void 0, void 0, function* () {
|
|
10
20
|
const app = express();
|
|
11
21
|
env.isProd = isProd;
|
|
12
22
|
env.provider = dbConfig.provider;
|
|
@@ -19,9 +29,11 @@ export const createExpressApp = (isProd, dbConfig, corsConfig, rateLimitConfig)
|
|
|
19
29
|
break;
|
|
20
30
|
}
|
|
21
31
|
initCors(corsConfig);
|
|
22
|
-
|
|
32
|
+
app.use(createGlobalRateLimiter(rateLimitConfig));
|
|
23
33
|
app.use(express.json());
|
|
34
|
+
if (isProd)
|
|
35
|
+
app.set('trust proxy', 1);
|
|
24
36
|
return app;
|
|
25
|
-
};
|
|
37
|
+
});
|
|
26
38
|
export const isProd = () => env.isProd;
|
|
27
39
|
export const getDbProvider = () => env.provider;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mielk-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"author": "mielk",
|
|
6
6
|
"description": "Wrapper for API operations",
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"types": "./dist/middlewares/index.d.ts",
|
|
33
33
|
"import": "./dist/middlewares/index.js",
|
|
34
34
|
"default": "./dist/middlewares/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./rate-limit": {
|
|
37
|
+
"types": "./dist/rateLimit/index.d.ts",
|
|
38
|
+
"import": "./dist/rateLimit/index.js",
|
|
39
|
+
"default": "./dist/rateLimit/index.js"
|
|
35
40
|
}
|
|
36
41
|
},
|
|
37
42
|
"scripts": {
|
|
@@ -71,7 +76,6 @@
|
|
|
71
76
|
],
|
|
72
77
|
"peerDependencies": {
|
|
73
78
|
"express": "^5",
|
|
74
|
-
"express-rate-limit": "^8",
|
|
75
79
|
"zod": "^4"
|
|
76
80
|
},
|
|
77
81
|
"optionalDependencies": {
|
|
@@ -79,7 +83,8 @@
|
|
|
79
83
|
},
|
|
80
84
|
"dependencies": {
|
|
81
85
|
"cors": "^2.8.6",
|
|
82
|
-
"
|
|
86
|
+
"express-rate-limit": "^8.3.1",
|
|
87
|
+
"mielk-fn": "^1.2.0",
|
|
83
88
|
"pg": "^8.20.0",
|
|
84
89
|
"tunnel-ssh": "^5.2.0"
|
|
85
90
|
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { Express } from 'express';
|
|
2
|
-
import { RateLimitRequestHandler } from 'express-rate-limit';
|
|
3
|
-
export interface RateLimitConfig {
|
|
4
|
-
windowsMs?: number;
|
|
5
|
-
max?: number;
|
|
6
|
-
}
|
|
7
|
-
export declare const setAppRateLimit: (app: Express, customConfig?: RateLimitConfig) => void;
|
|
8
|
-
export declare const getRateLimit: (minutes: number, maxAttempts: number, message?: string) => RateLimitRequestHandler;
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import rateLimit from 'express-rate-limit';
|
|
2
|
-
import { Msg } from '../../internal/messaging/messageTags.js';
|
|
3
|
-
const rateLimitConfig = {
|
|
4
|
-
windowMs: 60 * 1000,
|
|
5
|
-
max: 100,
|
|
6
|
-
};
|
|
7
|
-
export const setAppRateLimit = (app, customConfig) => {
|
|
8
|
-
const config = Object.assign(Object.assign({}, rateLimitConfig), customConfig);
|
|
9
|
-
const limiter = rateLimit(config);
|
|
10
|
-
app.use(limiter);
|
|
11
|
-
};
|
|
12
|
-
export const getRateLimit = (minutes, maxAttempts, message) => {
|
|
13
|
-
return rateLimit({
|
|
14
|
-
windowMs: minutes * 60 * 1000,
|
|
15
|
-
max: maxAttempts,
|
|
16
|
-
message: message || Msg.connection.rateLimitExceededMessage
|
|
17
|
-
});
|
|
18
|
-
};
|