2
0
Эх сурвалжийг харах

Merge pull request #10376 from growilabs/support/156162-172235-rate-limiter-feature-biome

support: Configure biome for rate-limiter feature
Yuki Takei 6 сар өмнө
parent
commit
480c0b1c45

+ 1 - 0
apps/app/.eslintrc.js

@@ -41,6 +41,7 @@ module.exports = {
     'src/features/page-bulk-export/**',
     'src/features/growi-plugin/**',
     'src/features/opentelemetry/**',
+    'src/features/rate-limiter/**',
     'src/stores-universal/**',
     'src/interfaces/**',
     'src/utils/**',

+ 14 - 12
apps/app/src/features/rate-limiter/config/index.ts

@@ -1,11 +1,11 @@
 export type IApiRateLimitConfig = {
-  method: string,
-  maxRequests: number,
-  usersPerIpProspection?: number,
-}
+  method: string;
+  maxRequests: number;
+  usersPerIpProspection?: number;
+};
 export type IApiRateLimitEndpointMap = {
-  [endpoint: string]: IApiRateLimitConfig
-}
+  [endpoint: string]: IApiRateLimitConfig;
+};
 
 export const DEFAULT_MAX_REQUESTS = 500;
 export const DEFAULT_DURATION_SEC = 60;
@@ -59,12 +59,14 @@ export const defaultConfig: IApiRateLimitEndpointMap = {
 };
 
 const isDev = process.env.NODE_ENV === 'development';
-const defaultConfigWithRegExpForDev: IApiRateLimitEndpointMap = isDev ? {
-  '/__nextjs_original-stack-frame': {
-    method: 'GET',
-    maxRequests: Infinity,
-  },
-} : {};
+const defaultConfigWithRegExpForDev: IApiRateLimitEndpointMap = isDev
+  ? {
+      '/__nextjs_original-stack-frame': {
+        method: 'GET',
+        maxRequests: Infinity,
+      },
+    }
+  : {};
 
 // default config with reg exp
 export const defaultConfigWithRegExp: IApiRateLimitEndpointMap = {

+ 10 - 8
apps/app/src/features/rate-limiter/middleware/consume-points.integ.ts

@@ -1,6 +1,10 @@
 import { faker } from '@faker-js/faker';
 
-const testRateLimitErrorWhenExceedingMaxRequests = async(method: string, key: string, maxRequests: number): Promise<void> => {
+const testRateLimitErrorWhenExceedingMaxRequests = async (
+  method: string,
+  key: string,
+  maxRequests: number,
+): Promise<void> => {
   // dynamic import is used because rateLimiterMongo needs to be initialized after connecting to DB
   // Issue: https://github.com/animir/node-rate-limiter-flexible/issues/216
   const { consumePoints } = await import('./consume-points');
@@ -20,8 +24,7 @@ const testRateLimitErrorWhenExceedingMaxRequests = async(method: string, key: st
         throw new Error('Exception occurred');
       }
     }
-  }
-  catch (err) {
+  } catch (err) {
     // Expect rate limit error to be called
     expect(err.message).not.toBe('Exception occurred');
     // Expect rate limit error at maxRequest + 1
@@ -29,9 +32,8 @@ const testRateLimitErrorWhenExceedingMaxRequests = async(method: string, key: st
   }
 };
 
-
-describe('consume-points.ts', async() => {
-  it('Should trigger a rate limit error when maxRequest is exceeded (maxRequest: 1)', async() => {
+describe('consume-points.ts', async () => {
+  it('Should trigger a rate limit error when maxRequest is exceeded (maxRequest: 1)', async () => {
     // setup
     const method = 'GET';
     const key = 'test-key-1';
@@ -40,7 +42,7 @@ describe('consume-points.ts', async() => {
     await testRateLimitErrorWhenExceedingMaxRequests(method, key, maxRequests);
   });
 
-  it('Should trigger a rate limit error when maxRequest is exceeded (maxRequest: 500)', async() => {
+  it('Should trigger a rate limit error when maxRequest is exceeded (maxRequest: 500)', async () => {
     // setup
     const method = 'GET';
     const key = 'test-key-2';
@@ -49,7 +51,7 @@ describe('consume-points.ts', async() => {
     await testRateLimitErrorWhenExceedingMaxRequests(method, key, maxRequests);
   });
 
-  it('Should trigger a rate limit error when maxRequest is exceeded (maxRequest: {random integer between 1 and 1000})', async() => {
+  it('Should trigger a rate limit error when maxRequest is exceeded (maxRequest: {random integer between 1 and 1000})', async () => {
     // setup
     const method = 'GET';
     const key = 'test-key-3';

+ 15 - 5
apps/app/src/features/rate-limiter/middleware/consume-points.ts

@@ -1,11 +1,14 @@
-import { type RateLimiterRes } from 'rate-limiter-flexible';
+import type { RateLimiterRes } from 'rate-limiter-flexible';
 
 import { DEFAULT_MAX_REQUESTS, type IApiRateLimitConfig } from '../config';
 
 import { rateLimiterFactory } from './rate-limiter-factory';
 
-export const consumePoints = async(
-    method: string, key: string | null, customizedConfig?: IApiRateLimitConfig, maxRequestsMultiplier?: number,
+export const consumePoints = async (
+  method: string,
+  key: string | null,
+  customizedConfig?: IApiRateLimitConfig,
+  maxRequestsMultiplier?: number,
 ): Promise<RateLimiterRes | undefined> => {
   if (key == null) {
     return;
@@ -14,7 +17,11 @@ export const consumePoints = async(
   let maxRequests = DEFAULT_MAX_REQUESTS;
 
   // use customizedConfig
-  if (customizedConfig != null && (customizedConfig.method.includes(method) || customizedConfig.method === 'ALL')) {
+  if (
+    customizedConfig != null &&
+    (customizedConfig.method.includes(method) ||
+      customizedConfig.method === 'ALL')
+  ) {
     maxRequests = customizedConfig.maxRequests;
   }
 
@@ -23,7 +30,10 @@ export const consumePoints = async(
     maxRequests *= maxRequestsMultiplier;
   }
 
-  const rateLimiter = rateLimiterFactory.getOrCreateRateLimiter(key, maxRequests);
+  const rateLimiter = rateLimiterFactory.getOrCreateRateLimiter(
+    key,
+    maxRequests,
+  );
 
   const pointsToConsume = 1;
   const rateLimiterRes = await rateLimiter.consume(key, pointsToConsume);

+ 26 - 22
apps/app/src/features/rate-limiter/middleware/factory.ts

@@ -1,11 +1,14 @@
 import type { IUserHasId } from '@growi/core';
 import type { Handler, Request } from 'express';
 import md5 from 'md5';
-import { type RateLimiterRes } from 'rate-limiter-flexible';
+import type { RateLimiterRes } from 'rate-limiter-flexible';
 
 import loggerFactory from '~/utils/logger';
 
-import { DEFAULT_USERS_PER_IP_PROSPECTION, type IApiRateLimitConfig } from '../config';
+import {
+  DEFAULT_USERS_PER_IP_PROSPECTION,
+  type IApiRateLimitConfig,
+} from '../config';
 import { generateApiRateLimitConfig } from '../utils/config-generator';
 
 import { consumePoints } from './consume-points';
@@ -22,10 +25,11 @@ const apiRateLimitConfig = generateApiRateLimitConfig();
 const configWithoutRegExp = apiRateLimitConfig.withoutRegExp;
 const configWithRegExp = apiRateLimitConfig.withRegExp;
 const allRegExp = new RegExp(Object.keys(configWithRegExp).join('|'));
-const keysWithRegExp = Object.keys(configWithRegExp).map(key => new RegExp(`^${key}`));
+const keysWithRegExp = Object.keys(configWithRegExp).map(
+  (key) => new RegExp(`^${key}`),
+);
 const valuesWithRegExp = Object.values(configWithRegExp);
 
-
 /**
  * consume per user per endpoint
  * @param method
@@ -33,8 +37,10 @@ const valuesWithRegExp = Object.values(configWithRegExp);
  * @param customizedConfig
  * @returns
  */
-const consumePointsByUser = async(
-    method: string, key: string | null, customizedConfig?: IApiRateLimitConfig,
+const consumePointsByUser = async (
+  method: string,
+  key: string | null,
+  customizedConfig?: IApiRateLimitConfig,
 ): Promise<RateLimiterRes | undefined> => {
   return consumePoints(method, key, customizedConfig);
 };
@@ -46,24 +52,25 @@ const consumePointsByUser = async(
  * @param customizedConfig
  * @returns
  */
-const consumePointsByIp = async(
-    method: string, key: string | null, customizedConfig?: IApiRateLimitConfig,
+const consumePointsByIp = async (
+  method: string,
+  key: string | null,
+  customizedConfig?: IApiRateLimitConfig,
 ): Promise<RateLimiterRes | undefined> => {
-  const maxRequestsMultiplier = customizedConfig?.usersPerIpProspection ?? DEFAULT_USERS_PER_IP_PROSPECTION;
+  const maxRequestsMultiplier =
+    customizedConfig?.usersPerIpProspection ?? DEFAULT_USERS_PER_IP_PROSPECTION;
   return consumePoints(method, key, customizedConfig, maxRequestsMultiplier);
 };
 
-
 export const middlewareFactory = (): Handler => {
-
-  return async(req: Request & { user?: IUserHasId }, res, next) => {
-
+  return async (req: Request & { user?: IUserHasId }, res, next) => {
     const endpoint = req.path;
 
     // determine keys
-    const keyForUser: string | null = req.user != null
-      ? md5(`${req.user._id}_${endpoint}_${req.method}`)
-      : null;
+    const keyForUser: string | null =
+      req.user != null
+        ? md5(`${req.user._id}_${endpoint}_${req.method}`)
+        : null;
     const keyForIp: string = md5(`${req.ip}_${endpoint}_${req.method}`);
 
     // determine customized config
@@ -71,8 +78,7 @@ export const middlewareFactory = (): Handler => {
     const configForEndpoint = configWithoutRegExp[endpoint];
     if (configForEndpoint) {
       customizedConfig = configForEndpoint;
-    }
-    else if (allRegExp.test(endpoint)) {
+    } else if (allRegExp.test(endpoint)) {
       keysWithRegExp.forEach((key, index) => {
         if (key.test(endpoint)) {
           customizedConfig = valuesWithRegExp[index];
@@ -84,8 +90,7 @@ export const middlewareFactory = (): Handler => {
     if (req.user != null) {
       try {
         await consumePointsByUser(req.method, keyForUser, customizedConfig);
-      }
-      catch {
+      } catch {
         logger.error(`${req.user._id}: too many request at ${endpoint}`);
         return res.sendStatus(429);
       }
@@ -94,8 +99,7 @@ export const middlewareFactory = (): Handler => {
     // check for ip
     try {
       await consumePointsByIp(req.method, keyForIp, customizedConfig);
-    }
-    catch {
+    } catch {
       logger.error(`${req.ip}: too many request at ${endpoint}`);
       return res.sendStatus(429);
     }

+ 4 - 3
apps/app/src/features/rate-limiter/middleware/rate-limiter-factory.ts

@@ -1,10 +1,12 @@
 import { connection } from 'mongoose';
-import { type IRateLimiterMongoOptions, RateLimiterMongo } from 'rate-limiter-flexible';
+import {
+  type IRateLimiterMongoOptions,
+  RateLimiterMongo,
+} from 'rate-limiter-flexible';
 
 import { DEFAULT_DURATION_SEC } from '../config';
 
 class RateLimiterFactory {
-
   private rateLimiters: Map<string, RateLimiterMongo> = new Map();
 
   getOrCreateRateLimiter(key: string, maxRequests: number): RateLimiterMongo {
@@ -24,7 +26,6 @@ class RateLimiterFactory {
 
     return rateLimiter;
   }
-
 }
 
 export const rateLimiterFactory = new RateLimiterFactory();

+ 30 - 17
apps/app/src/features/rate-limiter/utils/config-generator.ts

@@ -1,18 +1,21 @@
 import type { IApiRateLimitEndpointMap } from '../config';
-import {
-  defaultConfig, defaultConfigWithRegExp,
-} from '../config';
+import { defaultConfig, defaultConfigWithRegExp } from '../config';
 
 const envVar = process.env;
 
 // https://regex101.com/r/aNDjmI/1
 const regExp = /^API_RATE_LIMIT_(\w+)_ENDPOINT(_WITH_REGEXP)?$/;
 
-const generateApiRateLimitConfigFromEndpoint = (envVar: NodeJS.ProcessEnv, targets: string[], withRegExp: boolean): IApiRateLimitEndpointMap => {
+const generateApiRateLimitConfigFromEndpoint = (
+  envVar: NodeJS.ProcessEnv,
+  targets: string[],
+  withRegExp: boolean,
+): IApiRateLimitEndpointMap => {
   const apiRateLimitConfig: IApiRateLimitEndpointMap = {};
   targets.forEach((target) => {
-
-    const endpointKey = withRegExp ? `API_RATE_LIMIT_${target}_ENDPOINT_WITH_REGEXP` : `API_RATE_LIMIT_${target}_ENDPOINT`;
+    const endpointKey = withRegExp
+      ? `API_RATE_LIMIT_${target}_ENDPOINT_WITH_REGEXP`
+      : `API_RATE_LIMIT_${target}_ENDPOINT`;
 
     const endpoint = envVar[endpointKey];
 
@@ -43,26 +46,26 @@ const generateApiRateLimitConfigFromEndpoint = (envVar: NodeJS.ProcessEnv, targe
 };
 
 type ApiRateLimitConfigResult = {
-  'withoutRegExp': IApiRateLimitEndpointMap,
-  'withRegExp': IApiRateLimitEndpointMap
-}
+  withoutRegExp: IApiRateLimitEndpointMap;
+  withRegExp: IApiRateLimitEndpointMap;
+};
 
 export const generateApiRateLimitConfig = (): ApiRateLimitConfigResult => {
-
   const apiRateConfigTargets: string[] = [];
   const apiRateConfigTargetsWithRegExp: string[] = [];
   Object.keys(envVar).forEach((key) => {
     const result = key.match(regExp);
 
-    if (result == null) { return null }
+    if (result == null) {
+      return null;
+    }
 
     const target = result[1];
     const isWithRegExp = result[2] != null;
 
     if (isWithRegExp) {
       apiRateConfigTargetsWithRegExp.push(target);
-    }
-    else {
+    } else {
       apiRateConfigTargets.push(target);
     }
   });
@@ -72,17 +75,27 @@ export const generateApiRateLimitConfig = (): ApiRateLimitConfigResult => {
   apiRateConfigTargetsWithRegExp.sort();
 
   // get config
-  const apiRateLimitConfig = generateApiRateLimitConfigFromEndpoint(envVar, apiRateConfigTargets, false);
-  const apiRateLimitConfigWithRegExp = generateApiRateLimitConfigFromEndpoint(envVar, apiRateConfigTargetsWithRegExp, true);
+  const apiRateLimitConfig = generateApiRateLimitConfigFromEndpoint(
+    envVar,
+    apiRateConfigTargets,
+    false,
+  );
+  const apiRateLimitConfigWithRegExp = generateApiRateLimitConfigFromEndpoint(
+    envVar,
+    apiRateConfigTargetsWithRegExp,
+    true,
+  );
 
   const config = { ...defaultConfig, ...apiRateLimitConfig };
-  const configWithRegExp = { ...defaultConfigWithRegExp, ...apiRateLimitConfigWithRegExp };
+  const configWithRegExp = {
+    ...defaultConfigWithRegExp,
+    ...apiRateLimitConfigWithRegExp,
+  };
 
   const result: ApiRateLimitConfigResult = {
     withoutRegExp: config,
     withRegExp: configWithRegExp,
   };
 
-
   return result;
 };

+ 0 - 1
biome.json

@@ -31,7 +31,6 @@
       "!apps/app/src/client/**",
       "!apps/app/src/components/**",
       "!apps/app/src/features/openai/**",
-      "!apps/app/src/features/rate-limiter/**",
       "!apps/app/src/models/**",
       "!apps/app/src/pages/**",
       "!apps/app/src/server/**",