@lenne.tech/nest-server 3.1.0 → 3.2.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.
Files changed (41) hide show
  1. package/dist/core/common/decorators/restricted.decorator.js +2 -0
  2. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  3. package/dist/core/common/helpers/model.helper.js +1 -0
  4. package/dist/core/common/helpers/model.helper.js.map +1 -1
  5. package/dist/core/common/helpers/service.helper.d.ts +2 -1
  6. package/dist/core/common/helpers/service.helper.js +4 -1
  7. package/dist/core/common/helpers/service.helper.js.map +1 -1
  8. package/dist/core/common/pipes/map.pipe.d.ts +4 -0
  9. package/dist/core/common/pipes/map.pipe.js +25 -0
  10. package/dist/core/common/pipes/map.pipe.js.map +1 -0
  11. package/dist/core/modules/user/core-user.model.js +1 -0
  12. package/dist/core/modules/user/core-user.model.js.map +1 -1
  13. package/dist/core/modules/user/core-user.service.d.ts +6 -1
  14. package/dist/core/modules/user/core-user.service.js +45 -3
  15. package/dist/core/modules/user/core-user.service.js.map +1 -1
  16. package/dist/core/modules/user/inputs/core-user-create.input.js +4 -0
  17. package/dist/core/modules/user/inputs/core-user-create.input.js.map +1 -1
  18. package/dist/core/modules/user/inputs/core-user.input.d.ts +2 -1
  19. package/dist/core/modules/user/inputs/core-user.input.js +12 -2
  20. package/dist/core/modules/user/inputs/core-user.input.js.map +1 -1
  21. package/dist/main.js +2 -0
  22. package/dist/main.js.map +1 -1
  23. package/dist/server/modules/user/user.resolver.d.ts +1 -1
  24. package/dist/server/modules/user/user.resolver.js +11 -11
  25. package/dist/server/modules/user/user.resolver.js.map +1 -1
  26. package/dist/server/modules/user/user.service.d.ts +0 -3
  27. package/dist/server/modules/user/user.service.js +2 -44
  28. package/dist/server/modules/user/user.service.js.map +1 -1
  29. package/dist/tsconfig.build.tsbuildinfo +1 -1
  30. package/package.json +1 -1
  31. package/src/core/common/decorators/restricted.decorator.ts +2 -0
  32. package/src/core/common/helpers/model.helper.ts +3 -0
  33. package/src/core/common/helpers/service.helper.ts +8 -3
  34. package/src/core/common/pipes/map.pipe.ts +16 -0
  35. package/src/core/modules/user/core-user.model.ts +1 -0
  36. package/src/core/modules/user/core-user.service.ts +78 -3
  37. package/src/core/modules/user/inputs/core-user-create.input.ts +1 -1
  38. package/src/core/modules/user/inputs/core-user.input.ts +9 -8
  39. package/src/main.ts +4 -0
  40. package/src/server/modules/user/user.resolver.ts +13 -13
  41. package/src/server/modules/user/user.service.ts +3 -83
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
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",
@@ -73,10 +73,12 @@ export const checkRestricted = (
73
73
  ) {
74
74
  // User is not the owner
75
75
  delete data[propertyKey];
76
+ continue;
76
77
  }
77
78
  } else {
78
79
  // The user does not have the required rights
79
80
  delete data[propertyKey];
81
+ continue;
80
82
  }
81
83
  }
82
84
  }
@@ -77,6 +77,9 @@ export class ModelHelper {
77
77
  // Merge target with prepared source
78
78
  Object.assign(target, preparedSource);
79
79
 
80
+ // Remove all props with undefined
81
+ Object.keys(target).forEach((key) => target[key] === undefined && delete target[key]);
82
+
80
83
  // Return target
81
84
  return target;
82
85
  }
@@ -18,7 +18,7 @@ export class ServiceHelper {
18
18
  ) {
19
19
  // Configuration
20
20
  const config = {
21
- checkRoles: true,
21
+ checkRoles: false,
22
22
  clone: false,
23
23
  create: false,
24
24
  ...options,
@@ -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,6 +83,11 @@ 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
 
@@ -0,0 +1,16 @@
1
+ import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
2
+
3
+ @Injectable()
4
+ export class MapPipe implements PipeTransform {
5
+ transform(value: any, metadata: ArgumentMetadata) {
6
+ let result = value;
7
+
8
+ // Check map function is available
9
+ if ((metadata.metatype as any)?.map) {
10
+ // Map object to correct type
11
+ result = (metadata.metatype as any).map(value);
12
+ }
13
+
14
+ return result;
15
+ }
16
+ }
@@ -76,6 +76,7 @@ export abstract class CoreUserModel extends CorePersistenceModel {
76
76
  /**
77
77
  * Verification of the user
78
78
  */
79
+ @Field((type) => Boolean, { description: 'Verification state of the user', nullable: true })
79
80
  @Prop({ type: Boolean })
80
81
  verified = false;
81
82
 
@@ -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
  */
@@ -175,7 +250,7 @@ export abstract class CoreUserService<
175
250
  ) {
176
251
  // Configuration
177
252
  const config = {
178
- checkRoles: true,
253
+ checkRoles: false,
179
254
  clone: false,
180
255
  ...options,
181
256
  };
@@ -9,5 +9,5 @@ import { CoreUserInput } from './core-user.input';
9
9
  export abstract class CoreUserCreateInput extends CoreUserInput {
10
10
  @Field({ description: 'Email of the user', nullable: false })
11
11
  @IsEmail()
12
- email: string;
12
+ email: string = undefined;
13
13
  }
@@ -2,53 +2,54 @@ import { IsEmail, IsOptional } from 'class-validator';
2
2
  import { Field, InputType } from '@nestjs/graphql';
3
3
  import { Restricted } from '../../../common/decorators/restricted.decorator';
4
4
  import { RoleEnum } from '../../../common/enums/role.enum';
5
+ import { CoreModel } from '../../../common/models/core-model.model';
5
6
 
6
7
  /**
7
8
  * User input to update a user
8
9
  */
9
10
  @InputType({ description: 'User input', isAbstract: true })
10
- export abstract class CoreUserInput {
11
+ export abstract class CoreUserInput extends CoreModel {
11
12
  /**
12
13
  * Email of the user
13
14
  */
14
15
  @Field({ description: 'Email of the user', nullable: true })
15
16
  @IsOptional()
16
17
  @IsEmail()
17
- email?: string;
18
+ email?: string = undefined;
18
19
 
19
20
  /**
20
21
  * First name of the user
21
22
  */
22
23
  @Field({ description: 'First name of the user', nullable: true })
23
24
  @IsOptional()
24
- firstName?: string;
25
+ firstName?: string = undefined;
25
26
 
26
27
  /**
27
28
  * Last name of the user
28
29
  */
29
30
  @Field({ description: 'Last name of the user', nullable: true })
30
31
  @IsOptional()
31
- lastName?: string;
32
+ lastName?: string = undefined;
32
33
 
33
34
  /**
34
35
  * Roles of the user
35
36
  */
36
- @Restricted(RoleEnum.ADMIN, RoleEnum.OWNER)
37
+ @Restricted(RoleEnum.ADMIN)
37
38
  @Field((type) => [String], { description: 'Roles of the user', nullable: true })
38
39
  @IsOptional()
39
- roles?: string[];
40
+ roles?: string[] = [];
40
41
 
41
42
  /**
42
43
  * Username / alias of the user
43
44
  */
44
45
  @Field({ description: 'Username / alias of the user', nullable: true })
45
46
  @IsOptional()
46
- username?: string;
47
+ username?: string = undefined;
47
48
 
48
49
  /**
49
50
  * Password of the user
50
51
  */
51
52
  @Field({ description: 'Password of the user', nullable: true })
52
53
  @IsOptional()
53
- password?: string;
54
+ password?: string = undefined;
54
55
  }
package/src/main.ts CHANGED
@@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core';
2
2
  import { NestExpressApplication } from '@nestjs/platform-express';
3
3
  import envConfig from './config.env';
4
4
  import { ServerModule } from './server/server.module';
5
+ import { MapPipe } from './core/common/pipes/map.pipe';
5
6
 
6
7
  /**
7
8
  * Preparations for server start
@@ -13,6 +14,9 @@ async function bootstrap() {
13
14
  ServerModule
14
15
  );
15
16
 
17
+ // Add map pipe for mapping inputs to class
18
+ server.useGlobalPipes(new MapPipe());
19
+
16
20
  // Asset directory
17
21
  server.useStaticAssets(envConfig.staticAssets.path, envConfig.staticAssets.options);
18
22
 
@@ -43,14 +43,6 @@ export class UserResolver {
43
43
  return await this.usersService.find(args, info);
44
44
  }
45
45
 
46
- /**
47
- * Verify user with email
48
- */
49
- @Query((returns) => Boolean, { description: 'Verify user with email' })
50
- async verifyUser(@Args('token') token: string) {
51
- return await this.usersService.verify(token);
52
- }
53
-
54
46
  /**
55
47
  * Request new password for user with email
56
48
  */
@@ -59,17 +51,25 @@ export class UserResolver {
59
51
  return await this.usersService.requestPasswordResetMail(email);
60
52
  }
61
53
 
54
+ // ===========================================================================
55
+ // Mutations
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
+
62
65
  /**
63
66
  * Set new password for user with token
64
67
  */
65
- @Query((returns) => Boolean, { description: 'Set new password for user with token' })
68
+ @Mutation((returns) => Boolean, { description: 'Set new password for user with token' })
66
69
  async resetPassword(@Args('token') token: string, @Args('password') password: string) {
67
70
  return await this.usersService.resetPassword(token, password);
68
71
  }
69
72
 
70
- // ===========================================================================
71
- // Mutations
72
- // ===========================================================================
73
73
  /**
74
74
  * Create new user
75
75
  */
@@ -88,7 +88,7 @@ export class UserResolver {
88
88
  @Roles(RoleEnum.ADMIN, RoleEnum.OWNER)
89
89
  @Mutation((returns) => User, { description: 'Update existing user' })
90
90
  async updateUser(
91
- @Args('input') input: UserInput,
91
+ @Args('input', { type: () => UserInput }) input: UserInput,
92
92
  @Args('id') id: string,
93
93
  @GraphQLUser() user: User,
94
94
  @Info() info: GraphQLResolveInfo
@@ -1,10 +1,4 @@
1
- import {
2
- Inject,
3
- Injectable,
4
- NotFoundException,
5
- UnauthorizedException,
6
- UnprocessableEntityException,
7
- } from '@nestjs/common';
1
+ import { Inject, Injectable, UnauthorizedException, UnprocessableEntityException } from '@nestjs/common';
8
2
  import * as fs from 'fs';
9
3
  import { GraphQLResolveInfo } from 'graphql';
10
4
  import envConfig from '../../../config.env';
@@ -21,8 +15,6 @@ import { InjectModel } from '@nestjs/mongoose';
21
15
  import { Model } from 'mongoose';
22
16
  import { ICorePersistenceModel } from '../../../core/common/interfaces/core-persistence-model.interface';
23
17
  import { PubSub } from 'graphql-subscriptions';
24
- import * as crypto from 'crypto';
25
- import * as bcrypt from 'bcrypt';
26
18
 
27
19
  /**
28
20
  * User service
@@ -50,7 +42,7 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
50
42
  @InjectModel('User') protected readonly userModel: Model<User>,
51
43
  @Inject('PUB_SUB') protected readonly pubSub: PubSub
52
44
  ) {
53
- super(userModel);
45
+ super(userModel, emailService);
54
46
  this.model = User;
55
47
  }
56
48
 
@@ -68,86 +60,14 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
68
60
 
69
61
  await this.pubSub.publish('userCreated', User.map(user));
70
62
 
71
- const verificationToken = crypto.randomBytes(32).toString('hex');
72
- await this.userModel.findByIdAndUpdate(user.id, { $set: { verificationToken } }).exec();
73
-
74
63
  await this.emailService.sendMail(user.email, 'Welcome', {
75
64
  htmlTemplate: 'welcome',
76
- templateData: { name: user.username, link: envConfig.email.verificationLink + '/' + verificationToken },
65
+ templateData: { name: user.username, link: envConfig.email.verificationLink + '/' + user.verificationToken },
77
66
  });
78
67
 
79
68
  return user;
80
69
  }
81
70
 
82
- /**
83
- * Verify user with token
84
- *
85
- * @param token
86
- */
87
- async verify(token: string): Promise<boolean> {
88
- const user = await this.userModel.findOne({ verificationToken: token }).exec();
89
-
90
- if (!user) {
91
- throw new NotFoundException();
92
- }
93
-
94
- if (!user.verificationToken) {
95
- throw new Error('User has no token');
96
- }
97
-
98
- if (user.verified) {
99
- throw new Error('User already verified');
100
- }
101
-
102
- await this.userModel.findByIdAndUpdate(user.id, { $set: { verified: true, verificationToken: null } }).exec();
103
-
104
- return true;
105
- }
106
-
107
- /**
108
- * Set newpassword for user with token
109
- *
110
- * @param token
111
- * @param newPassword
112
- */
113
- async resetPassword(token: string, newPassword: string): Promise<boolean> {
114
- const user = await this.userModel.findOne({ passwordResetToken: token }).exec();
115
-
116
- if (!user) {
117
- throw new NotFoundException();
118
- }
119
-
120
- const cryptedPassword = await bcrypt.hash(newPassword, 10);
121
- await this.userModel
122
- .findByIdAndUpdate(user.id, { $set: { password: cryptedPassword, passwordResetToken: null } })
123
- .exec();
124
-
125
- return true;
126
- }
127
-
128
- /**
129
- * Request email with password reset link
130
- *
131
- * @param email
132
- */
133
- async requestPasswordResetMail(email: string): Promise<boolean> {
134
- const user = await this.userModel.findOne({ email }).exec();
135
-
136
- if (!user) {
137
- throw new NotFoundException();
138
- }
139
-
140
- const resetToken = crypto.randomBytes(32).toString('hex');
141
- await this.userModel.findByIdAndUpdate(user.id, { $set: { passwordResetToken: resetToken } }).exec();
142
-
143
- await this.emailService.sendMail(user.email, 'Password reset', {
144
- htmlTemplate: 'password-reset',
145
- templateData: { name: user.username, link: envConfig.email.passwordResetLink + '/' + resetToken },
146
- });
147
-
148
- return true;
149
- }
150
-
151
71
  /**
152
72
  * Get users via filter
153
73
  */