@adaptivestone/framework 5.0.0-alpha.1 → 5.0.0-alpha.10

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/CHANGELOG.md CHANGED
@@ -1,3 +1,49 @@
1
+ ### 5.0.0-alpha.10
2
+
3
+ [UPDATE] update deps
4
+ [NEW] IpDetector middleware that support detecting proxy and X-Forwarded-For header
5
+ [BREAKING] RateLimiter now need to have IpDetector middleware before
6
+
7
+ ### 5.0.0-alpha.9
8
+
9
+ [UPDATE] update deps
10
+ [BREAKING] removing staticFiles middleware as it not used in projects anymore. Docs with nginx config will be provided
11
+ [BREAKING] remove default AUTH_SALT. It should be provided on a app level now
12
+ [BREAKING] Vitest 2.0.0 https://vitest.dev/guide/migration.html#migrating-to-vitest-2-0
13
+
14
+ ### 5.0.0-alpha.8
15
+
16
+ [UPDATE] replace dotenv with loadEnvFile
17
+ [UPDATE] replace nodemon with node --watch (dev only)
18
+ [BREAKING] Minimum node version is 20.12 as for now (process.loadEnvFile)
19
+
20
+ ### 5.0.0-alpha.7
21
+
22
+ [UPDATE] deps update
23
+
24
+ ### 5.0.0-alpha.6
25
+
26
+ [UPDATE] Update internal documentation (jsdoc, d.ts)
27
+
28
+ ### 5.0.0-alpha.5
29
+
30
+ [UPDATE] More verbose errors for rapsing body request.
31
+ [UPDATE] deps update
32
+
33
+ ### 5.0.0-alpha.4
34
+
35
+ [UPDATE] Update rate-limiter-flexible to v5
36
+ [CHANGE] Cache update redis.setEX to redis.set(..,..,{EX:xx}) as setEX deprecated
37
+
38
+ ### 5.0.0-alpha.3
39
+
40
+ [UPDATE] deps update
41
+ [FIX] Migration commands apply
42
+
43
+ ### 5.0.0-alpha.2
44
+
45
+ [UPDATE] deps update
46
+
1
47
  ### 5.0.0-alpha.1
2
48
 
3
49
  [BREAKING] Vitest 1.0.0 https://vitest.dev/guide/migration.html#migrating-from-vitest-0-34-6
@@ -60,7 +60,9 @@ class CreateUser extends AbstractCommand {
60
60
 
61
61
  await user.generateToken();
62
62
 
63
- this.logger.info(`User was created/updated ${JSON.stringify(user, 0, 4)}`);
63
+ this.logger.info(
64
+ `User was created/updated ${JSON.stringify(user, null, 4)}`,
65
+ );
64
66
 
65
67
  return user;
66
68
  }
@@ -36,7 +36,7 @@ class Migrate extends AbstractCommand {
36
36
  for (const migration of migrations) {
37
37
  this.logger.info(`=== Start migration ${migration.file} ===`);
38
38
  // eslint-disable-next-line no-await-in-loop
39
- const MigrationCommand = await import(migration.path);
39
+ const { default: MigrationCommand } = await import(migration.path);
40
40
  const migrationCommand = new MigrationCommand(this.app);
41
41
  // eslint-disable-next-line no-await-in-loop
42
42
  await migrationCommand.up();
package/config/auth.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export default {
2
2
  hashRounds: 64,
3
- saltSecret: process.env.AUTH_SALT || 'gdfg45667_%%^trterte',
3
+ saltSecret:
4
+ process.env.AUTH_SALT || console.error('AUTH_SALT is not defined'),
4
5
  isAuthWithVefificationFlow: true,
5
6
  };
@@ -0,0 +1,14 @@
1
+ export default {
2
+ headers: ['X-Forwarded-For'],
3
+ trustedProxy: [
4
+ // list of trusted proxies.
5
+ '169.254.0.0/16', // linklocal
6
+ 'fe80::/10', // linklocal
7
+ '127.0.0.1/8', // loopback
8
+ '::1/128', // loopback
9
+ '10.0.0.0/8', // uniquelocal
10
+ '172.16.0.0/12', // uniquelocal
11
+ '192.168.0.0/16', // uniquelocal
12
+ 'fc00::/7', // uniquelocal
13
+ ],
14
+ };
package/folderConfig.js CHANGED
@@ -6,7 +6,6 @@ export default {
6
6
  models: path.resolve('./models'),
7
7
  controllers: path.resolve('./controllers'),
8
8
  views: path.resolve('./views'),
9
- public: path.resolve('./public'),
10
9
  locales: path.resolve('./locales'),
11
10
  emails: path.resolve('./services/messaging/email/templates'),
12
11
  commands: path.resolve('./commands'),
package/jsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "node16",
4
+ "target": "ES2022",
5
+ "moduleResolution": "node16",
6
+ "checkJs": true
7
+ },
8
+ "exclude": ["node_modules"]
9
+ }
@@ -13,10 +13,11 @@ class AbstractCommand extends Base {
13
13
 
14
14
  /**
15
15
  * Entry point to every command. This method should be overridden
16
- * @override
16
+ * @return {Promise<boolean>} resut
17
17
  */
18
18
  async run() {
19
19
  this.logger.error('You should implement run method');
20
+ return false;
20
21
  }
21
22
 
22
23
  static get loggerGroup() {
@@ -25,10 +25,12 @@ class AbstractController extends Base {
25
25
  const { routes } = this;
26
26
  let httpPath = this.getHttpPath();
27
27
 
28
+ // @ts-ignore
28
29
  if (this.getExpressPath) {
29
30
  this.logger.warn(
30
31
  `getExpressPath deprecated. Please use getHttpPath instead. Will be removed on v5`,
31
32
  );
33
+ // @ts-ignore
32
34
  httpPath = this.getExpressPath();
33
35
  }
34
36
 
@@ -58,6 +60,7 @@ class AbstractController extends Base {
58
60
  httpPath,
59
61
  );
60
62
  const middlewaresInfo = this.parseMiddlewares(
63
+ // @ts-ignore
61
64
  this.constructor.middleware,
62
65
  httpPath,
63
66
  );
@@ -370,6 +373,7 @@ class AbstractController extends Base {
370
373
  * You should provide path relative to controller and then array of middlewares to apply.
371
374
  * Order is matter.
372
375
  * Be default path apply to ANY' method, but you can preattach 'METHOD' into patch to scope patch to this METHOD
376
+ * @returns {Map<string, Array<AbstractMiddleware | [Function, ...any]>>}
373
377
  * @example
374
378
  * return new Map([
375
379
  * ['/*', [GetUserByToken]] // for any method for this controller
@@ -3,12 +3,12 @@ import Base from './Base.js';
3
3
 
4
4
  class AbstractModel extends Base {
5
5
  /**
6
- * @param {import('../server')} app //TODO change to *.d.ts as this is a Server, not app
6
+ * @param {import('../server.js').default['app']} app //TODO change to *.d.ts as this is a Server, not app
7
7
  * @param function callback optional callback when connection ready
8
8
  */
9
9
  constructor(app, callback = () => {}) {
10
10
  super(app);
11
- this.mongooseSchema = mongoose.Schema(this.modelSchema);
11
+ this.mongooseSchema = new mongoose.Schema(this.modelSchema);
12
12
  mongoose.set('strictQuery', true);
13
13
  this.mongooseSchema.set('timestamps', true);
14
14
  this.mongooseSchema.set('minimize', false);
@@ -22,6 +22,9 @@ class AbstractModel extends Base {
22
22
  );
23
23
  if (!mongoose.connection.readyState) {
24
24
  this.app.events.on('shutdown', async () => {
25
+ this.logger.verbose(
26
+ 'Shutdown was called. Closing all mongoose connections',
27
+ );
25
28
  for (const c of mongoose.connections) {
26
29
  c.close(true);
27
30
  }
package/modules/Base.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import winston from 'winston';
2
- import Server from '../server';
2
+ import Server from '../server.js';
3
+ import type { Dirent } from 'fs';
3
4
 
4
5
  declare class Base {
5
6
  app: Server['app'];
@@ -26,11 +27,11 @@ declare class Base {
26
27
  getFilesPathWithInheritance(
27
28
  internalFolder: string,
28
29
  externalFolder: string,
29
- ): Promise<string[]>;
30
+ ): Promise<Dirent[]>;
30
31
 
31
32
  /**
32
33
  * Return logger group. Just to have all logs groupped logically
33
34
  */
34
35
  static get loggerGroup(): string;
35
36
  }
36
- export = Base;
37
+ export default Base;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@adaptivestone/framework",
3
- "version": "5.0.0-alpha.1",
3
+ "version": "5.0.0-alpha.10",
4
4
  "description": "Adaptive stone node js framework",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "engines": {
8
- "node": ">=18.17.0"
8
+ "node": ">=20.12.0"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
@@ -13,14 +13,15 @@
13
13
  },
14
14
  "homepage": "https://framework.adaptivestone.com/",
15
15
  "scripts": {
16
- "dev": "nodemon ./index.js",
17
- "prod": "nodemon ./cluster.js",
16
+ "dev": "node --watch ./index.js",
17
+ "prod": "node --watch ./cluster.js",
18
18
  "test": "vitest run",
19
+ "t": "vitest --coverage=false --reporter=default",
19
20
  "prettier": "prettier --check '**/*.(js|jsx|ts|tsx|json|css|scss|md)'",
20
21
  "lint": "eslint '**/*.js'",
21
22
  "lint:fix": "eslint '**/*.js' --fix",
22
23
  "codestyle": "npm run prettier && npm run lint",
23
- "prepare": "husky install",
24
+ "prepare": "husky",
24
25
  "cli": "node cliCommand",
25
26
  "benchmark": "h2load -n 10000 -c 50 -p 'http/1.1' http://localhost:3300/",
26
27
  "benchmark2": "h2load -n 10000 -c 50 https://localhost:3300/",
@@ -30,14 +31,13 @@
30
31
  "license": "MIT",
31
32
  "dependencies": {
32
33
  "deepmerge": "^4.2.2",
33
- "dotenv": "^16.0.0",
34
34
  "express": "^4.17.1",
35
35
  "formidable": "^3.5.1",
36
36
  "html-to-text": "^9.0.3",
37
37
  "i18next": "^23.2.8",
38
38
  "i18next-chained-backend": "^4.0.0",
39
39
  "i18next-fs-backend": "^2.0.0",
40
- "juice": "^9.0.0",
40
+ "juice": "^10.0.0",
41
41
  "mime": "^4.0.0",
42
42
  "minimist": "^1.2.5",
43
43
  "mongoose": "^8.0.0",
@@ -45,25 +45,24 @@
45
45
  "nodemailer-sendmail-transport": "^1.0.2",
46
46
  "nodemailer-stub-transport": "^1.1.0",
47
47
  "pug": "^3.0.2",
48
- "rate-limiter-flexible": "^3.0.0",
48
+ "rate-limiter-flexible": "^5.0.0",
49
49
  "redis": "^4.3.1",
50
50
  "winston": "^3.3.3",
51
51
  "winston-transport-sentry-node": "^2.0.0",
52
52
  "yup": "^1.0.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@vitest/coverage-v8": "^1.0.0",
55
+ "@vitest/coverage-v8": "^2.0.0",
56
56
  "eslint": "^8.0.0",
57
57
  "eslint-config-airbnb-base": "^15.0.0",
58
58
  "eslint-config-prettier": "^9.0.0",
59
59
  "eslint-plugin-prettier": "^5.0.0",
60
- "eslint-plugin-vitest": "^0.3.1",
61
- "husky": "^8.0.0",
60
+ "eslint-plugin-vitest": "^0.4.0",
61
+ "husky": "^9.0.0",
62
62
  "lint-staged": "^15.0.0",
63
- "mongodb-memory-server": "^9.0.0",
64
- "nodemon": "^3.0.1",
63
+ "mongodb-memory-server": "^10.0.0",
65
64
  "prettier": "^3.0.0",
66
- "vitest": "^1.0.0"
65
+ "vitest": "^2.0.0"
67
66
  },
68
67
  "lint-staged": {
69
68
  "**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
package/server.d.ts CHANGED
@@ -9,6 +9,8 @@ import BaseCli from './modules/BaseCli';
9
9
  import Cache from './services/cache/Cache';
10
10
  import winston from 'winston';
11
11
 
12
+ import HttpServer from './services/http/HttpServer.js';
13
+
12
14
  type ServerConfig = {
13
15
  folders: ExpandDeep<TFolderConfig>;
14
16
  };
@@ -24,7 +26,7 @@ declare class Server {
24
26
  events: EventEmitter;
25
27
  get cache(): Server['cacheService'];
26
28
  get logger(): winston.Logger;
27
- httpServer: null;
29
+ httpServer: HttpServer | null;
28
30
  controllerManager: null;
29
31
  };
30
32
  cacheService: Cache;
@@ -33,7 +35,7 @@ declare class Server {
33
35
  configs: Map<string, {}>;
34
36
  models: Map<string, MongooseModel<any>>;
35
37
  };
36
- cli: boolean;
38
+ cli: null | BaseCli;
37
39
 
38
40
  /**
39
41
  * Construct new server
@@ -68,7 +70,7 @@ declare class Server {
68
70
  * @see updateConfig
69
71
  * @TODO generate that based on real data
70
72
  */
71
- getConfig(configName: string): {};
73
+ getConfig(configName: string): { [key: string]: any };
72
74
 
73
75
  /**
74
76
  * Return or create new logger instance. This is a main logger instance
@@ -79,7 +81,7 @@ declare class Server {
79
81
  * Primary designed for tests when we need to update some configs before start testing
80
82
  * Should be called before any initialization was done
81
83
  */
82
- updateConfig(configName: string, config: {}): {};
84
+ updateConfig(configName: string, config: {}): { [key: string]: any };
83
85
 
84
86
  /**
85
87
  * Return model from {modelName} (file name) on model folder.
@@ -93,4 +95,4 @@ declare class Server {
93
95
  runCliCommand(commandName: string, args: {}): Promise<BaseCli['run']>;
94
96
  }
95
97
 
96
- export = Server;
98
+ export default Server;
package/server.js CHANGED
@@ -1,16 +1,21 @@
1
1
  /* eslint-disable no-console */
2
2
  import EventEmitter from 'node:events';
3
- import { hrtime } from 'node:process';
3
+ import { hrtime, loadEnvFile } from 'node:process';
4
4
  import * as url from 'node:url';
5
5
  import path from 'node:path';
6
6
 
7
- import 'dotenv/config';
8
7
  import merge from 'deepmerge';
9
8
  import winston from 'winston';
10
9
  import { getFilesPathWithInheritance } from './helpers/files.js';
11
10
  import { consoleLogger } from './helpers/logger.js';
12
11
  import Cache from './services/cache/Cache.js';
13
12
 
13
+ try {
14
+ loadEnvFile();
15
+ } catch (e) {
16
+ console.warn('No env file found. This is ok. But please check youself.');
17
+ }
18
+
14
19
  /**
15
20
  * Main framework class.
16
21
  */
@@ -19,6 +24,8 @@ class Server {
19
24
 
20
25
  #isInited = false;
21
26
 
27
+ cli = null;
28
+
22
29
  /**
23
30
  * Construct new server
24
31
  * @param {Object} config main config object
@@ -27,7 +34,6 @@ class Server {
27
34
  * @param {String} config.folders.models path to folder with moidels files
28
35
  * @param {String} config.folders.controllers path to folder with controllers files
29
36
  * @param {String} config.folders.views path to folder with view files
30
- * @param {String} config.folders.public path to folder with public files
31
37
  * @param {String} config.folders.locales path to folder with locales files
32
38
  * @param {String} config.folders.emails path to folder with emails files
33
39
  */
@@ -56,8 +62,6 @@ class Server {
56
62
  models: new Map(),
57
63
  modelConstructors: new Map(),
58
64
  };
59
-
60
- this.cli = false;
61
65
  }
62
66
 
63
67
  /**
@@ -350,7 +354,7 @@ class Server {
350
354
  * Return model from {modelName} (file name) on model folder.
351
355
  * Support cache
352
356
  * @param {String} modelName name on config file to load
353
- * @returns {import('mongoose').Model}
357
+ * @returns {import('mongoose').Model | false| {}}
354
358
  */
355
359
  getModel(modelName) {
356
360
  if (modelName.endsWith('s')) {
@@ -1,5 +1,5 @@
1
1
  import Base from '../../modules/Base';
2
- import Server from '../../server';
2
+ import Server from '../../server.js';
3
3
 
4
4
  declare class Cache extends Base {
5
5
  app: Server['app'];
@@ -32,4 +32,4 @@ declare class Cache extends Base {
32
32
  removeKey(key: string): Promise<number>;
33
33
  }
34
34
 
35
- export = Cache;
35
+ export default Cache;
@@ -69,12 +69,14 @@ class Cache extends Base {
69
69
  return Promise.reject(e);
70
70
  }
71
71
 
72
- this.redisClient.setEx(
72
+ this.redisClient.set(
73
73
  key,
74
- storeTime,
75
74
  JSON.stringify(result, (jsonkey, value) =>
76
75
  typeof value === 'bigint' ? `${value}n` : value,
77
76
  ),
77
+ {
78
+ EX: storeTime,
79
+ },
78
80
  );
79
81
  } else {
80
82
  this.logger.verbose(
@@ -6,7 +6,7 @@ import RequestLoggerMiddleware from './middleware/RequestLogger.js';
6
6
  import I18nMiddleware from './middleware/I18n.js';
7
7
  import PrepareAppInfoMiddleware from './middleware/PrepareAppInfo.js';
8
8
  import RequestParserMiddleware from './middleware/RequestParser.js';
9
- import StaticFilesMiddleware from './middleware/StaticFiles.js';
9
+ import IpDetector from './middleware/IpDetector.js';
10
10
  import Cors from './middleware/Cors.js';
11
11
  import Base from '../../modules/Base.js';
12
12
 
@@ -26,6 +26,7 @@ class HttpServer extends Base {
26
26
  this.express.set('view engine', 'pug');
27
27
 
28
28
  this.express.use(new PrepareAppInfoMiddleware(this.app).getMiddleware());
29
+ this.express.use(new IpDetector(this.app).getMiddleware());
29
30
  this.express.use(new RequestLoggerMiddleware(this.app).getMiddleware());
30
31
  this.express.use(new I18nMiddleware(this.app).getMiddleware());
31
32
 
@@ -35,15 +36,6 @@ class HttpServer extends Base {
35
36
  origins: httpConfig.corsDomains,
36
37
  }).getMiddleware(),
37
38
  );
38
- // todo whitelist
39
- this.express.use(
40
- new StaticFilesMiddleware(this.app, {
41
- folders: [
42
- this.app.foldersConfig.public,
43
- path.join(dirname, '../../public/files'),
44
- ],
45
- }).getMiddleware(),
46
- );
47
39
 
48
40
  this.express.use(new RequestParserMiddleware(this.app).getMiddleware());
49
41
 
@@ -57,7 +49,7 @@ class HttpServer extends Base {
57
49
  res.status(500).json({ message: 'Something broke!' });
58
50
  });
59
51
 
60
- this.httpServer = http.Server(this.express);
52
+ this.httpServer = http.createServer(this.express);
61
53
 
62
54
  const listener = this.httpServer.listen(
63
55
  httpConfig.port,
@@ -11,7 +11,7 @@ class GetUserByToken extends AbstractMiddleware {
11
11
  name: 'Authorization',
12
12
  type: 'apiKey',
13
13
  in: 'header',
14
- description: this?.description,
14
+ description: this.constructor.description,
15
15
  },
16
16
  ];
17
17
  }
@@ -0,0 +1,59 @@
1
+ import { BlockList } from 'node:net';
2
+
3
+ import AbstractMiddleware from './AbstractMiddleware.js';
4
+
5
+ class IpDetector extends AbstractMiddleware {
6
+ static get description() {
7
+ return 'Detect real user IP address. Support proxy and load balancer';
8
+ }
9
+
10
+ constructor(app, params) {
11
+ super(app, params);
12
+ const { trustedProxy } = this.app.getConfig('ipDetector');
13
+
14
+ this.blockList = new BlockList();
15
+
16
+ for (const subnet of trustedProxy) {
17
+ const addressType = subnet.includes(':') ? 'ipv6' : 'ipv4';
18
+ if (subnet.includes('/')) {
19
+ // CIDR
20
+ const [realSubnet, prefixLength] = subnet.split('/');
21
+ this.blockList.addSubnet(
22
+ realSubnet,
23
+ parseInt(prefixLength, 10),
24
+ addressType,
25
+ );
26
+ } else if (subnet.includes('-')) {
27
+ // RANGE
28
+ const [start, end] = subnet.split('-');
29
+ this.blockList.addRange(start, end, addressType);
30
+ } else {
31
+ // just an address
32
+ this.blockList.addAddress(subnet, addressType);
33
+ }
34
+ }
35
+ }
36
+
37
+ async middleware(req, res, next) {
38
+ const { headers } = this.app.getConfig('ipDetector');
39
+ const initialIp = req.socket.remoteAddress;
40
+ req.appInfo.ip = initialIp;
41
+ const addressType = initialIp.includes(':') ? 'ipv6' : 'ipv4';
42
+
43
+ if (this.blockList.check(initialIp, addressType)) {
44
+ // we can trust this source
45
+ for (const header of headers) {
46
+ // in a range
47
+ const ipHeader = req.headers[header.toLowerCase()];
48
+ if (ipHeader) {
49
+ const [firstIp] = ipHeader.split(',').map((ip) => ip.trim());
50
+ req.appInfo.ip = firstIp;
51
+ break;
52
+ }
53
+ }
54
+ }
55
+ next();
56
+ }
57
+ }
58
+
59
+ export default IpDetector;
@@ -0,0 +1,143 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import IpDetector from './IpDetector.js';
3
+
4
+ const testVectors = [
5
+ // IPv4 CIDR blocks
6
+ {
7
+ cidr: '192.168.0.0/16',
8
+ tests: [
9
+ { ip: '192.168.1.1', matches: true },
10
+ { ip: '192.169.1.1', matches: false },
11
+ ],
12
+ },
13
+ {
14
+ cidr: '10.0.0.0/8',
15
+ tests: [
16
+ { ip: '10.0.0.1', matches: true },
17
+ { ip: '11.0.0.1', matches: false },
18
+ ],
19
+ },
20
+ {
21
+ cidr: '172.16.0.0/12',
22
+ tests: [
23
+ { ip: '172.16.0.1', matches: true },
24
+ { ip: '172.32.0.1', matches: false },
25
+ ],
26
+ },
27
+
28
+ // // IPv6 CIDR blocks
29
+ {
30
+ cidr: '2001:db8::/32',
31
+ tests: [
32
+ { ip: '2001:db8::1', matches: true },
33
+ { ip: '2001:db9::1', matches: false },
34
+ ],
35
+ },
36
+ {
37
+ cidr: 'fe80::/10',
38
+ tests: [
39
+ { ip: 'fe80::1', matches: true },
40
+ { ip: 'fec0::1', matches: false },
41
+ ],
42
+ },
43
+ {
44
+ cidr: '::ffff:0:0/96',
45
+ tests: [
46
+ { ip: '::ffff:192.0.2.1', matches: true },
47
+ { ip: '2001:db8::1', matches: false },
48
+ ],
49
+ },
50
+
51
+ // // Specific IPv4 addresses
52
+ {
53
+ cidr: '203.0.113.1/32',
54
+ tests: [
55
+ { ip: '203.0.113.1', matches: true },
56
+ { ip: '203.0.113.2', matches: false },
57
+ ],
58
+ },
59
+
60
+ // // Specific IPv6 addresses
61
+ {
62
+ cidr: '2001:db8:85a3::8a2e:370:7334/128',
63
+ tests: [
64
+ { ip: '2001:db8:85a3::8a2e:370:7334', matches: true },
65
+ { ip: '2001:db8:85a3::8a2e:370:7335', matches: false },
66
+ ],
67
+ },
68
+
69
+ // // Mixed scenarios
70
+ {
71
+ cidr: '::ffff:192.0.2.0/120',
72
+ tests: [
73
+ { ip: '::ffff:192.0.2.1', matches: true },
74
+ { ip: '192.0.2.1', matches: true }, // IPv4-mapped addresses should match their IPv4 equivalents
75
+ { ip: '::ffff:192.0.3.1', matches: false },
76
+ ],
77
+ },
78
+
79
+ // // Edge cases
80
+ {
81
+ cidr: '0.0.0.0/0',
82
+ tests: [
83
+ { ip: '0.0.0.0', matches: true },
84
+ { ip: '255.255.255.255', matches: true },
85
+ { ip: '2001:db8::1', matches: false }, // Matches any IPv4 but not IPv6
86
+ ],
87
+ },
88
+ {
89
+ cidr: '::/0',
90
+ tests: [
91
+ { ip: '::1', matches: true },
92
+ { ip: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', matches: true },
93
+ { ip: '192.168.1.1', matches: true },
94
+ ],
95
+ },
96
+ {
97
+ cidr: '8.8.8.8-8.8.8.10', // our feature range
98
+ tests: [
99
+ { ip: '8.8.8.7', matches: false },
100
+ { ip: '8.8.8.8', matches: true },
101
+ { ip: '8.8.8.9', matches: true },
102
+ { ip: '8.8.8.10', matches: true },
103
+ { ip: '8.8.8.11', matches: false },
104
+ ],
105
+ },
106
+ {
107
+ cidr: '1.1.1.1', // one ip
108
+ tests: [
109
+ { ip: '8.8.8.7', matches: false },
110
+ { ip: '1.1.1.1', matches: true },
111
+ ],
112
+ },
113
+ ];
114
+
115
+ describe('ipDetector methods', () => {
116
+ it('have description fields', async () => {
117
+ expect.assertions(1);
118
+ const middleware = new IpDetector(global.server.app);
119
+ expect(middleware.constructor.description).toBeDefined();
120
+ });
121
+
122
+ it('middleware that works', async () => {
123
+ expect.hasAssertions();
124
+ const nextFunction = () => {};
125
+ for (const vector of testVectors) {
126
+ global.server.app.updateConfig('ipDetector', {
127
+ trustedProxy: [vector.cidr],
128
+ });
129
+ const middleware = new IpDetector(global.server.app);
130
+ for (const test of vector.tests) {
131
+ const req = {
132
+ appInfo: {},
133
+ headers: { 'x-forwarded-for': 'notAnIP' },
134
+ socket: { remoteAddress: test.ip },
135
+ };
136
+ // eslint-disable-next-line no-await-in-loop
137
+ await middleware.middleware(req, {}, nextFunction);
138
+ const result = req.appInfo.ip === 'notAnIP';
139
+ expect(result).toBe(test.matches);
140
+ }
141
+ }
142
+ });
143
+ });
@@ -78,7 +78,13 @@ class RateLimiter extends AbstractMiddleware {
78
78
 
79
79
  const key = [];
80
80
  if (ip) {
81
- key.push(req.ip);
81
+ if (!req.appInfo.ip) {
82
+ this.logger.error(
83
+ `RateLimiter: Can't get remote address from request. Please check that you used IpDetecor middleware before RateLimiter`,
84
+ );
85
+ } else {
86
+ key.push(req.appInfo.ip);
87
+ }
82
88
  }
83
89
  if (route) {
84
90
  key.push(req.originalUrl);
@@ -58,8 +58,8 @@ describe('rate limiter methods', () => {
58
58
  });
59
59
 
60
60
  const res = await redisRateLimiter.gerenateConsumeKey({
61
- ip: '192.168.0.0',
62
61
  appInfo: {
62
+ ip: '192.168.0.0',
63
63
  user: {
64
64
  id: 'someId',
65
65
  },
@@ -80,7 +80,9 @@ describe('rate limiter methods', () => {
80
80
  });
81
81
 
82
82
  const res = await redisRateLimiter.gerenateConsumeKey({
83
- ip: '192.168.0.0',
83
+ appInfo: {
84
+ ip: '192.168.0.0',
85
+ },
84
86
  body: {
85
87
  email: 'foo@example.com',
86
88
  },
@@ -18,7 +18,10 @@ class RequestParser extends AbstractMiddleware {
18
18
  [fields, files] = await form.parse(req);
19
19
  } catch (err) {
20
20
  this.logger.error(`Parsing failed ${err}`);
21
- return next(err);
21
+ return res.status(400).json({
22
+ message: `Error to parse your request. You provided invalid content type or content-length. Please check your request headers and content type.`,
23
+ });
24
+ // return next(err);
22
25
  }
23
26
  this.logger.verbose(
24
27
  `Parsing multipart/formdata request DONE ${Date.now() - time}ms`,
@@ -12,7 +12,6 @@ describe('reqest parser limiter methods', () => {
12
12
  });
13
13
  it('middleware that works', async () => {
14
14
  expect.assertions(4);
15
- console.log();
16
15
 
17
16
  await new Promise((done) => {
18
17
  // from https://github.com/node-formidable/formidable/blob/master/test-node/standalone/promise.test.js
@@ -80,12 +79,21 @@ d\r
80
79
  const server = createServer(async (req, res) => {
81
80
  req.appInfo = {};
82
81
  const middleware = new RequestParser(global.server.app);
83
- middleware.middleware(req, {}, (err) => {
84
- expect(err).toBeDefined();
82
+ let status;
85
83
 
86
- res.writeHead(200);
87
- res.end('ok');
88
- });
84
+ const resp = {
85
+ status: (code) => {
86
+ status = code;
87
+ return resp;
88
+ },
89
+ json: () => resp,
90
+ };
91
+ await middleware.middleware(req, resp, () => {});
92
+ expect(status).toBe(400);
93
+ // expect(err).toBeDefined();
94
+
95
+ res.writeHead(200);
96
+ res.end('ok');
89
97
  });
90
98
  server.listen(null, async () => {
91
99
  const chosenPort = server.address().port;
@@ -66,7 +66,8 @@ class Mail extends Base {
66
66
  * @param {object} templateData
67
67
  * @returns string
68
68
  */
69
- static async #renderTemplateFile({ type, fullPath } = {}, templateData = {}) {
69
+ // eslint-disable-next-line class-methods-use-this
70
+ async #renderTemplateFile({ type, fullPath } = {}, templateData = {}) {
70
71
  if (!type) {
71
72
  return null;
72
73
  }
@@ -116,19 +117,10 @@ class Mail extends Base {
116
117
 
117
118
  const [htmlRendered, subjectRendered, textRendered, extraCss] =
118
119
  await Promise.all([
119
- this.constructor.#renderTemplateFile(
120
- templates.html,
121
- templateDataToRender,
122
- ),
123
- this.constructor.#renderTemplateFile(
124
- templates.subject,
125
- templateDataToRender,
126
- ),
127
- this.constructor.#renderTemplateFile(
128
- templates.text,
129
- templateDataToRender,
130
- ),
131
- this.constructor.#renderTemplateFile(templates.style),
120
+ this.#renderTemplateFile(templates.html, templateDataToRender),
121
+ this.#renderTemplateFile(templates.subject, templateDataToRender),
122
+ this.#renderTemplateFile(templates.text, templateDataToRender),
123
+ this.#renderTemplateFile(templates.style),
132
124
  ]);
133
125
 
134
126
  juice.tableElements = ['TABLE'];
@@ -171,7 +163,7 @@ class Mail extends Base {
171
163
 
172
164
  /**
173
165
  * Send provided text (html) to email. Low level function. All data should be prepared before sending (like inline styles)
174
- * @param {objetc} app application
166
+ * @param {import('../../../server.js').default['app']} app application
175
167
  * @param {string} to send to
176
168
  * @param {string} subject email topic
177
169
  * @param {string} html hmlt body of emain
package/tests/setup.js CHANGED
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable no-undef */
2
2
  import path from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
3
4
  import { MongoMemoryReplSet } from 'mongodb-memory-server';
4
5
  import mongoose from 'mongoose';
5
6
  import redis from 'redis';
@@ -19,6 +20,8 @@ beforeAll(async () => {
19
20
  });
20
21
  await mongoMemoryServerInstance.waitUntilRunning();
21
22
  process.env.LOGGER_CONSOLE_LEVEL = 'error';
23
+ process.env.AUTH_SALT = randomBytes(16).toString('hex');
24
+
22
25
  const connectionStringMongo = await mongoMemoryServerInstance.getUri();
23
26
  // console.info('MONGO_URI: ', connectionStringMongo);
24
27
  global.server = new Server({
@@ -27,7 +30,6 @@ beforeAll(async () => {
27
30
  controllers:
28
31
  process.env.TEST_FOLDER_CONTROLLERS || path.resolve('./controllers'),
29
32
  views: process.env.TEST_FOLDER_VIEWS || path.resolve('./views'),
30
- public: process.env.TEST_FOLDER_PUBLIC || path.resolve('./public'),
31
33
  models: process.env.TEST_FOLDER_MODELS || path.resolve('./models'),
32
34
  emails:
33
35
  process.env.TEST_FOLDER_EMAIL ||
@@ -12,13 +12,13 @@ mongoose.set('autoIndex', false);
12
12
 
13
13
  beforeAll(async () => {
14
14
  process.env.LOGGER_CONSOLE_LEVEL = 'error';
15
+ process.env.AUTH_SALT = crypto.randomBytes(16).toString('hex');
15
16
  global.server = new Server({
16
17
  folders: {
17
18
  config: process.env.TEST_FOLDER_CONFIG || path.resolve('./config'),
18
19
  controllers:
19
20
  process.env.TEST_FOLDER_CONTROLLERS || path.resolve('./controllers'),
20
21
  views: process.env.TEST_FOLDER_VIEWS || path.resolve('./views'),
21
- public: process.env.TEST_FOLDER_PUBLIC || path.resolve('./public'),
22
22
  models: process.env.TEST_FOLDER_MODELS || path.resolve('./models'),
23
23
  emails:
24
24
  process.env.TEST_FOLDER_EMAIL ||
@@ -3,7 +3,6 @@
3
3
  * @param models path to folder with moidels files
4
4
  * @param controllers path to folder with controllers files
5
5
  * @param views path to folder with view files
6
- * @param public path to folder with public files
7
6
  * @param locales path to folder with locales files
8
7
  * @param emails path to folder with emails files
9
8
  */
@@ -12,7 +11,6 @@ type FolderConfig = {
12
11
  models: string;
13
12
  controllers: string;
14
13
  views: string;
15
- public: string;
16
14
  emails: string;
17
15
  };
18
16
 
@@ -1,59 +0,0 @@
1
- import fsPromises from 'node:fs/promises';
2
- import fs from 'node:fs';
3
- import path from 'node:path';
4
- import mime from 'mime';
5
- import AbstractMiddleware from './AbstractMiddleware.js';
6
- /**
7
- * Middleware for static files
8
- */
9
- class StaticFiles extends AbstractMiddleware {
10
- constructor(app, params) {
11
- super(app);
12
- this.params = params;
13
- if (!params || !params.folders || !params.folders.length) {
14
- throw new Error('StaticFiles inited without folders config');
15
- }
16
- }
17
-
18
- static get description() {
19
- return 'Static file server middleware. Host you static files from public foolder. Mostly for dev.';
20
- }
21
-
22
- async middleware(req, res, next) {
23
- if (req.method !== 'GET') {
24
- // only get supported
25
- return next();
26
- }
27
- const { folders } = this.params;
28
-
29
- const promises = [];
30
-
31
- for (const f of folders) {
32
- const filePath = path.join(f, req.url);
33
- promises.push(
34
- fsPromises
35
- .stat(filePath)
36
- .catch(() => {
37
- // nothing there, file just not exists
38
- })
39
- .then((stats) => ({ stats, file: filePath })),
40
- );
41
- }
42
-
43
- const fileStats = await Promise.all(promises);
44
-
45
- for (const fileStat of fileStats) {
46
- if (fileStat.stats && fileStat.stats.isFile()) {
47
- const contentType = mime.getType(fileStat.file);
48
- const fileStream = fs.createReadStream(fileStat.file);
49
- res.set('Content-Type', contentType);
50
- fileStream.pipe(res);
51
- return null;
52
- }
53
- }
54
-
55
- return next();
56
- }
57
- }
58
-
59
- export default StaticFiles;