@adaptivestone/framework 4.4.0 → 4.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/CHANGELOG.md +6 -0
- package/babel.config.js +3 -0
- package/controllers/index.js +20 -17
- package/models/User.js +8 -8
- package/modules/AbstractModel.js +1 -0
- package/modules/BaseCli.js +2 -2
- package/package.json +3 -2
- package/server.js +9 -9
- package/services/http/middleware/RateLimiter.js +5 -6
- package/services/http/middleware/RateLimiter.test.js +179 -0
- package/tests/setup.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
### 4.5.0
|
|
2
|
+
|
|
3
|
+
[NEW] Now getSuper() available as a method on monsgoose models
|
|
4
|
+
[UPDATE] Update rate-limiter-flexible to v3
|
|
5
|
+
[UPDATE] Update test runner to suport ESM. In case problem with test please copy babel.config.js from framework to your project directory
|
|
6
|
+
|
|
1
7
|
### 4.4.0
|
|
2
8
|
|
|
3
9
|
[NEW] New method to grab url of server it testing enviroument global.server.testingGetUrl("/some/endpoint")
|
package/babel.config.js
ADDED
package/controllers/index.js
CHANGED
|
@@ -28,26 +28,29 @@ class ControllerManager extends Base {
|
|
|
28
28
|
}
|
|
29
29
|
return 0;
|
|
30
30
|
});
|
|
31
|
-
|
|
31
|
+
const controllers = [];
|
|
32
32
|
for (const controller of controllersToLoad) {
|
|
33
33
|
// TODO wait until https://github.com/nodejs/node/issues/35889
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
controllers.push(
|
|
35
|
+
import(controller.path).then(({ default: ControllerModule }) => {
|
|
36
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
37
|
+
// const ControllerModule = require(controller.path);
|
|
38
|
+
const contollerName = ControllerModule.name.toLowerCase();
|
|
39
|
+
let prefix = path.dirname(controller.file);
|
|
40
|
+
if (prefix === '.') {
|
|
41
|
+
prefix = '';
|
|
42
|
+
}
|
|
43
|
+
const controllePath = prefix
|
|
44
|
+
? `${prefix}/${contollerName}`
|
|
45
|
+
: contollerName;
|
|
46
|
+
this.controllers[controllePath] = new ControllerModule(
|
|
47
|
+
this.app,
|
|
48
|
+
prefix,
|
|
49
|
+
);
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
await Promise.all(controllers);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
static get loggerGroup() {
|
package/models/User.js
CHANGED
|
@@ -88,8 +88,8 @@ class User extends AbstractModel {
|
|
|
88
88
|
const scryptAsync = promisify(scrypt);
|
|
89
89
|
const data = await scryptAsync(
|
|
90
90
|
this.email + Date.now(),
|
|
91
|
-
this.
|
|
92
|
-
this.
|
|
91
|
+
this.getSuper().saltSecret,
|
|
92
|
+
this.getSuper().hashRounds,
|
|
93
93
|
);
|
|
94
94
|
const token = data.toString('base64url');
|
|
95
95
|
this.sessionTokens.push({ token, valid: timestamp });
|
|
@@ -138,8 +138,8 @@ class User extends AbstractModel {
|
|
|
138
138
|
const scryptAsync = promisify(scrypt);
|
|
139
139
|
const data = await scryptAsync(
|
|
140
140
|
userMongoose.email + Date.now(),
|
|
141
|
-
userMongoose.
|
|
142
|
-
userMongoose.
|
|
141
|
+
userMongoose.getSuper().saltSecret,
|
|
142
|
+
userMongoose.getSuper().hashRounds,
|
|
143
143
|
);
|
|
144
144
|
const token = data.toString('base64url');
|
|
145
145
|
// if (err) {
|
|
@@ -177,7 +177,7 @@ class User extends AbstractModel {
|
|
|
177
177
|
const passwordRecoveryToken =
|
|
178
178
|
await User.generateUserPasswordRecoveryToken(this);
|
|
179
179
|
const mail = new Mailer(
|
|
180
|
-
this.
|
|
180
|
+
this.getSuper().app,
|
|
181
181
|
'recovery',
|
|
182
182
|
{
|
|
183
183
|
link: `${i18n.language}/auth/recovery?password_recovery_token=${passwordRecoveryToken.token}`,
|
|
@@ -194,8 +194,8 @@ class User extends AbstractModel {
|
|
|
194
194
|
const scryptAsync = promisify(scrypt);
|
|
195
195
|
const data = await scryptAsync(
|
|
196
196
|
userMongoose.email + Date.now(),
|
|
197
|
-
userMongoose.
|
|
198
|
-
userMongoose.
|
|
197
|
+
userMongoose.getSuper().saltSecret,
|
|
198
|
+
userMongoose.getSuper().hashRounds,
|
|
199
199
|
);
|
|
200
200
|
const token = data.toString('base64url');
|
|
201
201
|
// if (err) {
|
|
@@ -243,7 +243,7 @@ class User extends AbstractModel {
|
|
|
243
243
|
async sendVerificationEmail(i18n) {
|
|
244
244
|
const verificationToken = await User.generateUserVerificationToken(this);
|
|
245
245
|
const mail = new Mailer(
|
|
246
|
-
this.
|
|
246
|
+
this.getSuper().app,
|
|
247
247
|
'verification',
|
|
248
248
|
{
|
|
249
249
|
link: `${i18n.language}/auth/login?verification_token=${verificationToken.token}`,
|
package/modules/AbstractModel.js
CHANGED
|
@@ -14,6 +14,7 @@ class AbstractModel extends Base {
|
|
|
14
14
|
this.mongooseSchema.set('minimize', false);
|
|
15
15
|
this.mongooseSchema.loadClass(this.constructor);
|
|
16
16
|
this.mongooseSchema.statics.getSuper = () => this;
|
|
17
|
+
this.mongooseSchema.methods.getSuper = () => this;
|
|
17
18
|
this.initHooks();
|
|
18
19
|
this.mongooseModel = mongoose.model(
|
|
19
20
|
this.constructor.name,
|
package/modules/BaseCli.js
CHANGED
|
@@ -48,10 +48,10 @@ class Cli extends Base {
|
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
50
|
// TODO wait until https://github.com/nodejs/node/issues/35889
|
|
51
|
-
|
|
51
|
+
const { default: Command } = await import(this.commands[command]);
|
|
52
52
|
|
|
53
53
|
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
54
|
-
const Command = require(this.commands[command]);
|
|
54
|
+
// const Command = require(this.commands[command]);
|
|
55
55
|
|
|
56
56
|
const c = new Command(this.app, this.commands, args);
|
|
57
57
|
let result = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaptivestone/framework",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Adaptive stone node js framework",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"engines": {
|
|
@@ -56,13 +56,14 @@
|
|
|
56
56
|
"nodemailer-sendmail-transport": "^1.0.2",
|
|
57
57
|
"nodemailer-stub-transport": "^1.1.0",
|
|
58
58
|
"pug": "^3.0.2",
|
|
59
|
-
"rate-limiter-flexible": "^
|
|
59
|
+
"rate-limiter-flexible": "^3.0.0",
|
|
60
60
|
"redis": "^4.3.1",
|
|
61
61
|
"winston": "^3.3.3",
|
|
62
62
|
"winston-transport-sentry-node": "^2.0.0",
|
|
63
63
|
"yup": "^1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
+
"@babel/preset-env": "^7.22.15",
|
|
66
67
|
"eslint": "^8.0.0",
|
|
67
68
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
68
69
|
"eslint-config-prettier": "^9.0.0",
|
package/server.js
CHANGED
|
@@ -52,17 +52,17 @@ class Server {
|
|
|
52
52
|
*/
|
|
53
53
|
async startServer(callbackBefore404 = async () => Promise.resolve()) {
|
|
54
54
|
// eslint-disable-next-line global-require
|
|
55
|
-
const HttpServer = require('./services/http/HttpServer');
|
|
55
|
+
// const HttpServer = require('./services/http/HttpServer');
|
|
56
56
|
// eslint-disable-next-line global-require
|
|
57
|
-
const ControllerManager = require('./controllers/index');
|
|
57
|
+
// const ControllerManager = require('./controllers/index');
|
|
58
58
|
// TODO wait until https://github.com/nodejs/node/issues/35889
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
const [{ default: HttpServer }, { default: ControllerManager }] =
|
|
60
|
+
await Promise.all([
|
|
61
|
+
// eslint-disable-next-line import/extensions
|
|
62
|
+
import('./services/http/HttpServer.js'), // Speed optimisation
|
|
63
|
+
// eslint-disable-next-line import/extensions
|
|
64
|
+
import('./controllers/index.js'), // Speed optimisation
|
|
65
|
+
]);
|
|
66
66
|
|
|
67
67
|
this.addErrorHandling();
|
|
68
68
|
|
|
@@ -16,10 +16,9 @@ class RateLimiter extends AbstractMiddleware {
|
|
|
16
16
|
|
|
17
17
|
constructor(app, params) {
|
|
18
18
|
super(app, params);
|
|
19
|
-
const routeParams = params;
|
|
20
19
|
const limiterOptions = this.app.getConfig('rateLimiter');
|
|
21
20
|
|
|
22
|
-
this.finalOptions = merge(limiterOptions,
|
|
21
|
+
this.finalOptions = merge(limiterOptions, params);
|
|
23
22
|
this.limiter = null;
|
|
24
23
|
|
|
25
24
|
switch (this.finalOptions.driver) {
|
|
@@ -32,7 +31,7 @@ class RateLimiter extends AbstractMiddleware {
|
|
|
32
31
|
break;
|
|
33
32
|
|
|
34
33
|
case 'mongo':
|
|
35
|
-
this.limiter = RateLimiterMongo({
|
|
34
|
+
this.limiter = new RateLimiterMongo({
|
|
36
35
|
storeClient: mongoose.connection,
|
|
37
36
|
...this.finalOptions.limiterOptions,
|
|
38
37
|
});
|
|
@@ -50,7 +49,6 @@ class RateLimiter extends AbstractMiddleware {
|
|
|
50
49
|
const redisConfig = this.app.getConfig('redis');
|
|
51
50
|
const redisClient = redis.createClient({
|
|
52
51
|
url: redisConfig.url,
|
|
53
|
-
legacyMode: true,
|
|
54
52
|
});
|
|
55
53
|
|
|
56
54
|
// TODO: change it
|
|
@@ -71,6 +69,7 @@ class RateLimiter extends AbstractMiddleware {
|
|
|
71
69
|
|
|
72
70
|
return new RateLimiterRedis({
|
|
73
71
|
storeClient: redisClient,
|
|
72
|
+
useRedisPackage: true,
|
|
74
73
|
...this.finalOptions.limiterOptions,
|
|
75
74
|
});
|
|
76
75
|
}
|
|
@@ -91,7 +90,7 @@ class RateLimiter extends AbstractMiddleware {
|
|
|
91
90
|
|
|
92
91
|
if (request && request.length) {
|
|
93
92
|
request.forEach((val) => {
|
|
94
|
-
if (req.body[val]) {
|
|
93
|
+
if (req.body && req.body[val]) {
|
|
95
94
|
key.push(req.body[val]);
|
|
96
95
|
}
|
|
97
96
|
// if (req.appInfo.request && req.appInfo.request[val]) {
|
|
@@ -108,6 +107,7 @@ class RateLimiter extends AbstractMiddleware {
|
|
|
108
107
|
this.logger.info(
|
|
109
108
|
`RateLimmiter not inited correclty! Please check init logs `,
|
|
110
109
|
);
|
|
110
|
+
return res.status(500).send('');
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const { namespace } = this.app.getConfig('redis');
|
|
@@ -119,7 +119,6 @@ class RateLimiter extends AbstractMiddleware {
|
|
|
119
119
|
.catch(() => {
|
|
120
120
|
this.logger.warn(`Too many requests. Consume key: ${consumeKey}`);
|
|
121
121
|
});
|
|
122
|
-
|
|
123
122
|
if (consumeResult) {
|
|
124
123
|
return next();
|
|
125
124
|
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
const { setTimeout } = require('node:timers/promises');
|
|
1
2
|
const RateLimiter = require('./RateLimiter');
|
|
2
3
|
|
|
4
|
+
let mongoRateLimiter;
|
|
5
|
+
|
|
3
6
|
describe('rate limiter methods', () => {
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
mongoRateLimiter = new RateLimiter(global.server.app, {
|
|
9
|
+
driver: 'mongo',
|
|
10
|
+
limiterOptions: {
|
|
11
|
+
keyPrefix: `mongo_${Date.now()}`,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
// we need to wait because redis mongo ask mongo to create indexes
|
|
18
|
+
await setTimeout(200);
|
|
19
|
+
});
|
|
4
20
|
it('have description fields', async () => {
|
|
5
21
|
expect.assertions(1);
|
|
6
22
|
const middleware = new RateLimiter(global.server.app, {
|
|
@@ -47,4 +63,167 @@ describe('rate limiter methods', () => {
|
|
|
47
63
|
|
|
48
64
|
expect(res).toBe('192.168.0.0__someId');
|
|
49
65
|
});
|
|
66
|
+
|
|
67
|
+
it('generateConsumeKey with request works correctly', async () => {
|
|
68
|
+
expect.assertions(1);
|
|
69
|
+
|
|
70
|
+
const redisRateLimiter = new RateLimiter(global.server.app, {
|
|
71
|
+
driver: 'redis',
|
|
72
|
+
consumeKeyComponents: {
|
|
73
|
+
request: ['email'],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const res = await redisRateLimiter.gerenateConsumeKey({
|
|
78
|
+
ip: '192.168.0.0',
|
|
79
|
+
body: {
|
|
80
|
+
email: 'foo@example.com',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(res).toBe('192.168.0.0__foo@example.com');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('middleware without driver should fail', async () => {
|
|
88
|
+
expect.assertions(2);
|
|
89
|
+
const rateLimiter = new RateLimiter(global.server.app, {
|
|
90
|
+
driver: 'unknown',
|
|
91
|
+
});
|
|
92
|
+
const nextFunction = jest.fn(() => {});
|
|
93
|
+
const req = {
|
|
94
|
+
appInfo: {},
|
|
95
|
+
};
|
|
96
|
+
let status;
|
|
97
|
+
let isSend;
|
|
98
|
+
await rateLimiter.middleware(
|
|
99
|
+
req,
|
|
100
|
+
{
|
|
101
|
+
status(statusCode) {
|
|
102
|
+
status = statusCode;
|
|
103
|
+
return this;
|
|
104
|
+
},
|
|
105
|
+
send() {
|
|
106
|
+
isSend = true;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
nextFunction,
|
|
110
|
+
);
|
|
111
|
+
expect(status).toBe(500);
|
|
112
|
+
expect(isSend).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const makeOneRequest = async ({ rateLimiter, driver, request }) => {
|
|
116
|
+
let realRateLimiter = rateLimiter;
|
|
117
|
+
if (!realRateLimiter) {
|
|
118
|
+
realRateLimiter = new RateLimiter(global.server.app, {
|
|
119
|
+
driver,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const req = {
|
|
123
|
+
appInfo: {},
|
|
124
|
+
...request,
|
|
125
|
+
};
|
|
126
|
+
let status;
|
|
127
|
+
let isSend = false;
|
|
128
|
+
let isNextCalled = false;
|
|
129
|
+
await realRateLimiter.middleware(
|
|
130
|
+
req,
|
|
131
|
+
{
|
|
132
|
+
status(statusCode) {
|
|
133
|
+
status = statusCode;
|
|
134
|
+
return this;
|
|
135
|
+
},
|
|
136
|
+
send() {
|
|
137
|
+
isSend = true;
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
() => {
|
|
141
|
+
isNextCalled = true;
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
return { status, isSend, isNextCalled };
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
it('middleware should works with a mongo drivers', async () => {
|
|
148
|
+
expect.assertions(1);
|
|
149
|
+
const { isNextCalled } = await makeOneRequest({
|
|
150
|
+
rateLimiter: mongoRateLimiter,
|
|
151
|
+
request: { ip: '10.10.0.1' },
|
|
152
|
+
});
|
|
153
|
+
expect(isNextCalled).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('middleware should works with a memory drivers', async () => {
|
|
157
|
+
expect.assertions(1);
|
|
158
|
+
const { isNextCalled } = await makeOneRequest({
|
|
159
|
+
driver: 'memory',
|
|
160
|
+
request: { ip: '10.10.0.1' },
|
|
161
|
+
});
|
|
162
|
+
expect(isNextCalled).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('middleware should works with a redis drivers', async () => {
|
|
166
|
+
expect.assertions(1);
|
|
167
|
+
const { isNextCalled } = await makeOneRequest({
|
|
168
|
+
driver: 'redis',
|
|
169
|
+
request: { ip: '10.10.0.1' },
|
|
170
|
+
});
|
|
171
|
+
expect(isNextCalled).toBe(true);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('middleware should rate limits for us. mongo driver', async () => {
|
|
175
|
+
expect.assertions(2);
|
|
176
|
+
|
|
177
|
+
const middlewares = Array.from({ length: 20 }, () =>
|
|
178
|
+
makeOneRequest({ rateLimiter: mongoRateLimiter }),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const data = await Promise.all(middlewares);
|
|
182
|
+
|
|
183
|
+
const status = data.find((obj) => obj.status === 429);
|
|
184
|
+
const isSend = data.find((obj) => obj.isSend);
|
|
185
|
+
|
|
186
|
+
expect(status.status).toBe(429);
|
|
187
|
+
expect(isSend.isSend).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('middleware should rate limits for us. memory driver', async () => {
|
|
191
|
+
expect.assertions(2);
|
|
192
|
+
|
|
193
|
+
const rateLimiter = new RateLimiter(global.server.app, {
|
|
194
|
+
driver: 'memory',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const middlewares = Array.from({ length: 20 }, () =>
|
|
198
|
+
makeOneRequest({ rateLimiter }),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const data = await Promise.all(middlewares);
|
|
202
|
+
|
|
203
|
+
const status = data.find((obj) => obj.status === 429);
|
|
204
|
+
const isSend = data.find((obj) => obj.isSend);
|
|
205
|
+
|
|
206
|
+
expect(status.status).toBe(429);
|
|
207
|
+
expect(isSend.isSend).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('middleware should rate limits for us. redis driver', async () => {
|
|
211
|
+
expect.assertions(2);
|
|
212
|
+
|
|
213
|
+
const rateLimiter = new RateLimiter(global.server.app, {
|
|
214
|
+
driver: 'redis',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const middlewares = Array.from({ length: 20 }, () =>
|
|
218
|
+
makeOneRequest({ rateLimiter }),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const data = await Promise.all(middlewares);
|
|
222
|
+
|
|
223
|
+
const status = data.find((obj) => obj.status === 429);
|
|
224
|
+
const isSend = data.find((obj) => obj.isSend);
|
|
225
|
+
|
|
226
|
+
expect(status.status).toBe(429);
|
|
227
|
+
expect(isSend.isSend).toBe(true);
|
|
228
|
+
});
|
|
50
229
|
});
|
package/tests/setup.js
CHANGED
|
@@ -14,7 +14,7 @@ jest.setTimeout(1000000);
|
|
|
14
14
|
beforeAll(async () => {
|
|
15
15
|
mongoMemoryServerInstance = await MongoMemoryReplSet.create({
|
|
16
16
|
// binary: { version: '4.4.6' },
|
|
17
|
-
replSet: { storageEngine: 'wiredTiger' },
|
|
17
|
+
replSet: { count: 1, storageEngine: 'wiredTiger' },
|
|
18
18
|
});
|
|
19
19
|
await mongoMemoryServerInstance.waitUntilRunning();
|
|
20
20
|
process.env.LOGGER_CONSOLE_LEVEL = 'error';
|