@adaptivestone/framework 5.0.0-alpha.2 → 5.0.0-alpha.20

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/commands/CreateUser.js +3 -1
  3. package/commands/GenerateRandomBytes.js +15 -0
  4. package/commands/migration/Migrate.js +1 -1
  5. package/config/auth.js +5 -1
  6. package/config/ipDetector.js +14 -0
  7. package/controllers/Auth.js +1 -1
  8. package/folderConfig.js +0 -1
  9. package/helpers/files.js +8 -8
  10. package/jsconfig.json +9 -0
  11. package/modules/AbstractCommand.js +2 -1
  12. package/modules/AbstractController.js +20 -16
  13. package/modules/AbstractModel.d.ts +48 -0
  14. package/modules/AbstractModel.js +20 -2
  15. package/modules/Base.d.ts +5 -4
  16. package/modules/Base.js +11 -1
  17. package/package.json +12 -15
  18. package/server.d.ts +7 -5
  19. package/server.js +11 -7
  20. package/services/cache/Cache.d.ts +2 -2
  21. package/services/cache/Cache.js +10 -6
  22. package/services/http/HttpServer.js +11 -19
  23. package/services/http/middleware/GetUserByToken.js +2 -1
  24. package/services/http/middleware/I18n.js +20 -21
  25. package/services/http/middleware/IpDetector.js +59 -0
  26. package/services/http/middleware/Pagination.js +3 -2
  27. package/services/http/middleware/RateLimiter.js +8 -2
  28. package/services/http/middleware/RequestParser.js +4 -1
  29. package/services/messaging/email/index.js +7 -15
  30. package/tests/setup.js +3 -1
  31. package/tests/setupVitest.js +1 -1
  32. package/types/TFoldersConfig.d.ts +0 -2
  33. package/.eslintrc.cjs +0 -41
  34. package/controllers/Auth.test.js +0 -451
  35. package/controllers/Home.test.js +0 -12
  36. package/models/Migration.test.js +0 -20
  37. package/models/Sequence.test.js +0 -43
  38. package/models/User.test.js +0 -143
  39. package/modules/Modules.test.js +0 -18
  40. package/services/cache/Cache.test.js +0 -81
  41. package/services/http/middleware/Auth.test.js +0 -57
  42. package/services/http/middleware/Cors.test.js +0 -147
  43. package/services/http/middleware/GetUserByToken.test.js +0 -108
  44. package/services/http/middleware/I18n.test.js +0 -96
  45. package/services/http/middleware/PrepareAppInfo.test.js +0 -26
  46. package/services/http/middleware/RateLimiter.test.js +0 -233
  47. package/services/http/middleware/RequestParser.test.js +0 -112
  48. package/services/http/middleware/Role.test.js +0 -93
  49. package/services/http/middleware/StaticFiles.js +0 -59
  50. package/services/validate/ValidateService.test.js +0 -107
@@ -43,8 +43,10 @@ class Cache extends Base {
43
43
  }
44
44
  const key = this.getKeyWithNameSpace(keyValue);
45
45
  // 5 mins default
46
- let resolve = null;
47
- let reject = null;
46
+ // eslint-disable-next-line no-unused-vars
47
+ let resolve = (value) => {};
48
+ // eslint-disable-next-line no-unused-vars
49
+ let reject = (value) => {};
48
50
  if (this.promiseMapping.has(key)) {
49
51
  return this.promiseMapping.get(key);
50
52
  }
@@ -69,12 +71,14 @@ class Cache extends Base {
69
71
  return Promise.reject(e);
70
72
  }
71
73
 
72
- this.redisClient.setEx(
74
+ this.redisClient.set(
73
75
  key,
74
- storeTime,
75
- JSON.stringify(result, (jsonkey, value) =>
76
+ JSON.stringify(result, (_jsonkey, value) =>
76
77
  typeof value === 'bigint' ? `${value}n` : value,
77
78
  ),
79
+ {
80
+ EX: storeTime,
81
+ },
78
82
  );
79
83
  } else {
80
84
  this.logger.verbose(
@@ -84,7 +88,7 @@ class Cache extends Base {
84
88
  )}`,
85
89
  );
86
90
  try {
87
- result = JSON.parse(result, (jsonkey, value) => {
91
+ result = JSON.parse(result, (_jsonkey, value) => {
88
92
  if (typeof value === 'string' && /^\d+n$/.test(value)) {
89
93
  return BigInt(value.slice(0, value.length - 1));
90
94
  }
@@ -1,12 +1,12 @@
1
1
  import http from 'node:http';
2
- import path from 'node:path';
3
- import * as url from 'node:url';
2
+ // import path from 'node:path';
3
+ // import * as url from 'node:url';
4
4
  import express from 'express';
5
5
  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
 
@@ -18,14 +18,15 @@ class HttpServer extends Base {
18
18
  super(app);
19
19
  this.express = express();
20
20
  this.express.disable('x-powered-by');
21
- const dirname = url.fileURLToPath(new URL('.', import.meta.url));
22
- this.express.set('views', [
23
- this.app.foldersConfig.views,
24
- path.join(dirname, '../../views'),
25
- ]);
26
- this.express.set('view engine', 'pug');
21
+ // const dirname = url.fileURLToPath(new URL('.', import.meta.url));
22
+ // this.express.set('views', [
23
+ // this.app.foldersConfig.views,
24
+ // path.join(dirname, '../../views'),
25
+ // ]);
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,
@@ -5,13 +5,14 @@ class GetUserByToken extends AbstractMiddleware {
5
5
  return 'Grab a token and try to parse the user from it. It user exist will add req.appInfo.user variable';
6
6
  }
7
7
 
8
+ // eslint-disable-next-line class-methods-use-this
8
9
  get usedAuthParameters() {
9
10
  return [
10
11
  {
11
12
  name: 'Authorization',
12
13
  type: 'apiKey',
13
14
  in: 'header',
14
- description: this?.description,
15
+ description: GetUserByToken.description,
15
16
  },
16
17
  ];
17
18
  }
@@ -1,37 +1,36 @@
1
1
  import i18next from 'i18next';
2
2
  import BackendFS from 'i18next-fs-backend';
3
- import Backend from 'i18next-chained-backend';
4
3
  import AbstractMiddleware from './AbstractMiddleware.js';
5
4
 
6
5
  class I18n extends AbstractMiddleware {
6
+ cache = {};
7
+
8
+ enabled = true;
9
+
10
+ lookupQuerystring = '';
11
+
12
+ supportedLngs = [];
13
+
14
+ fallbackLng = 'en';
15
+
16
+ /** @type {i18next} */
17
+ i18n = {
18
+ // @ts-ignore
19
+ t: (text) => text,
20
+ language: 'en',
21
+ };
22
+
7
23
  constructor(app, params) {
8
24
  super(app, params);
9
25
  const I18NConfig = this.app.getConfig('i18n');
10
- this.i18n = {
11
- t: (text) => text,
12
- language: I18NConfig.fallbackLng,
13
- };
14
- this.cache = {};
15
26
 
16
27
  if (I18NConfig.enabled) {
17
28
  this.logger.info('Enabling i18n support');
18
29
  this.i18n = i18next;
19
- i18next.use(Backend).init({
30
+ i18next.use(BackendFS).init({
20
31
  backend: {
21
- backends: [
22
- BackendFS,
23
- // BackendFS,
24
- ],
25
- backendOptions: [
26
- // {
27
- // loadPath: __dirname + '/../../locales/{{lng}}/{{ns}}.json',
28
- // addPath: __dirname + '/../../locales/{{lng}}/{{ns}}.missing.json'
29
- // },
30
- {
31
- loadPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.json`,
32
- addPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.missing.json`,
33
- },
34
- ],
32
+ loadPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.json`,
33
+ addPath: `${this.app.foldersConfig.locales}/{{lng}}/{{ns}}.missing.json`,
35
34
  },
36
35
  fallbackLng: I18NConfig.fallbackLng,
37
36
  preload: I18NConfig.preload,
@@ -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;
@@ -19,8 +19,9 @@ class Pagination extends AbstractMiddleware {
19
19
  async middleware(req, res, next) {
20
20
  let { limit, maxLimit } = this.params;
21
21
 
22
- limit = typeof limit === 'number' ? parseInt(limit, 10) : 10;
23
- maxLimit = typeof maxLimit === 'number' ? parseInt(maxLimit, 10) : 100;
22
+ limit = (typeof limit !== 'number' ? parseInt(limit, 10) : limit) || 10;
23
+ maxLimit =
24
+ (typeof maxLimit !== 'number' ? parseInt(maxLimit, 10) : maxLimit) || 100;
24
25
 
25
26
  req.appInfo.pagination = {};
26
27
  req.appInfo.pagination.page =
@@ -78,10 +78,16 @@ 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
- key.push(req.originalUrl);
90
+ key.push(`${req.baseUrl ?? ''}${req.path ?? ''}`); // to avoid quesry params
85
91
  }
86
92
  if (user && req.appInfo?.user) {
87
93
  key.push(req.appInfo?.user.id);
@@ -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`,
@@ -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
 
package/.eslintrc.cjs DELETED
@@ -1,41 +0,0 @@
1
- module.exports = {
2
- env: {
3
- es2023: true,
4
- node: true,
5
- },
6
- extends: ['airbnb-base', 'prettier'],
7
- plugins: ['prettier'],
8
- parserOptions: {
9
- ecmaVersion: 2023,
10
- },
11
- rules: {
12
- 'no-restricted-syntax': [
13
- 'error',
14
- {
15
- selector: 'ForInStatement',
16
- message:
17
- 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.',
18
- },
19
- {
20
- selector: 'LabeledStatement',
21
- message:
22
- 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.',
23
- },
24
- {
25
- selector: 'WithStatement',
26
- message:
27
- '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
28
- },
29
- ],
30
- 'prettier/prettier': 'error',
31
- 'import/extensions': 'off', // it have a problem with dynamic imports
32
- 'import/prefer-default-export': 'off', // we want to have signgke default export too
33
- },
34
- overrides: [
35
- {
36
- files: ['**/*.test.js'],
37
- extends: ['plugin:vitest/all', 'plugin:vitest/recommended'],
38
- plugins: ['vitest'],
39
- },
40
- ],
41
- };