@lenne.tech/nest-server 2.0.4 → 3.1.2
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/dist/config.env.js +14 -1
- package/dist/config.env.js.map +1 -1
- package/dist/core/common/helpers/service.helper.d.ts +3 -1
- package/dist/core/common/helpers/service.helper.js +23 -1
- package/dist/core/common/helpers/service.helper.js.map +1 -1
- package/dist/core/common/interfaces/mailjet-options.interface.d.ts +4 -0
- package/dist/core/common/interfaces/mailjet-options.interface.js +3 -0
- package/dist/core/common/interfaces/mailjet-options.interface.js.map +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
- package/dist/core/common/services/mailjet.service.d.ts +15 -0
- package/dist/core/common/services/mailjet.service.js +64 -0
- package/dist/core/common/services/mailjet.service.js.map +1 -0
- package/dist/core/modules/user/core-basic-user.service.d.ts +1 -1
- package/dist/core/modules/user/core-user.model.d.ts +3 -0
- package/dist/core/modules/user/core-user.model.js +17 -0
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/core/modules/user/core-user.service.d.ts +10 -4
- package/dist/core/modules/user/core-user.service.js +72 -3
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/core.module.js +3 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server/modules/user/user.model.d.ts +1 -1
- package/dist/server/modules/user/user.resolver.d.ts +3 -0
- package/dist/server/modules/user/user.resolver.js +31 -0
- package/dist/server/modules/user/user.resolver.js.map +1 -1
- package/dist/server/modules/user/user.service.js +3 -4
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/server/server.module.js.map +1 -1
- package/dist/test/test.helper.js +1 -1
- package/dist/test/test.helper.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +42 -48
- package/src/config.env.ts +14 -1
- package/src/core/common/helpers/service.helper.ts +34 -4
- package/src/core/common/interfaces/mailjet-options.interface.ts +4 -0
- package/src/core/common/interfaces/server-options.interface.ts +12 -0
- package/src/core/common/services/mailjet.service.ts +84 -0
- package/src/core/modules/user/core-basic-user.service.ts +3 -3
- package/src/core/modules/user/core-user.model.ts +20 -0
- package/src/core/modules/user/core-user.service.ts +131 -10
- package/src/core.module.ts +3 -1
- package/src/index.ts +1 -0
- package/src/server/modules/user/user.resolver.ts +24 -0
- package/src/server/modules/user/user.service.ts +6 -4
- package/src/server/server.module.ts +0 -1
- package/src/templates/password-reset.ejs +3 -0
- package/src/templates/welcome.ejs +3 -2
- package/src/test/test.helper.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -49,48 +49,40 @@
|
|
|
49
49
|
"url": "https://github.com/lenneTech/nest-server/issues"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
|
-
"node": ">=
|
|
53
|
-
},
|
|
54
|
-
"peerDependencies": {
|
|
55
|
-
"@nestjs/testing": "8.1.2",
|
|
56
|
-
"@nestjs/core": "8.1.2",
|
|
57
|
-
"@nestjs/common": "8.1.2",
|
|
58
|
-
"@nestjs/graphql": "9.1.1",
|
|
59
|
-
"@nestjs/jwt": "8.0.0",
|
|
60
|
-
"@nestjs/mongoose": "9.0.1",
|
|
61
|
-
"@nestjs/passport": "8.0.1",
|
|
62
|
-
"@nestjs/platform-express": "8.1.2"
|
|
52
|
+
"node": ">= 16.13.0"
|
|
63
53
|
},
|
|
64
54
|
"dependencies": {
|
|
65
|
-
"@apollo/federation": "0.33.
|
|
55
|
+
"@apollo/federation": "0.33.9",
|
|
66
56
|
"@apollo/gateway": "0.42.3",
|
|
67
|
-
"@nestjs/
|
|
68
|
-
"@nestjs/core": "8.
|
|
69
|
-
"@nestjs/
|
|
70
|
-
"@nestjs/graphql": "9.1.1",
|
|
57
|
+
"@nestjs/common": "8.2.6",
|
|
58
|
+
"@nestjs/core": "8.2.6",
|
|
59
|
+
"@nestjs/graphql": "9.1.2",
|
|
71
60
|
"@nestjs/jwt": "8.0.0",
|
|
72
|
-
"@nestjs/mongoose": "9.0.
|
|
73
|
-
"@nestjs/passport": "8.0
|
|
74
|
-
"@nestjs/platform-express": "8.
|
|
61
|
+
"@nestjs/mongoose": "9.0.2",
|
|
62
|
+
"@nestjs/passport": "8.1.0",
|
|
63
|
+
"@nestjs/platform-express": "8.2.6",
|
|
64
|
+
"@nestjs/testing": "8.2.6",
|
|
65
|
+
"@shelf/jest-mongodb": "2.2.0",
|
|
75
66
|
"@types/ejs": "3.1.0",
|
|
76
|
-
"@types/jest": "27.0
|
|
77
|
-
"@types/lodash": "4.14.
|
|
67
|
+
"@types/jest": "27.4.0",
|
|
68
|
+
"@types/lodash": "4.14.178",
|
|
78
69
|
"@types/multer": "1.4.7",
|
|
79
|
-
"@types/node": "16.11.
|
|
70
|
+
"@types/node": "16.11.21",
|
|
71
|
+
"@types/node-mailjet": "3.3.8",
|
|
80
72
|
"@types/nodemailer": "6.4.4",
|
|
81
73
|
"@types/passport": "1.0.7",
|
|
82
74
|
"@types/supertest": "2.0.11",
|
|
83
|
-
"@typescript-eslint/eslint-plugin": "5.
|
|
84
|
-
"@typescript-eslint/parser": "5.
|
|
85
|
-
"apollo-server-core": "3.
|
|
86
|
-
"apollo-server-express": "3.
|
|
75
|
+
"@typescript-eslint/eslint-plugin": "5.10.0",
|
|
76
|
+
"@typescript-eslint/parser": "5.10.0",
|
|
77
|
+
"apollo-server-core": "3.6.2",
|
|
78
|
+
"apollo-server-express": "3.6.2",
|
|
87
79
|
"bcrypt": "5.0.1",
|
|
88
|
-
"class-transformer": "0.
|
|
89
|
-
"class-validator": "0.13.
|
|
80
|
+
"class-transformer": "0.5.1",
|
|
81
|
+
"class-validator": "0.13.2",
|
|
90
82
|
"coffeescript": "2.6.1",
|
|
91
83
|
"ejs": "3.1.6",
|
|
92
|
-
"fastify": "3.
|
|
93
|
-
"graphql": "15.
|
|
84
|
+
"fastify": "3.27.0",
|
|
85
|
+
"graphql": "15.8.0",
|
|
94
86
|
"graphql-subscriptions": "2.0.0",
|
|
95
87
|
"grunt": "1.4.1",
|
|
96
88
|
"grunt-bg-shell": "2.3.3",
|
|
@@ -98,33 +90,35 @@
|
|
|
98
90
|
"grunt-contrib-watch": "1.1.0",
|
|
99
91
|
"grunt-sync": "0.8.2",
|
|
100
92
|
"husky": "7.0.4",
|
|
101
|
-
"jest": "27.
|
|
102
|
-
"json-to-graphql-query": "2.
|
|
103
|
-
"light-my-request": "4.
|
|
93
|
+
"jest": "27.4.7",
|
|
94
|
+
"json-to-graphql-query": "2.2.0",
|
|
95
|
+
"light-my-request": "4.7.0",
|
|
104
96
|
"lodash": "4.17.21",
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
97
|
+
"mongodb": "4.3.0",
|
|
98
|
+
"mongoose": "6.1.7",
|
|
99
|
+
"multer": "1.4.4",
|
|
100
|
+
"node-mailjet": "3.3.5",
|
|
101
|
+
"nodemailer": "6.7.2",
|
|
102
|
+
"nodemon": "2.0.15",
|
|
103
|
+
"passport": "0.5.2",
|
|
110
104
|
"passport-jwt": "4.0.0",
|
|
111
105
|
"reflect-metadata": "0.1.13",
|
|
112
106
|
"rimraf": "3.0.2",
|
|
113
|
-
"rxjs": "7.
|
|
114
|
-
"supertest": "6.
|
|
115
|
-
"ts-morph": "
|
|
107
|
+
"rxjs": "7.5.2",
|
|
108
|
+
"supertest": "6.2.2",
|
|
109
|
+
"ts-morph": "13.0.3",
|
|
116
110
|
"ts-node": "10.4.0",
|
|
117
|
-
"tsconfig-paths": "3.
|
|
111
|
+
"tsconfig-paths": "3.12.0"
|
|
118
112
|
},
|
|
119
113
|
"devDependencies": {
|
|
120
|
-
"eslint": "8.
|
|
114
|
+
"eslint": "8.7.0",
|
|
121
115
|
"eslint-config-prettier": "8.3.0",
|
|
122
116
|
"find-file-up": "2.0.1",
|
|
123
117
|
"pm2": "5.1.2",
|
|
124
|
-
"prettier": "2.
|
|
125
|
-
"pretty-quick": "3.1.
|
|
126
|
-
"ts-jest": "27.
|
|
127
|
-
"typescript": "4.
|
|
118
|
+
"prettier": "2.5.1",
|
|
119
|
+
"pretty-quick": "3.1.3",
|
|
120
|
+
"ts-jest": "27.1.3",
|
|
121
|
+
"typescript": "4.5.5"
|
|
128
122
|
},
|
|
129
123
|
"jest": {
|
|
130
124
|
"collectCoverage": true,
|
package/src/config.env.ts
CHANGED
|
@@ -19,10 +19,16 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
19
19
|
port: 587,
|
|
20
20
|
secure: false,
|
|
21
21
|
},
|
|
22
|
+
mailjet: {
|
|
23
|
+
api_key_public: 'MAILJET_API_KEY_PUBLIC',
|
|
24
|
+
api_key_private: 'MAILJET_API_KEY_PRIVATE',
|
|
25
|
+
},
|
|
22
26
|
defaultSender: {
|
|
23
27
|
email: 'rebeca68@ethereal.email',
|
|
24
28
|
name: 'Rebeca Sixtyeight',
|
|
25
29
|
},
|
|
30
|
+
verificationLink: 'http://localhost:4200/user/verification',
|
|
31
|
+
passwordResetLink: 'http://localhost:4200/user/password-reset',
|
|
26
32
|
},
|
|
27
33
|
env: 'development',
|
|
28
34
|
graphQl: {
|
|
@@ -60,10 +66,16 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
60
66
|
port: 587,
|
|
61
67
|
secure: false,
|
|
62
68
|
},
|
|
69
|
+
mailjet: {
|
|
70
|
+
api_key_public: 'MAILJET_API_KEY_PUBLIC',
|
|
71
|
+
api_key_private: 'MAILJET_API_KEY_PRIVATE',
|
|
72
|
+
},
|
|
63
73
|
defaultSender: {
|
|
64
74
|
email: 'rebeca68@ethereal.email',
|
|
65
75
|
name: 'Rebeca Sixtyeight',
|
|
66
76
|
},
|
|
77
|
+
verificationLink: 'http://localhost:4200/user/verification',
|
|
78
|
+
passwordResetLink: 'http://localhost:4200/user/password-reset',
|
|
67
79
|
},
|
|
68
80
|
env: 'productive',
|
|
69
81
|
graphQl: {
|
|
@@ -93,7 +105,8 @@ const config: { [env: string]: IServerOptions } = {
|
|
|
93
105
|
*
|
|
94
106
|
* default: development
|
|
95
107
|
*/
|
|
96
|
-
const envConfig = config[process.env
|
|
108
|
+
const envConfig = config[process.env['NODE' + '_ENV'] || 'development'] || config.development;
|
|
109
|
+
console.log('Server starts in mode: ', process.env['NODE' + '_ENV'] || 'development');
|
|
97
110
|
|
|
98
111
|
/**
|
|
99
112
|
* Export envConfig as default
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { UnauthorizedException } from '@nestjs/common';
|
|
1
2
|
import * as bcrypt from 'bcrypt';
|
|
3
|
+
import * as _ from 'lodash';
|
|
4
|
+
import { RoleEnum } from '../enums/role.enum';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* Helper class for services
|
|
@@ -9,13 +12,15 @@ export class ServiceHelper {
|
|
|
9
12
|
*/
|
|
10
13
|
static async prepareInput(
|
|
11
14
|
input: { [key: string]: any },
|
|
12
|
-
currentUser: { id: string },
|
|
15
|
+
currentUser: { [key: string]: any; id: string },
|
|
13
16
|
options: { [key: string]: any; create?: boolean; clone?: boolean } = {},
|
|
14
17
|
...args: any[]
|
|
15
18
|
) {
|
|
16
19
|
// Configuration
|
|
17
20
|
const config = {
|
|
21
|
+
checkRoles: true,
|
|
18
22
|
clone: false,
|
|
23
|
+
create: false,
|
|
19
24
|
...options,
|
|
20
25
|
};
|
|
21
26
|
|
|
@@ -24,7 +29,21 @@ export class ServiceHelper {
|
|
|
24
29
|
input = JSON.parse(JSON.stringify(input));
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
//
|
|
32
|
+
// Process roles
|
|
33
|
+
if (input.roles && config.checkRoles && (!currentUser?.hasRole || !currentUser.hasRole(RoleEnum.ADMIN))) {
|
|
34
|
+
if (!(currentUser as any)?.roles) {
|
|
35
|
+
throw new UnauthorizedException('Missing roles of current user');
|
|
36
|
+
} else {
|
|
37
|
+
const allowedRoles = _.intersection(input.roles, (currentUser as any).roles);
|
|
38
|
+
if (allowedRoles.length !== input.roles.length) {
|
|
39
|
+
const missingRoles = _.difference(input.roles, (currentUser as any).roles);
|
|
40
|
+
throw new UnauthorizedException('Current user not allowed setting roles: ' + missingRoles);
|
|
41
|
+
}
|
|
42
|
+
input.roles = allowedRoles;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Hash password
|
|
28
47
|
if (input.password) {
|
|
29
48
|
input.password = await bcrypt.hash((input as any).password, 10);
|
|
30
49
|
}
|
|
@@ -46,11 +65,11 @@ export class ServiceHelper {
|
|
|
46
65
|
/**
|
|
47
66
|
* Prepare output before return
|
|
48
67
|
*/
|
|
49
|
-
static async prepareOutput(
|
|
68
|
+
static async prepareOutput<T = Record<string, any>>(
|
|
50
69
|
output: any,
|
|
51
70
|
userModel: new () => any,
|
|
52
71
|
userService: any,
|
|
53
|
-
options: { [key: string]: any; clone?: boolean } = {},
|
|
72
|
+
options: { [key: string]: any; clone?: boolean; targetModel?: Partial<T> } = {},
|
|
54
73
|
...args: any[]
|
|
55
74
|
) {
|
|
56
75
|
// Configuration
|
|
@@ -64,9 +83,20 @@ export class ServiceHelper {
|
|
|
64
83
|
output = JSON.parse(JSON.stringify(output));
|
|
65
84
|
}
|
|
66
85
|
|
|
86
|
+
// Map output if target model exist
|
|
87
|
+
if (options.targetModel) {
|
|
88
|
+
(options.targetModel as any).map(output);
|
|
89
|
+
}
|
|
90
|
+
|
|
67
91
|
// Remove password if exists
|
|
68
92
|
delete output.password;
|
|
69
93
|
|
|
94
|
+
// Remove verification token if exists
|
|
95
|
+
delete output.verificationToken;
|
|
96
|
+
|
|
97
|
+
// Remove password reset token if exists
|
|
98
|
+
delete output.passwordResetToken;
|
|
99
|
+
|
|
70
100
|
// Return prepared user
|
|
71
101
|
return output;
|
|
72
102
|
}
|
|
@@ -3,6 +3,7 @@ import { JwtModuleOptions } from '@nestjs/jwt';
|
|
|
3
3
|
import { ServeStaticOptions } from '@nestjs/platform-express/interfaces/serve-static-options.interface';
|
|
4
4
|
import * as SMTPTransport from 'nodemailer/lib/smtp-transport';
|
|
5
5
|
import { MongooseModuleOptions } from '@nestjs/mongoose/dist/interfaces/mongoose-options.interface';
|
|
6
|
+
import { MailjetOptions } from './mailjet-options.interface';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Options for the server
|
|
@@ -101,6 +102,17 @@ export interface IServerOptions {
|
|
|
101
102
|
*/
|
|
102
103
|
smtp?: SMTPTransport | SMTPTransport.Options | string;
|
|
103
104
|
|
|
105
|
+
mailjet?: MailjetOptions;
|
|
106
|
+
/**
|
|
107
|
+
* Verification link for email
|
|
108
|
+
*/
|
|
109
|
+
verificationLink?: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Password reset link for email
|
|
113
|
+
*/
|
|
114
|
+
passwordResetLink?: string;
|
|
115
|
+
|
|
104
116
|
/**
|
|
105
117
|
* Data for default sender
|
|
106
118
|
*/
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { ConfigService } from './config.service';
|
|
3
|
+
import * as mailjet from 'node-mailjet';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mailjet service
|
|
7
|
+
*/
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class MailjetService {
|
|
10
|
+
/**
|
|
11
|
+
* Inject services
|
|
12
|
+
*/
|
|
13
|
+
constructor(protected configService: ConfigService) {}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Send a mail
|
|
17
|
+
*/
|
|
18
|
+
public async sendMail(
|
|
19
|
+
recipients: string | string[],
|
|
20
|
+
subject: string,
|
|
21
|
+
templateId: number,
|
|
22
|
+
config: {
|
|
23
|
+
senderEmail?: string;
|
|
24
|
+
senderName?: string;
|
|
25
|
+
attachments?: mailjet.Email.Attachment[];
|
|
26
|
+
templateData?: { [key: string]: any };
|
|
27
|
+
sandbox?: boolean;
|
|
28
|
+
}
|
|
29
|
+
): Promise<mailjet.Email.PostResponse> {
|
|
30
|
+
// Process config
|
|
31
|
+
const { senderName, senderEmail, templateData, attachments, sandbox } = {
|
|
32
|
+
senderEmail: this.configService.get('email.defaultSender.email'),
|
|
33
|
+
senderName: this.configService.get('email.defaultSender.name'),
|
|
34
|
+
sandbox: false,
|
|
35
|
+
attachments: null,
|
|
36
|
+
templateData: null,
|
|
37
|
+
...config,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Parse recipients
|
|
41
|
+
let to;
|
|
42
|
+
if (Array.isArray(recipients)) {
|
|
43
|
+
to = [];
|
|
44
|
+
for (const recipient of recipients) {
|
|
45
|
+
to.push({ Email: recipient });
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
to = [{ Email: recipients }];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse body for mailjet request
|
|
52
|
+
const body: mailjet.Email.SendParams = {
|
|
53
|
+
Messages: [
|
|
54
|
+
{
|
|
55
|
+
From: {
|
|
56
|
+
Email: senderEmail,
|
|
57
|
+
Name: senderName,
|
|
58
|
+
},
|
|
59
|
+
To: to,
|
|
60
|
+
TemplateID: templateId,
|
|
61
|
+
TemplateLanguage: true,
|
|
62
|
+
Variables: templateData,
|
|
63
|
+
Subject: subject,
|
|
64
|
+
Attachments: attachments,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
SandboxMode: sandbox,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let connection: mailjet.Email.Client;
|
|
71
|
+
try {
|
|
72
|
+
// Connect to mailjet
|
|
73
|
+
connection = await mailjet.connect(
|
|
74
|
+
this.configService.get('email.mailjet.api_key_public'),
|
|
75
|
+
this.configService.get('email.mailjet.api_key_private')
|
|
76
|
+
);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
throw new Error('Cannot connect to mailjet.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Send mail with mailjet
|
|
82
|
+
return connection.post('send', { version: 'v3.1' }).request(body);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -16,9 +16,9 @@ const pubSub = new PubSub();
|
|
|
16
16
|
* User service
|
|
17
17
|
*/
|
|
18
18
|
export abstract class CoreBasicUserService<
|
|
19
|
-
TUser
|
|
20
|
-
TUserInput
|
|
21
|
-
TUserCreateInput
|
|
19
|
+
TUser extends CoreUserModel,
|
|
20
|
+
TUserInput extends CoreUserInput,
|
|
21
|
+
TUserCreateInput extends CoreUserCreateInput
|
|
22
22
|
> {
|
|
23
23
|
protected readonly model: ICorePersistenceModel;
|
|
24
24
|
|
|
@@ -59,6 +59,26 @@ export abstract class CoreUserModel extends CorePersistenceModel {
|
|
|
59
59
|
@Prop()
|
|
60
60
|
username: string = undefined;
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Password reset token of the user
|
|
64
|
+
*/
|
|
65
|
+
@IsOptional()
|
|
66
|
+
@Prop()
|
|
67
|
+
passwordResetToken: string = undefined;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Verification token of the user
|
|
71
|
+
*/
|
|
72
|
+
@IsOptional()
|
|
73
|
+
@Prop()
|
|
74
|
+
verificationToken: string = undefined;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Verification of the user
|
|
78
|
+
*/
|
|
79
|
+
@Prop({ type: Boolean })
|
|
80
|
+
verified = false;
|
|
81
|
+
|
|
62
82
|
// ===================================================================================================================
|
|
63
83
|
// Methods
|
|
64
84
|
// ===================================================================================================================
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
NotFoundException,
|
|
4
|
+
UnauthorizedException,
|
|
5
|
+
UnprocessableEntityException,
|
|
6
|
+
} from '@nestjs/common';
|
|
2
7
|
import * as bcrypt from 'bcrypt';
|
|
3
8
|
import { PubSub } from 'graphql-subscriptions';
|
|
4
9
|
import { FilterArgs } from '../../common/args/filter.args';
|
|
10
|
+
import { RoleEnum } from '../../common/enums/role.enum';
|
|
5
11
|
import { Filter } from '../../common/helpers/filter.helper';
|
|
6
12
|
import { CoreBasicUserService } from './core-basic-user.service';
|
|
7
13
|
import { CoreUserModel } from './core-user.model';
|
|
8
14
|
import { CoreUserCreateInput } from './inputs/core-user-create.input';
|
|
9
15
|
import { CoreUserInput } from './inputs/core-user.input';
|
|
10
16
|
import { Model } from 'mongoose';
|
|
17
|
+
import * as _ from 'lodash';
|
|
18
|
+
import * as crypto from 'crypto';
|
|
19
|
+
import envConfig from '../../../config.env';
|
|
20
|
+
import { EmailService } from '../../common/services/email.service';
|
|
11
21
|
|
|
12
22
|
// Subscription
|
|
13
23
|
const pubSub = new PubSub();
|
|
@@ -16,11 +26,11 @@ const pubSub = new PubSub();
|
|
|
16
26
|
* User service
|
|
17
27
|
*/
|
|
18
28
|
export abstract class CoreUserService<
|
|
19
|
-
TUser
|
|
20
|
-
TUserInput
|
|
21
|
-
TUserCreateInput
|
|
29
|
+
TUser extends CoreUserModel,
|
|
30
|
+
TUserInput extends CoreUserInput,
|
|
31
|
+
TUserCreateInput extends CoreUserCreateInput
|
|
22
32
|
> extends CoreBasicUserService<TUser, TUserInput, TUserCreateInput> {
|
|
23
|
-
protected constructor(protected readonly userModel: Model<any
|
|
33
|
+
protected constructor(protected readonly userModel: Model<any>, protected emailService: EmailService) {
|
|
24
34
|
super(userModel);
|
|
25
35
|
}
|
|
26
36
|
|
|
@@ -35,8 +45,11 @@ export abstract class CoreUserService<
|
|
|
35
45
|
// Prepare input
|
|
36
46
|
await this.prepareInput(input, currentUser, { create: true });
|
|
37
47
|
|
|
48
|
+
// Generate verification token
|
|
49
|
+
const newUser = { ...input, ...{ verificationToken: crypto.randomBytes(32).toString('hex') } };
|
|
50
|
+
|
|
38
51
|
// Create new user
|
|
39
|
-
const createdUser = new this.userModel(this.model.map(
|
|
52
|
+
const createdUser = new this.userModel(this.model.map(newUser));
|
|
40
53
|
|
|
41
54
|
try {
|
|
42
55
|
// Save created user
|
|
@@ -108,6 +121,93 @@ export abstract class CoreUserService<
|
|
|
108
121
|
);
|
|
109
122
|
}
|
|
110
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Verify user with token
|
|
126
|
+
*
|
|
127
|
+
* @param token
|
|
128
|
+
*/
|
|
129
|
+
async verify(token: string): Promise<boolean> {
|
|
130
|
+
const user = await this.userModel.findOne({ verificationToken: token }).exec();
|
|
131
|
+
|
|
132
|
+
if (!user) {
|
|
133
|
+
throw new NotFoundException();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!user.verificationToken) {
|
|
137
|
+
throw new Error('User has no token');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (user.verified) {
|
|
141
|
+
throw new Error('User already verified');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await this.userModel.findByIdAndUpdate(user.id, { $set: { verified: true, verificationToken: null } }).exec();
|
|
145
|
+
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Set newpassword for user with token
|
|
151
|
+
*
|
|
152
|
+
* @param token
|
|
153
|
+
* @param newPassword
|
|
154
|
+
*/
|
|
155
|
+
async resetPassword(token: string, newPassword: string): Promise<boolean> {
|
|
156
|
+
const user = await this.userModel.findOne({ passwordResetToken: token }).exec();
|
|
157
|
+
|
|
158
|
+
if (!user) {
|
|
159
|
+
throw new NotFoundException();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const cryptedPassword = await bcrypt.hash(newPassword, 10);
|
|
163
|
+
await this.userModel
|
|
164
|
+
.findByIdAndUpdate(user.id, { $set: { password: cryptedPassword, passwordResetToken: null } })
|
|
165
|
+
.exec();
|
|
166
|
+
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Request email with password reset link
|
|
172
|
+
*
|
|
173
|
+
* @param email
|
|
174
|
+
*/
|
|
175
|
+
async requestPasswordResetMail(email: string): Promise<boolean> {
|
|
176
|
+
const user = await this.userModel.findOne({ email }).exec();
|
|
177
|
+
|
|
178
|
+
if (!user) {
|
|
179
|
+
throw new NotFoundException();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const resetToken = crypto.randomBytes(32).toString('hex');
|
|
183
|
+
await this.userModel.findByIdAndUpdate(user.id, { $set: { passwordResetToken: resetToken } }).exec();
|
|
184
|
+
|
|
185
|
+
await this.emailService.sendMail(user.email, 'Password reset', {
|
|
186
|
+
htmlTemplate: 'password-reset',
|
|
187
|
+
templateData: { name: user.username, link: envConfig.email.passwordResetLink + '/' + resetToken },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Set roles for specified user
|
|
195
|
+
*/
|
|
196
|
+
async setRoles(userId: string, roles: string[]): Promise<TUser> {
|
|
197
|
+
// Check roles
|
|
198
|
+
if (!Array.isArray(roles)) {
|
|
199
|
+
throw new BadRequestException('Missing roles');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check roles values
|
|
203
|
+
if (roles.some((role) => typeof role !== 'string')) {
|
|
204
|
+
throw new BadRequestException('roles contains invalid values');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update and return user
|
|
208
|
+
return this.userModel.findByIdAndUpdate(userId, { roles }).exec();
|
|
209
|
+
}
|
|
210
|
+
|
|
111
211
|
/**
|
|
112
212
|
* Update user via ID
|
|
113
213
|
*/
|
|
@@ -144,22 +244,37 @@ export abstract class CoreUserService<
|
|
|
144
244
|
*/
|
|
145
245
|
protected async prepareInput(
|
|
146
246
|
input: { [key: string]: any },
|
|
147
|
-
currentUser
|
|
148
|
-
options: { [key: string]: any;
|
|
247
|
+
currentUser?: TUser,
|
|
248
|
+
options: { [key: string]: any; checkRoles?: boolean; clone?: boolean } = {},
|
|
149
249
|
...args: any[]
|
|
150
250
|
) {
|
|
151
251
|
// Configuration
|
|
152
252
|
const config = {
|
|
253
|
+
checkRoles: true,
|
|
153
254
|
clone: false,
|
|
154
255
|
...options,
|
|
155
256
|
};
|
|
156
257
|
|
|
157
|
-
// Clone
|
|
258
|
+
// Clone input
|
|
158
259
|
if (config.clone) {
|
|
159
260
|
input = JSON.parse(JSON.stringify(input));
|
|
160
261
|
}
|
|
161
262
|
|
|
162
|
-
//
|
|
263
|
+
// Process roles
|
|
264
|
+
if (input.roles && config.checkRoles && (!currentUser?.hasRole || !currentUser.hasRole(RoleEnum.ADMIN))) {
|
|
265
|
+
if (!(currentUser as any)?.roles) {
|
|
266
|
+
throw new UnauthorizedException('Missing roles of current user');
|
|
267
|
+
} else {
|
|
268
|
+
const allowedRoles = _.intersection(input.roles, (currentUser as any).roles);
|
|
269
|
+
if (allowedRoles.length !== input.roles.length) {
|
|
270
|
+
const missingRoles = _.difference(input.roles, (currentUser as any).roles);
|
|
271
|
+
throw new UnauthorizedException('Current user not allowed setting roles: ' + missingRoles);
|
|
272
|
+
}
|
|
273
|
+
input.roles = allowedRoles;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Hash password
|
|
163
278
|
if (input.password) {
|
|
164
279
|
input.password = await bcrypt.hash((input as any).password, 10);
|
|
165
280
|
}
|
|
@@ -190,6 +305,12 @@ export abstract class CoreUserService<
|
|
|
190
305
|
// Remove password if exists
|
|
191
306
|
delete (user as any).password;
|
|
192
307
|
|
|
308
|
+
// Remove verification token if exists
|
|
309
|
+
delete (user as any).verificationToken;
|
|
310
|
+
|
|
311
|
+
// Remove password reset token if exists
|
|
312
|
+
delete (user as any).passwordResetToken;
|
|
313
|
+
|
|
193
314
|
// Return prepared user
|
|
194
315
|
return user;
|
|
195
316
|
}
|
package/src/core.module.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { ConfigService } from './core/common/services/config.service';
|
|
|
9
9
|
import { EmailService } from './core/common/services/email.service';
|
|
10
10
|
import { TemplateService } from './core/common/services/template.service';
|
|
11
11
|
import { MongooseModule } from '@nestjs/mongoose';
|
|
12
|
+
import { MailjetService } from './core/common/services/mailjet.service';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Core module (dynamic)
|
|
@@ -91,6 +92,7 @@ export class CoreModule {
|
|
|
91
92
|
// Core Services
|
|
92
93
|
EmailService,
|
|
93
94
|
TemplateService,
|
|
95
|
+
MailjetService,
|
|
94
96
|
];
|
|
95
97
|
|
|
96
98
|
// Return dynamic module
|
|
@@ -101,7 +103,7 @@ export class CoreModule {
|
|
|
101
103
|
GraphQLModule.forRoot(config.graphQl),
|
|
102
104
|
],
|
|
103
105
|
providers,
|
|
104
|
-
exports: [ConfigService, EmailService, TemplateService],
|
|
106
|
+
exports: [ConfigService, EmailService, TemplateService, MailjetService],
|
|
105
107
|
};
|
|
106
108
|
}
|
|
107
109
|
}
|
package/src/index.ts
CHANGED
|
@@ -41,6 +41,7 @@ export * from './core/common/scalars/json.scalar';
|
|
|
41
41
|
export * from './core/common/services/config.service';
|
|
42
42
|
export * from './core/common/services/email.service';
|
|
43
43
|
export * from './core/common/services/template.service';
|
|
44
|
+
export * from './core/common/services/mailjet.service';
|
|
44
45
|
|
|
45
46
|
// =====================================================================================================================
|
|
46
47
|
// Core - Modules - Auth
|