@adaptivestone/framework 3.4.2 → 4.0.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 +30 -0
- package/LICENCE +21 -0
- package/cluster.js +3 -3
- package/commands/CreateUser.js +27 -0
- package/commands/Documentation.js +1 -1
- package/commands/GetOpenApiJson.js +53 -23
- package/commands/migration/Create.js +2 -2
- package/config/auth.js +1 -1
- package/config/i18n.js +4 -3
- package/config/mail.js +5 -1
- package/controllers/Home.js +2 -2
- package/controllers/Home.test.js +11 -0
- package/controllers/index.js +15 -15
- package/folderConfig.js +1 -1
- package/helpers/yup.js +24 -0
- package/index.js +8 -0
- package/models/User.js +38 -27
- package/models/User.test.js +68 -18
- package/modules/AbstractController.js +144 -208
- package/modules/AbstractModel.js +2 -1
- package/modules/Base.js +3 -2
- package/modules/BaseCli.js +6 -2
- package/package.json +18 -14
- package/server.d.ts +1 -1
- package/server.js +25 -8
- package/services/cache/Cache.d.ts +3 -3
- package/services/cache/Cache.js +17 -3
- package/services/documentation/DocumentationGenerator.js +171 -0
- package/services/http/HttpServer.js +16 -96
- package/services/http/middleware/AbstractMiddleware.js +20 -0
- package/services/http/middleware/GetUserByToken.js +4 -0
- package/services/http/middleware/I18n.js +119 -0
- package/services/http/middleware/I18n.test.js +77 -0
- package/services/http/middleware/Pagination.js +56 -0
- package/services/http/middleware/PrepareAppInfo.test.js +22 -0
- package/services/http/middleware/{Middlewares.test.js → RateLimiter.test.js} +1 -1
- package/services/http/middleware/RequestLogger.js +22 -0
- package/services/http/middleware/RequestParser.js +36 -0
- package/services/messaging/email/index.js +141 -41
- package/services/messaging/email/resources/.gitkeep +1 -0
- package/services/validate/ValidateService.js +161 -0
- package/services/validate/ValidateService.test.js +105 -0
- package/services/validate/drivers/AbstractValidator.js +37 -0
- package/services/validate/drivers/CustomValidator.js +52 -0
- package/services/validate/drivers/YupValidator.js +103 -0
- package/tests/setup.js +13 -5
- package/services/messaging/email/templates/emptyTemplate/style.less +0 -0
- package/services/messaging/email/templates/password/html.handlebars +0 -13
- package/services/messaging/email/templates/password/style.less +0 -0
- package/services/messaging/email/templates/password/subject.handlebars +0 -1
- package/services/messaging/email/templates/password/text.handlebars +0 -1
- package/services/messaging/email/templates/verification/style.less +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
### 4.0.0 - DEV
|
|
2
|
+
|
|
3
|
+
[BREAKING] change bcrypt encryption with scrypt
|
|
4
|
+
[BREAKING] change internal express parser to formidable parser. Affect you if external formidable is used
|
|
5
|
+
[BREAKING] should not affect any user. Changed email-templates module to internal implementation. Idea to keep dependensy list smaller
|
|
6
|
+
[BREAKING] change i18n middleware to internal one. Nothing should be affected
|
|
7
|
+
[BREAKING] now validation of request splitted between request and query
|
|
8
|
+
[BREAKING] supportedLngs option added to i18n config
|
|
9
|
+
[BREAKING] email inliner now looking for src/services/messaging/email/resources folder instead of 'build' folder.
|
|
10
|
+
|
|
11
|
+
[BREAKING] Mongoose v7. https://mongoosejs.com/docs/migrating_to_7.html
|
|
12
|
+
[BREAKING] Yup validation was updated to v1 https://github.com/jquense/yup/issues/1906
|
|
13
|
+
|
|
14
|
+
[DEPRECATED] getExpress path is deprecated. Renamed to getHttpPath
|
|
15
|
+
|
|
16
|
+
[NEW] pagination middleware
|
|
17
|
+
[NEW] requestLogger middleware. Migrated from core server to be an middleware
|
|
18
|
+
[NEW] CreateUser command
|
|
19
|
+
[NEW] custom yup validator for validate File requests
|
|
20
|
+
[UPDATE] updated deps
|
|
21
|
+
[UPDATE] openApi generator support files
|
|
22
|
+
[UPDATE] updated 18n middleware. Introduced internal cachce. Speed up of request processing up to 100%
|
|
23
|
+
[UPDATE] cache drivers to JSON support BigInt numbers
|
|
24
|
+
|
|
25
|
+
### 3.4.3
|
|
26
|
+
|
|
27
|
+
[UPDATE] updated deps
|
|
28
|
+
[FIX] fix tests for redis
|
|
29
|
+
[FIX] support in tests TEST_FOLDER_EMAILS
|
|
30
|
+
|
|
1
31
|
### 3.4.2
|
|
2
32
|
|
|
3
33
|
[UPDATE] updated deps
|
package/LICENCE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2016-2023 Andrei Lahunou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/cluster.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const cluster = require('cluster');
|
|
2
|
-
const numCPUs = require('os').cpus().length;
|
|
1
|
+
const cluster = require('node:cluster');
|
|
2
|
+
const numCPUs = require('node:os').cpus().length;
|
|
3
3
|
|
|
4
4
|
if (cluster.isMaster) {
|
|
5
5
|
// eslint-disable-next-line no-console
|
|
@@ -22,5 +22,5 @@ if (cluster.isMaster) {
|
|
|
22
22
|
});
|
|
23
23
|
} else {
|
|
24
24
|
// eslint-disable-next-line global-require
|
|
25
|
-
require('./
|
|
25
|
+
require('./index');
|
|
26
26
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const AbstractCommand = require('../modules/AbstractCommand');
|
|
2
|
+
|
|
3
|
+
// Example: node src/cli createuser --email=somemail@gmail.com --password=somePassword --roles=user,admin,someOtherRoles
|
|
4
|
+
class CreateUser extends AbstractCommand {
|
|
5
|
+
async run() {
|
|
6
|
+
const User = this.app.getModel('User');
|
|
7
|
+
const { email, password, roles } = this.args;
|
|
8
|
+
|
|
9
|
+
if (!email || !password) {
|
|
10
|
+
this.logger.error('Input validation failded');
|
|
11
|
+
this.logger.error('Please add "email" and "password" variables');
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const user = await User.create({
|
|
15
|
+
email,
|
|
16
|
+
password,
|
|
17
|
+
roles: roles?.split(','),
|
|
18
|
+
});
|
|
19
|
+
await user.generateToken();
|
|
20
|
+
|
|
21
|
+
this.logger.info(`User was created ${JSON.stringify(user, 0, 4)}`);
|
|
22
|
+
|
|
23
|
+
return user;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = CreateUser;
|
|
@@ -5,7 +5,7 @@ class Documentation extends AbstractCommand {
|
|
|
5
5
|
async run() {
|
|
6
6
|
const CM = new ControllerManager(this.app);
|
|
7
7
|
this.app.documentation = [];
|
|
8
|
-
await CM.initControllers(
|
|
8
|
+
await CM.initControllers();
|
|
9
9
|
return this.app.documentation;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const fs = require('fs').promises;
|
|
1
|
+
const fs = require('node:fs').promises;
|
|
2
2
|
const AbstractCommand = require('../modules/AbstractCommand');
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -165,6 +165,7 @@ class GetOpenApiJson extends AbstractCommand {
|
|
|
165
165
|
const routeTitle = route[Object.keys(route)[0]].name;
|
|
166
166
|
|
|
167
167
|
const routeFields = route[Object.keys(route)[0]].fields;
|
|
168
|
+
const routeQueryFields = route[Object.keys(route)[0]].queryFields;
|
|
168
169
|
|
|
169
170
|
if (!openApi.paths[routeName][methodName]) {
|
|
170
171
|
openApi.paths[routeName][methodName] = {};
|
|
@@ -202,6 +203,17 @@ class GetOpenApiJson extends AbstractCommand {
|
|
|
202
203
|
},
|
|
203
204
|
};
|
|
204
205
|
|
|
206
|
+
for (const queryField of routeQueryFields) {
|
|
207
|
+
openApi.paths[routeName][methodName].parameters.push({
|
|
208
|
+
name: queryField.name,
|
|
209
|
+
in: 'query',
|
|
210
|
+
required: queryField?.required,
|
|
211
|
+
schema: {
|
|
212
|
+
type: queryField.type,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
205
217
|
for (const routeField of routeParameters) {
|
|
206
218
|
openApi.paths[routeName][methodName].parameters.push({
|
|
207
219
|
name: routeField,
|
|
@@ -216,34 +228,36 @@ class GetOpenApiJson extends AbstractCommand {
|
|
|
216
228
|
if (routeFields.length) {
|
|
217
229
|
const groupBodyFields = {};
|
|
218
230
|
const requiredFields = [];
|
|
231
|
+
let isMultipartFormaData = false;
|
|
219
232
|
for (const field of routeFields) {
|
|
220
|
-
if (field.
|
|
233
|
+
if (field.required) {
|
|
221
234
|
requiredFields.push(field.name);
|
|
222
235
|
}
|
|
223
236
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
objectFields[objField.name] = {
|
|
229
|
-
// fields file has mixed type but openApi doesnt have this type
|
|
230
|
-
type: objField.type === 'mixed' ? 'string' : objField.type,
|
|
237
|
+
switch (field.type) {
|
|
238
|
+
case 'object':
|
|
239
|
+
groupBodyFields[field.name] = {
|
|
240
|
+
properties: {},
|
|
231
241
|
};
|
|
232
|
-
}
|
|
233
242
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
for (const objField of field.fields) {
|
|
244
|
+
groupBodyFields[field.name].properties[objField.name] = {
|
|
245
|
+
// fields file has mixed type but openApi doesnt have this type
|
|
246
|
+
type: objField.type === 'mixed' ? 'string' : objField.type,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
break;
|
|
239
251
|
|
|
240
|
-
|
|
241
|
-
groupBodyFields[field.name]
|
|
242
|
-
|
|
252
|
+
case 'array':
|
|
253
|
+
groupBodyFields[field.name] = {
|
|
254
|
+
items: {
|
|
255
|
+
type: field.innerType,
|
|
256
|
+
},
|
|
243
257
|
};
|
|
244
|
-
|
|
258
|
+
break;
|
|
245
259
|
|
|
246
|
-
|
|
260
|
+
case 'lazy':
|
|
247
261
|
groupBodyFields[field.name] = {
|
|
248
262
|
oneOf: [
|
|
249
263
|
{
|
|
@@ -254,13 +268,29 @@ class GetOpenApiJson extends AbstractCommand {
|
|
|
254
268
|
},
|
|
255
269
|
],
|
|
256
270
|
};
|
|
257
|
-
|
|
271
|
+
break;
|
|
272
|
+
|
|
273
|
+
case 'file':
|
|
274
|
+
groupBodyFields[field.name] = {
|
|
275
|
+
type: 'string',
|
|
276
|
+
format: 'binary',
|
|
277
|
+
};
|
|
278
|
+
isMultipartFormaData = true;
|
|
279
|
+
break;
|
|
280
|
+
default:
|
|
281
|
+
groupBodyFields[field.name] = {
|
|
282
|
+
type: field.type,
|
|
283
|
+
};
|
|
258
284
|
}
|
|
259
285
|
}
|
|
260
286
|
|
|
287
|
+
const contentType = isMultipartFormaData
|
|
288
|
+
? 'multipart/form-data'
|
|
289
|
+
: 'application/json';
|
|
290
|
+
|
|
261
291
|
openApi.paths[routeName][methodName].requestBody = {
|
|
262
292
|
content: {
|
|
263
|
-
|
|
293
|
+
[contentType]: {
|
|
264
294
|
schema: {
|
|
265
295
|
type: 'object',
|
|
266
296
|
properties: groupBodyFields,
|
|
@@ -271,7 +301,7 @@ class GetOpenApiJson extends AbstractCommand {
|
|
|
271
301
|
|
|
272
302
|
if (requiredFields.length) {
|
|
273
303
|
openApi.paths[routeName][methodName].requestBody.content[
|
|
274
|
-
|
|
304
|
+
contentType
|
|
275
305
|
].schema.required = requiredFields;
|
|
276
306
|
}
|
|
277
307
|
}
|
package/config/auth.js
CHANGED
package/config/i18n.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* You can use any options from https://www.i18next.com/overview/configuration-options
|
|
3
|
+
*/
|
|
1
4
|
module.exports = {
|
|
2
5
|
enabled: true,
|
|
3
6
|
preload: ['en', 'ru'],
|
|
7
|
+
supportedLngs: ['en', 'ru'], // should be at least one supported
|
|
4
8
|
fallbackLng: 'en',
|
|
5
9
|
saveMissing: false,
|
|
6
10
|
debug: false,
|
|
7
|
-
// https://github.com/i18next/i18next-http-middleware#detector-options
|
|
8
|
-
// in additional we have 'xLang' that detect language based on header 'xLang'
|
|
9
|
-
langDetectionOders: ['xLang'], // from order option
|
|
10
11
|
lookupQuerystring: 'lng', // string to detect language on query
|
|
11
12
|
};
|
package/config/mail.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const path = require('node:path');
|
|
2
|
+
|
|
1
3
|
module.exports = {
|
|
2
4
|
from: 'Localhost <info@localhost>',
|
|
3
5
|
transports: {
|
|
@@ -20,6 +22,8 @@ module.exports = {
|
|
|
20
22
|
transport: process.env.EMAIL_TRANSPORT || 'smtp',
|
|
21
23
|
webResources: {
|
|
22
24
|
// https://github.com/jrit/web-resource-inliner path to find resources
|
|
23
|
-
relativeTo: '
|
|
25
|
+
relativeTo: path.resolve('src/services/messaging/email/resources'),
|
|
26
|
+
images: false,
|
|
24
27
|
},
|
|
28
|
+
globalVariablesToTemplates: {},
|
|
25
29
|
};
|
package/controllers/Home.js
CHANGED
|
@@ -12,11 +12,11 @@ class Home extends AbstractController {
|
|
|
12
12
|
|
|
13
13
|
// eslint-disable-next-line class-methods-use-this
|
|
14
14
|
async home(req, res) {
|
|
15
|
-
res.
|
|
15
|
+
return res.json({ message: 'Home' });
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// eslint-disable-next-line class-methods-use-this
|
|
19
|
-
|
|
19
|
+
getHttpPath() {
|
|
20
20
|
return '/';
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const request = require('supertest');
|
|
2
|
+
|
|
3
|
+
describe('home', () => {
|
|
4
|
+
it('can open home have', async () => {
|
|
5
|
+
expect.assertions(1);
|
|
6
|
+
const { status } = await request(global.server.app.httpServer.express).get(
|
|
7
|
+
'/',
|
|
8
|
+
);
|
|
9
|
+
expect(status).toBe(200);
|
|
10
|
+
});
|
|
11
|
+
});
|
package/controllers/index.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
|
-
const path = require('path');
|
|
1
|
+
const path = require('node:path');
|
|
2
2
|
const Base = require('../modules/Base');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Class do autoloading a http
|
|
5
|
+
* Class do autoloading a http controllers
|
|
6
6
|
*/
|
|
7
7
|
class ControllerManager extends Base {
|
|
8
8
|
constructor(app) {
|
|
9
9
|
super(app);
|
|
10
|
-
this.
|
|
10
|
+
this.controllers = {};
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Load controllers
|
|
15
|
-
* @param {object} folderConfig
|
|
16
|
-
* @param {object} folderConfig.folders server folder config
|
|
17
|
-
* @param {string} folderConfig.controllers controller folder path
|
|
18
15
|
*/
|
|
19
|
-
async initControllers(
|
|
16
|
+
async initControllers() {
|
|
20
17
|
const controllersToLoad = await this.getFilesPathWithInheritance(
|
|
21
18
|
__dirname,
|
|
22
|
-
|
|
19
|
+
this.app.foldersConfig.controllers,
|
|
23
20
|
);
|
|
24
21
|
|
|
25
22
|
controllersToLoad.sort((a, b) => {
|
|
@@ -31,9 +28,12 @@ class ControllerManager extends Base {
|
|
|
31
28
|
}
|
|
32
29
|
return 0;
|
|
33
30
|
});
|
|
34
|
-
|
|
31
|
+
// const controllers = [];
|
|
35
32
|
for (const controller of controllersToLoad) {
|
|
36
|
-
//
|
|
33
|
+
// TODO wait until https://github.com/nodejs/node/issues/35889
|
|
34
|
+
// controllers.push(
|
|
35
|
+
// import(controller.path).then(({ default: ControllerModule }) => {
|
|
36
|
+
// eslint-disable-next-line import/no-dynamic-require, global-require
|
|
37
37
|
const ControllerModule = require(controller.path);
|
|
38
38
|
const contollerName = ControllerModule.name.toLowerCase();
|
|
39
39
|
let prefix = path.dirname(controller.file);
|
|
@@ -41,13 +41,13 @@ class ControllerManager extends Base {
|
|
|
41
41
|
prefix = '';
|
|
42
42
|
}
|
|
43
43
|
const controllePath = prefix
|
|
44
|
-
? `${
|
|
44
|
+
? `${prefix}/${contollerName}`
|
|
45
45
|
: contollerName;
|
|
46
|
-
this.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
46
|
+
this.controllers[controllePath] = new ControllerModule(this.app, prefix);
|
|
47
|
+
// }),
|
|
48
|
+
// );
|
|
50
49
|
}
|
|
50
|
+
// await Promise.all(controllers);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
static get loggerGroup() {
|
package/folderConfig.js
CHANGED
package/helpers/yup.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { Schema } = require('yup');
|
|
2
|
+
const formidable = require('formidable');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validator for file
|
|
6
|
+
* use as
|
|
7
|
+
* @example
|
|
8
|
+
* request: yup.object().shape({
|
|
9
|
+
* someFile: new YupFile().required(),
|
|
10
|
+
* })
|
|
11
|
+
*/
|
|
12
|
+
class YupFile extends Schema {
|
|
13
|
+
constructor() {
|
|
14
|
+
super({
|
|
15
|
+
type: 'file',
|
|
16
|
+
check: (value) => value instanceof formidable.PersistentFile,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = {
|
|
22
|
+
// eslint-disable-next-line import/prefer-default-export
|
|
23
|
+
YupFile,
|
|
24
|
+
};
|
package/index.js
ADDED
package/models/User.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-param-reassign */
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
const { scrypt } = require('node:crypto');
|
|
4
|
+
const { promisify } = require('node:util');
|
|
3
5
|
|
|
4
6
|
const AbstractModel = require('../modules/AbstractModel');
|
|
5
7
|
|
|
@@ -9,12 +11,12 @@ class User extends AbstractModel {
|
|
|
9
11
|
constructor(app) {
|
|
10
12
|
super(app);
|
|
11
13
|
const authConfig = this.app.getConfig('auth');
|
|
12
|
-
this.
|
|
14
|
+
this.hashRounds = authConfig.hashRounds;
|
|
13
15
|
this.saltSecret = authConfig.saltSecret;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
initHooks() {
|
|
17
|
-
this.mongooseSchema.pre('save', async function () {
|
|
19
|
+
this.mongooseSchema.pre('save', async function userPreSaveHook() {
|
|
18
20
|
if (this.isModified('password')) {
|
|
19
21
|
this.password = await this.constructor.hashPassword(this.password);
|
|
20
22
|
}
|
|
@@ -72,12 +74,9 @@ class User extends AbstractModel {
|
|
|
72
74
|
if (!data) {
|
|
73
75
|
return false;
|
|
74
76
|
}
|
|
75
|
-
const
|
|
76
|
-
String(password) + data.constructor.getSuper().saltSecret,
|
|
77
|
-
data.password,
|
|
78
|
-
);
|
|
77
|
+
const hashedPasswords = await this.hashPassword(password);
|
|
79
78
|
|
|
80
|
-
if (
|
|
79
|
+
if (data.password !== hashedPasswords) {
|
|
81
80
|
return false;
|
|
82
81
|
}
|
|
83
82
|
return data;
|
|
@@ -86,10 +85,13 @@ class User extends AbstractModel {
|
|
|
86
85
|
async generateToken() {
|
|
87
86
|
const timestamp = new Date();
|
|
88
87
|
timestamp.setDate(timestamp.getDate() + 30);
|
|
89
|
-
const
|
|
88
|
+
const scryptAsync = promisify(scrypt);
|
|
89
|
+
const data = await scryptAsync(
|
|
90
90
|
this.email + Date.now(),
|
|
91
|
-
this.constructor.getSuper().
|
|
91
|
+
this.constructor.getSuper().saltSecret,
|
|
92
|
+
this.constructor.getSuper().hashRounds,
|
|
92
93
|
);
|
|
94
|
+
const token = data.toString('base64url');
|
|
93
95
|
this.sessionTokens.push({ token, valid: timestamp });
|
|
94
96
|
await this.save();
|
|
95
97
|
return { token, valid: timestamp };
|
|
@@ -108,10 +110,13 @@ class User extends AbstractModel {
|
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
static async hashPassword(password) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
const scryptAsync = promisify(scrypt);
|
|
114
|
+
const data = await scryptAsync(
|
|
115
|
+
String(password),
|
|
116
|
+
this.getSuper().saltSecret,
|
|
117
|
+
this.getSuper().hashRounds,
|
|
114
118
|
);
|
|
119
|
+
return data.toString('base64url');
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
static async getUserByToken(token) {
|
|
@@ -130,10 +135,13 @@ class User extends AbstractModel {
|
|
|
130
135
|
static async generateUserPasswordRecoveryToken(userMongoose) {
|
|
131
136
|
const date = new Date();
|
|
132
137
|
date.setDate(date.getDate() + 14);
|
|
133
|
-
const
|
|
138
|
+
const scryptAsync = promisify(scrypt);
|
|
139
|
+
const data = await scryptAsync(
|
|
134
140
|
userMongoose.email + Date.now(),
|
|
135
|
-
userMongoose.constructor.getSuper().
|
|
141
|
+
userMongoose.constructor.getSuper().saltSecret,
|
|
142
|
+
userMongoose.constructor.getSuper().hashRounds,
|
|
136
143
|
);
|
|
144
|
+
const token = data.toString('base64url');
|
|
137
145
|
// if (err) {
|
|
138
146
|
// this.logger.error("Hash 2 error ", err);
|
|
139
147
|
// reject(err);
|
|
@@ -184,10 +192,13 @@ class User extends AbstractModel {
|
|
|
184
192
|
static async generateUserVerificationToken(userMongoose) {
|
|
185
193
|
const date = new Date();
|
|
186
194
|
date.setDate(date.getDate() + 14);
|
|
187
|
-
const
|
|
195
|
+
const scryptAsync = promisify(scrypt);
|
|
196
|
+
const data = await scryptAsync(
|
|
188
197
|
userMongoose.email + Date.now(),
|
|
189
|
-
userMongoose.constructor.getSuper().
|
|
198
|
+
userMongoose.constructor.getSuper().saltSecret,
|
|
199
|
+
userMongoose.constructor.getSuper().hashRounds,
|
|
190
200
|
);
|
|
201
|
+
const token = data.toString('base64url');
|
|
191
202
|
// if (err) {
|
|
192
203
|
// this.logger.error("Hash 2 error ", err);
|
|
193
204
|
// reject(err);
|
|
@@ -219,16 +230,16 @@ class User extends AbstractModel {
|
|
|
219
230
|
return result;
|
|
220
231
|
}
|
|
221
232
|
|
|
222
|
-
removeVerificationToken(verificationToken) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
233
|
+
// async removeVerificationToken(verificationToken) {
|
|
234
|
+
// this.mongooseModel.updateOne(
|
|
235
|
+
// {
|
|
236
|
+
// verificationTokens: {
|
|
237
|
+
// $elemMatch: { token: String(verificationToken) },
|
|
238
|
+
// },
|
|
239
|
+
// },
|
|
240
|
+
// { $pop: { verificationTokens: 1 } },
|
|
241
|
+
// );
|
|
242
|
+
// }
|
|
232
243
|
|
|
233
244
|
async sendVerificationEmail(i18n) {
|
|
234
245
|
const verificationToken = await User.generateUserVerificationToken(this);
|
package/models/User.test.js
CHANGED
|
@@ -22,7 +22,7 @@ describe('user model', () => {
|
|
|
22
22
|
const user = await global.server.app.getModel('User').findOne({
|
|
23
23
|
email: userEmail,
|
|
24
24
|
});
|
|
25
|
-
expect(user.password
|
|
25
|
+
expect(user.password).not.toBe(userPassword);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it('passwords should not be changed on other fields save', async () => {
|
|
@@ -32,22 +32,74 @@ describe('user model', () => {
|
|
|
32
32
|
});
|
|
33
33
|
const psw = user.password;
|
|
34
34
|
user.email = 'rrrr';
|
|
35
|
-
user.save();
|
|
35
|
+
await user.save();
|
|
36
|
+
user.email = userEmail;
|
|
37
|
+
await user.save();
|
|
36
38
|
|
|
37
|
-
expect(user.password
|
|
39
|
+
expect(user.password).toBe(psw);
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
describe('
|
|
42
|
+
describe('getUserByEmailAndPassword', () => {
|
|
43
|
+
it('should WORK with valid creds', async () => {
|
|
44
|
+
expect.assertions(1);
|
|
45
|
+
const userModel = await global.server.app.getModel('User');
|
|
46
|
+
const user = await userModel.getUserByEmailAndPassword(
|
|
47
|
+
userEmail,
|
|
48
|
+
userPassword,
|
|
49
|
+
);
|
|
50
|
+
expect(user.id).toBe(globalUser.id);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should NOT with INvalid creds', async () => {
|
|
54
|
+
expect.assertions(1);
|
|
55
|
+
const userModel = await global.server.app.getModel('User');
|
|
56
|
+
const user = await userModel.getUserByEmailAndPassword(
|
|
57
|
+
userEmail,
|
|
58
|
+
'wrongPassword',
|
|
59
|
+
);
|
|
60
|
+
expect(user).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should NOT with wrong email', async () => {
|
|
64
|
+
expect.assertions(1);
|
|
65
|
+
const userModel = await global.server.app.getModel('User');
|
|
66
|
+
const user = await userModel.getUserByEmailAndPassword(
|
|
67
|
+
'not@exists.com',
|
|
68
|
+
userPassword,
|
|
69
|
+
);
|
|
70
|
+
expect(user).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('getUserByToken', () => {
|
|
41
75
|
it('should NOT work for non valid token', async () => {
|
|
42
|
-
expect.assertions(
|
|
76
|
+
expect.assertions(1);
|
|
77
|
+
|
|
78
|
+
const user = await global.server.app
|
|
79
|
+
.getModel('User')
|
|
80
|
+
.getUserByToken('fake one');
|
|
81
|
+
expect(user).toBe(false);
|
|
82
|
+
});
|
|
43
83
|
|
|
84
|
+
it('should work for VALID token', async () => {
|
|
85
|
+
expect.assertions(1);
|
|
86
|
+
const token = await globalUser.generateToken();
|
|
44
87
|
const user = await global.server.app
|
|
45
88
|
.getModel('User')
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
89
|
+
.getUserByToken(token.token);
|
|
90
|
+
expect(user.id).toBe(globalUser.id);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('getUserByVerificationToken', () => {
|
|
95
|
+
it('should NOT work for non valid token', async () => {
|
|
96
|
+
expect.assertions(1);
|
|
97
|
+
|
|
98
|
+
await expect(
|
|
99
|
+
global.server.app
|
|
100
|
+
.getModel('User')
|
|
101
|
+
.getUserByVerificationToken('fake one'),
|
|
102
|
+
).rejects.toStrictEqual(new Error('User not exists'));
|
|
51
103
|
});
|
|
52
104
|
|
|
53
105
|
it('should work for VALID token', async () => {
|
|
@@ -65,15 +117,13 @@ describe('user model', () => {
|
|
|
65
117
|
|
|
66
118
|
describe('getUserByPasswordRecoveryToken', () => {
|
|
67
119
|
it('should NOT work for non valid token', async () => {
|
|
68
|
-
expect.assertions(
|
|
120
|
+
expect.assertions(1);
|
|
69
121
|
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
expect(user).toBeUndefined();
|
|
122
|
+
await expect(
|
|
123
|
+
global.server.app
|
|
124
|
+
.getModel('User')
|
|
125
|
+
.getUserByPasswordRecoveryToken('fake one'),
|
|
126
|
+
).rejects.toStrictEqual(new Error('User not exists'));
|
|
77
127
|
});
|
|
78
128
|
|
|
79
129
|
it('should work for VALID token', async () => {
|