@lenne.tech/nest-server 3.0.0 → 3.1.3

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 (46) hide show
  1. package/dist/config.env.js +14 -1
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/helpers/service.helper.d.ts +2 -1
  4. package/dist/core/common/helpers/service.helper.js +5 -0
  5. package/dist/core/common/helpers/service.helper.js.map +1 -1
  6. package/dist/core/common/interfaces/mailjet-options.interface.d.ts +4 -0
  7. package/dist/core/common/interfaces/mailjet-options.interface.js +3 -0
  8. package/dist/core/common/interfaces/mailjet-options.interface.js.map +1 -0
  9. package/dist/core/common/interfaces/server-options.interface.d.ts +4 -0
  10. package/dist/core/common/services/mailjet.service.d.ts +15 -0
  11. package/dist/core/common/services/mailjet.service.js +64 -0
  12. package/dist/core/common/services/mailjet.service.js.map +1 -0
  13. package/dist/core/modules/user/core-user.model.d.ts +3 -0
  14. package/dist/core/modules/user/core-user.model.js +18 -0
  15. package/dist/core/modules/user/core-user.model.js.map +1 -1
  16. package/dist/core/modules/user/core-user.service.d.ts +6 -1
  17. package/dist/core/modules/user/core-user.service.js +46 -2
  18. package/dist/core/modules/user/core-user.service.js.map +1 -1
  19. package/dist/core.module.js +3 -1
  20. package/dist/core.module.js.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/server/modules/user/user.resolver.d.ts +3 -0
  25. package/dist/server/modules/user/user.resolver.js +31 -0
  26. package/dist/server/modules/user/user.resolver.js.map +1 -1
  27. package/dist/server/modules/user/user.service.js +3 -4
  28. package/dist/server/modules/user/user.service.js.map +1 -1
  29. package/dist/test/test.helper.js +1 -1
  30. package/dist/test/test.helper.js.map +1 -1
  31. package/dist/tsconfig.build.tsbuildinfo +1 -1
  32. package/package.json +5 -1
  33. package/src/config.env.ts +14 -1
  34. package/src/core/common/helpers/service.helper.ts +13 -2
  35. package/src/core/common/interfaces/mailjet-options.interface.ts +4 -0
  36. package/src/core/common/interfaces/server-options.interface.ts +12 -0
  37. package/src/core/common/services/mailjet.service.ts +84 -0
  38. package/src/core/modules/user/core-user.model.ts +21 -0
  39. package/src/core/modules/user/core-user.service.ts +83 -2
  40. package/src/core.module.ts +3 -1
  41. package/src/index.ts +1 -0
  42. package/src/server/modules/user/user.resolver.ts +24 -0
  43. package/src/server/modules/user/user.service.ts +6 -4
  44. package/src/templates/password-reset.ejs +3 -0
  45. package/src/templates/welcome.ejs +3 -2
  46. 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.0.0",
3
+ "version": "3.1.3",
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",
@@ -62,11 +62,13 @@
62
62
  "@nestjs/passport": "8.1.0",
63
63
  "@nestjs/platform-express": "8.2.6",
64
64
  "@nestjs/testing": "8.2.6",
65
+ "@shelf/jest-mongodb": "2.2.0",
65
66
  "@types/ejs": "3.1.0",
66
67
  "@types/jest": "27.4.0",
67
68
  "@types/lodash": "4.14.178",
68
69
  "@types/multer": "1.4.7",
69
70
  "@types/node": "16.11.21",
71
+ "@types/node-mailjet": "3.3.8",
70
72
  "@types/nodemailer": "6.4.4",
71
73
  "@types/passport": "1.0.7",
72
74
  "@types/supertest": "2.0.11",
@@ -92,8 +94,10 @@
92
94
  "json-to-graphql-query": "2.2.0",
93
95
  "light-my-request": "4.7.0",
94
96
  "lodash": "4.17.21",
97
+ "mongodb": "4.3.0",
95
98
  "mongoose": "6.1.7",
96
99
  "multer": "1.4.4",
100
+ "node-mailjet": "3.3.5",
97
101
  "nodemailer": "6.7.2",
98
102
  "nodemon": "2.0.15",
99
103
  "passport": "0.5.2",
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.NODE_ENV || 'development'] || config.development;
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
@@ -65,11 +65,11 @@ export class ServiceHelper {
65
65
  /**
66
66
  * Prepare output before return
67
67
  */
68
- static async prepareOutput(
68
+ static async prepareOutput<T = Record<string, any>>(
69
69
  output: any,
70
70
  userModel: new () => any,
71
71
  userService: any,
72
- options: { [key: string]: any; clone?: boolean } = {},
72
+ options: { [key: string]: any; clone?: boolean; targetModel?: Partial<T> } = {},
73
73
  ...args: any[]
74
74
  ) {
75
75
  // Configuration
@@ -83,9 +83,20 @@ export class ServiceHelper {
83
83
  output = JSON.parse(JSON.stringify(output));
84
84
  }
85
85
 
86
+ // Map output if target model exist
87
+ if (options.targetModel) {
88
+ (options.targetModel as any).map(output);
89
+ }
90
+
86
91
  // Remove password if exists
87
92
  delete output.password;
88
93
 
94
+ // Remove verification token if exists
95
+ delete output.verificationToken;
96
+
97
+ // Remove password reset token if exists
98
+ delete output.passwordResetToken;
99
+
89
100
  // Return prepared user
90
101
  return output;
91
102
  }
@@ -0,0 +1,4 @@
1
+ export interface MailjetOptions {
2
+ api_key_public: string;
3
+ api_key_private: string;
4
+ }
@@ -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
+ }
@@ -59,6 +59,27 @@ 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
+ @Field((type) => Boolean, { description: 'Verification state of the user', nullable: true })
80
+ @Prop({ type: Boolean })
81
+ verified = false;
82
+
62
83
  // ===================================================================================================================
63
84
  // Methods
64
85
  // ===================================================================================================================
@@ -15,6 +15,9 @@ import { CoreUserCreateInput } from './inputs/core-user-create.input';
15
15
  import { CoreUserInput } from './inputs/core-user.input';
16
16
  import { Model } from 'mongoose';
17
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';
18
21
 
19
22
  // Subscription
20
23
  const pubSub = new PubSub();
@@ -27,7 +30,7 @@ export abstract class CoreUserService<
27
30
  TUserInput extends CoreUserInput,
28
31
  TUserCreateInput extends CoreUserCreateInput
29
32
  > extends CoreBasicUserService<TUser, TUserInput, TUserCreateInput> {
30
- protected constructor(protected readonly userModel: Model<any>) {
33
+ protected constructor(protected readonly userModel: Model<any>, protected emailService: EmailService) {
31
34
  super(userModel);
32
35
  }
33
36
 
@@ -42,8 +45,11 @@ export abstract class CoreUserService<
42
45
  // Prepare input
43
46
  await this.prepareInput(input, currentUser, { create: true });
44
47
 
48
+ // Generate verification token
49
+ const newUser = { ...input, ...{ verificationToken: crypto.randomBytes(32).toString('hex') } };
50
+
45
51
  // Create new user
46
- const createdUser = new this.userModel(this.model.map(input));
52
+ const createdUser = new this.userModel(this.model.map(newUser));
47
53
 
48
54
  try {
49
55
  // Save created user
@@ -115,6 +121,75 @@ export abstract class CoreUserService<
115
121
  );
116
122
  }
117
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
+
118
193
  /**
119
194
  * Set roles for specified user
120
195
  */
@@ -230,6 +305,12 @@ export abstract class CoreUserService<
230
305
  // Remove password if exists
231
306
  delete (user as any).password;
232
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
+
233
314
  // Return prepared user
234
315
  return user;
235
316
  }
@@ -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
@@ -43,9 +43,33 @@ export class UserResolver {
43
43
  return await this.usersService.find(args, info);
44
44
  }
45
45
 
46
+ /**
47
+ * Request new password for user with email
48
+ */
49
+ @Query((returns) => Boolean, { description: 'Request new password for user with email' })
50
+ async requestPasswordResetMail(@Args('email') email: string) {
51
+ return await this.usersService.requestPasswordResetMail(email);
52
+ }
53
+
46
54
  // ===========================================================================
47
55
  // Mutations
48
56
  // ===========================================================================
57
+ /**
58
+ * Verify user with email
59
+ */
60
+ @Mutation((returns) => Boolean, { description: 'Verify user with email' })
61
+ async verifyUser(@Args('token') token: string) {
62
+ return await this.usersService.verify(token);
63
+ }
64
+
65
+ /**
66
+ * Set new password for user with token
67
+ */
68
+ @Mutation((returns) => Boolean, { description: 'Set new password for user with token' })
69
+ async resetPassword(@Args('token') token: string, @Args('password') password: string) {
70
+ return await this.usersService.resetPassword(token, password);
71
+ }
72
+
49
73
  /**
50
74
  * Create new user
51
75
  */
@@ -42,7 +42,7 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
42
42
  @InjectModel('User') protected readonly userModel: Model<User>,
43
43
  @Inject('PUB_SUB') protected readonly pubSub: PubSub
44
44
  ) {
45
- super(userModel);
45
+ super(userModel, emailService);
46
46
  this.model = User;
47
47
  }
48
48
 
@@ -55,12 +55,14 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
55
55
  */
56
56
  async create(input: UserCreateInput, currentUser?: User, ...args: any[]): Promise<User> {
57
57
  const user = await super.create(input, currentUser);
58
- const text = `Welcome ${user.firstName}, this is plain text from server.`;
58
+
59
+ await this.prepareOutput(user, args[0]);
60
+
59
61
  await this.pubSub.publish('userCreated', User.map(user));
62
+
60
63
  await this.emailService.sendMail(user.email, 'Welcome', {
61
64
  htmlTemplate: 'welcome',
62
- templateData: user,
63
- text,
65
+ templateData: { name: user.username, link: envConfig.email.verificationLink + '/' + user.verificationToken },
64
66
  });
65
67
 
66
68
  return user;
@@ -0,0 +1,3 @@
1
+ <h1>Hello <%= name %>,</h1>
2
+ <p>you requested a link for setting a new password. Here it is.</p><br />
3
+ <a href="<%= link %>">Passwort zurücksetzten</a>
@@ -1,2 +1,3 @@
1
- <h1>Welcome <%= firstName %></h1>
2
- <p>This is a EJS template from server.</p>
1
+ <h1>Welcome <%= name %>,</h1>
2
+ <p>please confirm your email address.</p>
3
+ <a href="<%= link %>">E-Mail bestätigen</a>
@@ -113,7 +113,7 @@ export class TestHelper {
113
113
  /**
114
114
  * GraphQL request
115
115
  * @param graphql
116
- * @param statusCode
116
+ * @param options
117
117
  */
118
118
  async graphQl(graphql: string | TestGraphQLConfig, options: TestGraphQLOptions = {}): Promise<any> {
119
119
  // Default options
@@ -148,7 +148,7 @@ export class TestHelper {
148
148
  graphql = Object.assign(
149
149
  {
150
150
  arguments: null,
151
- fields: ['id'],
151
+ fields: [],
152
152
  name: null,
153
153
  type: TestGraphQLType.QUERY,
154
154
  },