@lenne.tech/nest-server 8.1.0 → 8.3.1

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 (61) hide show
  1. package/dist/config.env.js +3 -2
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/decorators/restricted.decorator.d.ts +3 -0
  4. package/dist/core/common/decorators/restricted.decorator.js +14 -8
  5. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  6. package/dist/core/common/enums/role.enum.d.ts +3 -2
  7. package/dist/core/common/enums/role.enum.js +3 -2
  8. package/dist/core/common/enums/role.enum.js.map +1 -1
  9. package/dist/core/common/helpers/db.helper.d.ts +2 -0
  10. package/dist/core/common/helpers/db.helper.js +13 -2
  11. package/dist/core/common/helpers/db.helper.js.map +1 -1
  12. package/dist/core/common/helpers/input.helper.d.ts +14 -2
  13. package/dist/core/common/helpers/input.helper.js +53 -11
  14. package/dist/core/common/helpers/input.helper.js.map +1 -1
  15. package/dist/core/common/interfaces/service-options.interface.d.ts +6 -0
  16. package/dist/core/common/models/core-model.model.d.ts +1 -1
  17. package/dist/core/common/models/core-model.model.js.map +1 -1
  18. package/dist/core/common/pipes/check-input.pipe.js +1 -1
  19. package/dist/core/common/pipes/check-input.pipe.js.map +1 -1
  20. package/dist/core/common/services/crud.service.js +17 -19
  21. package/dist/core/common/services/crud.service.js.map +1 -1
  22. package/dist/core/common/services/module.service.d.ts +14 -1
  23. package/dist/core/common/services/module.service.js +43 -4
  24. package/dist/core/common/services/module.service.js.map +1 -1
  25. package/dist/core/common/types/ids.type.d.ts +8 -0
  26. package/dist/core/common/types/ids.type.js +3 -0
  27. package/dist/core/common/types/ids.type.js.map +1 -0
  28. package/dist/core/modules/auth/guards/roles.guard.js +1 -2
  29. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  30. package/dist/core/modules/user/core-user.service.js +34 -36
  31. package/dist/core/modules/user/core-user.service.js.map +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/server/modules/user/avatar.controller.js +1 -1
  36. package/dist/server/modules/user/avatar.controller.js.map +1 -1
  37. package/dist/server/modules/user/user.model.d.ts +1 -1
  38. package/dist/server/modules/user/user.resolver.d.ts +2 -2
  39. package/dist/server/modules/user/user.resolver.js +30 -14
  40. package/dist/server/modules/user/user.resolver.js.map +1 -1
  41. package/dist/server/modules/user/user.service.js +5 -1
  42. package/dist/server/modules/user/user.service.js.map +1 -1
  43. package/dist/tsconfig.build.tsbuildinfo +1 -1
  44. package/package.json +39 -39
  45. package/src/config.env.ts +3 -2
  46. package/src/core/common/decorators/restricted.decorator.ts +24 -12
  47. package/src/core/common/enums/role.enum.ts +24 -6
  48. package/src/core/common/helpers/db.helper.ts +16 -1
  49. package/src/core/common/helpers/input.helper.ts +65 -14
  50. package/src/core/common/interfaces/service-options.interface.ts +19 -1
  51. package/src/core/common/models/core-model.model.ts +1 -1
  52. package/src/core/common/pipes/check-input.pipe.ts +1 -1
  53. package/src/core/common/services/crud.service.ts +17 -22
  54. package/src/core/common/services/module.service.ts +83 -9
  55. package/src/core/common/types/ids.type.ts +7 -0
  56. package/src/core/modules/auth/guards/roles.guard.ts +5 -7
  57. package/src/core/modules/user/core-user.service.ts +38 -45
  58. package/src/index.ts +1 -0
  59. package/src/server/modules/user/avatar.controller.ts +1 -1
  60. package/src/server/modules/user/user.resolver.ts +26 -20
  61. package/src/server/modules/user/user.service.ts +8 -1
@@ -1,9 +1,11 @@
1
- import { Document, Model } from 'mongoose';
2
- import { popAndMap } from '../helpers/db.helper';
1
+ import { Document, Model, Types } from 'mongoose';
2
+ import { getStringIds, popAndMap } from '../helpers/db.helper';
3
+ import { check } from '../helpers/input.helper';
3
4
  import { prepareInput, prepareOutput } from '../helpers/service.helper';
4
5
  import { ServiceOptions } from '../interfaces/service-options.interface';
5
6
  import { CoreModel } from '../models/core-model.model';
6
7
  import { FieldSelection } from '../types/field-selection.type';
8
+ import { IdsType } from '../types/ids.type';
7
9
 
8
10
  /**
9
11
  * Module service class to be extended by concrete module services
@@ -30,17 +32,49 @@ export abstract class ModuleService<T extends CoreModel = any> {
30
32
  this.mainModelConstructor = options?.mainModelConstructor;
31
33
  }
32
34
 
35
+ /**
36
+ * Check rights of current user for input
37
+ */
38
+ checkRights(
39
+ input: any,
40
+ currentUser: { id: any; hasRole: (roles: string[]) => boolean },
41
+ options?: {
42
+ creator?: IdsType;
43
+ metatype?: any;
44
+ ownerIds?: IdsType;
45
+ roles?: string | string[];
46
+ throwError?: boolean;
47
+ }
48
+ ): Promise<any> {
49
+ const config = {
50
+ metatype: this.mainModelConstructor,
51
+ ...options,
52
+ };
53
+ return check(input, currentUser, config);
54
+ }
55
+
56
+ /**
57
+ * Get function to get Object via ID, necessary for checkInput
58
+ */
59
+ abstract get(id: any, ...args: any[]): any;
60
+
33
61
  /**
34
62
  * Run service function with pre- and post-functions
35
63
  */
36
64
  async process(
37
65
  serviceFunc: (options?: { [key: string]: any; input?: any; serviceOptions?: ServiceOptions }) => any,
38
- options?: { [key: string]: any; input?: any; serviceOptions?: ServiceOptions }
66
+ options?: {
67
+ [key: string]: any;
68
+ dbObject?: string | Types.ObjectId | any;
69
+ input?: any;
70
+ serviceOptions?: ServiceOptions;
71
+ }
39
72
  ) {
40
73
  // Configuration with default values
41
74
  const config = {
42
- currentUser: null,
43
- fieldSelection: null,
75
+ checkRights: true,
76
+ dbObject: options?.dbObject,
77
+ input: options?.input,
44
78
  processFieldSelection: {},
45
79
  prepareInput: {},
46
80
  prepareOutput: {},
@@ -50,11 +84,42 @@ export abstract class ModuleService<T extends CoreModel = any> {
50
84
 
51
85
  // Prepare input
52
86
  if (config.prepareInput && this.prepareInput) {
53
- await this.prepareInput(options?.input, config.prepareInput);
87
+ await this.prepareInput(config.input, config.prepareInput);
88
+ }
89
+
90
+ // Get DB object
91
+ const getDbObject = async () => {
92
+ if (config.dbObject) {
93
+ if (typeof config.dbObject === 'string' || config.dbObject instanceof Types.ObjectId) {
94
+ const dbObject = await this.get(getStringIds(config.dbObject));
95
+ if (dbObject) {
96
+ config.dbObject = dbObject;
97
+ }
98
+ }
99
+ }
100
+ return config.dbObject;
101
+ };
102
+
103
+ // Get owner IDs
104
+ let ownerIds = undefined;
105
+ if (config.checkRights && this.checkRights) {
106
+ ownerIds = getStringIds(config.ownerIds);
107
+ if (!ownerIds?.length) {
108
+ ownerIds = (await getDbObject())?.ownerIds;
109
+ }
110
+ }
111
+
112
+ // Check rights for input
113
+ if (config.input && config.checkRights && this.checkRights) {
114
+ const opts: any = { creator: (await getDbObject())?.createdBy, ownerIds, roles: config.roles };
115
+ if (config.inputType) {
116
+ opts.metatype = config.resultType;
117
+ }
118
+ config.input = await this.checkRights(config.input, config.currentUser as any, opts);
54
119
  }
55
120
 
56
121
  // Run service function
57
- const result = await serviceFunc(options);
122
+ let result = await serviceFunc(config);
58
123
 
59
124
  // Pop and map main model
60
125
  if (config.processFieldSelection && config.fieldSelection && this.processFieldSelection) {
@@ -67,10 +132,19 @@ export abstract class ModuleService<T extends CoreModel = any> {
67
132
  if (config.processFieldSelection && config.fieldSelection && this.processFieldSelection) {
68
133
  config.prepareOutput.targetModel = null;
69
134
  }
70
- return this.prepareOutput(result, config.prepareOutput);
135
+ result = await this.prepareOutput(result, config.prepareOutput);
136
+ }
137
+
138
+ // Check output rights
139
+ if (config.checkRights && this.checkRights) {
140
+ const opts: any = { creator: (await getDbObject())?.createdBy, ownerIds, roles: config.roles, throwError: false };
141
+ if (config.resultType) {
142
+ opts.metatype = config.resultType;
143
+ }
144
+ result = await this.checkRights(result, config.currentUser as any, opts);
71
145
  }
72
146
 
73
- // Return result without output preparation
147
+ // Return (prepared) result
74
148
  return result;
75
149
  }
76
150
 
@@ -0,0 +1,7 @@
1
+ import { Types } from 'mongoose';
2
+
3
+ export type IdsType =
4
+ | string
5
+ | Types.ObjectId
6
+ | { id?: string | Types.ObjectId; _id?: string | Types.ObjectId }
7
+ | (string | Types.ObjectId | { id?: string | Types.ObjectId; _id?: string | Types.ObjectId })[];
@@ -1,4 +1,4 @@
1
- import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
1
+ import { ExecutionContext, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
2
2
  import { Reflector } from '@nestjs/core';
3
3
  import { GqlExecutionContext } from '@nestjs/graphql';
4
4
  import { RoleEnum } from '../../../common/enums/role.enum';
@@ -8,7 +8,8 @@ import { AuthGuard } from './auth.guard';
8
8
  * Role guard
9
9
  *
10
10
  * The RoleGuard is activated by the Role decorator. It checks whether the current user has at least one of the
11
- * specified roles. If this is not the case, an UnauthorizedException is thrown.
11
+ * specified roles or is logged in when the S_USER role is set.
12
+ * If this is not the case, an UnauthorizedException is thrown.
12
13
  */
13
14
  @Injectable()
14
15
  export class RolesGuard extends AuthGuard('jwt') {
@@ -41,11 +42,8 @@ export class RolesGuard extends AuthGuard('jwt') {
41
42
  // Get args
42
43
  const args: any = GqlExecutionContext.create(context).getArgs();
43
44
 
44
- // Check special role for user or owner
45
- if (
46
- user &&
47
- (roles.includes(RoleEnum.USER) || (roles.includes(RoleEnum.OWNER) && user.id.toString() === args.id))
48
- ) {
45
+ // Check special user role (user is logged in)
46
+ if (user && roles.includes(RoleEnum.S_USER)) {
49
47
  return user;
50
48
  }
51
49
 
@@ -35,7 +35,7 @@ export abstract class CoreUserService<
35
35
  * Create user
36
36
  */
37
37
  async create(input: any, serviceOptions?: ServiceOptions): Promise<TUser> {
38
- merge({ prepareInput: { create: true } }, serviceOptions);
38
+ serviceOptions = merge({ prepareInput: { create: true } }, serviceOptions);
39
39
  return this.process(
40
40
  async (data) => {
41
41
  // Create user with verification token
@@ -68,16 +68,11 @@ export abstract class CoreUserService<
68
68
  * Get user via email
69
69
  */
70
70
  async getViaEmail(email: string, serviceOptions?: ServiceOptions): Promise<TUser> {
71
- return this.process(
72
- async () => {
73
- const user = await this.mainDbModel.findOne({ email }).exec();
74
- if (!user) {
75
- throw new NotFoundException(`No user found with email: ${email}`);
76
- }
77
- return user;
78
- },
79
- { serviceOptions }
80
- );
71
+ const dbObject = await this.mainDbModel.findOne({ email }).exec();
72
+ if (!dbObject) {
73
+ throw new NotFoundException(`No user found with email: ${email}`);
74
+ }
75
+ return this.process(async () => dbObject, { dbObject, serviceOptions });
81
76
  }
82
77
 
83
78
  /**
@@ -97,30 +92,29 @@ export abstract class CoreUserService<
97
92
  * Verify user with token
98
93
  */
99
94
  async verify(token: string, serviceOptions?: ServiceOptions): Promise<TUser> {
95
+ // Get user
96
+ const dbObject = await this.mainDbModel.findOne({ verificationToken: token }).exec();
97
+ if (!dbObject) {
98
+ throw new NotFoundException(`No user found with verify token: ${token}`);
99
+ }
100
+ if (!dbObject.verificationToken) {
101
+ throw new Error('User has no token');
102
+ }
103
+ if (dbObject.verified) {
104
+ throw new Error('User already verified');
105
+ }
100
106
  return this.process(
101
107
  async () => {
102
- // Get user
103
- const user = await this.mainDbModel.findOne({ verificationToken: token }).exec();
104
- if (!user) {
105
- throw new NotFoundException(`No user found with verify token: ${token}`);
106
- }
107
- if (!user.verificationToken) {
108
- throw new Error('User has no token');
109
- }
110
- if (user.verified) {
111
- throw new Error('User already verified');
112
- }
113
-
114
108
  // Update user
115
- await Object.assign(user, {
109
+ await Object.assign(dbObject, {
116
110
  verified: true,
117
111
  verificationToken: null,
118
112
  }).save();
119
113
 
120
114
  // Return prepared user
121
- return user;
115
+ return dbObject;
122
116
  },
123
- { serviceOptions }
117
+ { dbObject, serviceOptions }
124
118
  );
125
119
  }
126
120
 
@@ -128,24 +122,24 @@ export abstract class CoreUserService<
128
122
  * Set newpassword for user with token
129
123
  */
130
124
  async resetPassword(token: string, newPassword: string, serviceOptions?: ServiceOptions): Promise<TUser> {
125
+ // Get user
126
+ const dbObject = await this.mainDbModel.findOne({ passwordResetToken: token }).exec();
127
+ if (!dbObject) {
128
+ throw new NotFoundException(`No user found with password reset token: ${token}`);
129
+ }
130
+
131
131
  return this.process(
132
132
  async () => {
133
- // Get user
134
- const user = await this.mainDbModel.findOne({ passwordResetToken: token }).exec();
135
- if (!user) {
136
- throw new NotFoundException(`No user found with password reset token: ${token}`);
137
- }
138
-
139
133
  // Update user
140
- await Object.assign(user, {
134
+ await Object.assign(dbObject, {
141
135
  password: await bcrypt.hash(newPassword, 10),
142
136
  passwordResetToken: null,
143
137
  }).save();
144
138
 
145
139
  // Return user
146
- return user;
140
+ return dbObject;
147
141
  },
148
- { serviceOptions }
142
+ { dbObject, serviceOptions }
149
143
  );
150
144
  }
151
145
 
@@ -153,23 +147,22 @@ export abstract class CoreUserService<
153
147
  * Set password rest token for email
154
148
  */
155
149
  async setPasswordResetTokenForEmail(email: string, serviceOptions?: ServiceOptions): Promise<TUser> {
150
+ // Get user
151
+ const dbObject = await this.mainDbModel.findOne({ email }).exec();
152
+ if (!dbObject) {
153
+ throw new NotFoundException(`No user found with email: ${email}`);
154
+ }
156
155
  return this.process(
157
156
  async () => {
158
- // Get user
159
- const user = await this.mainDbModel.findOne({ email }).exec();
160
- if (!user) {
161
- throw new NotFoundException(`No user found with email: ${email}`);
162
- }
163
-
164
157
  // Set reset token
165
158
  const resetToken = crypto.randomBytes(32).toString('hex');
166
- user.passwordResetToken = resetToken;
167
- await user.save();
159
+ dbObject.passwordResetToken = resetToken;
160
+ await dbObject.save();
168
161
 
169
162
  // Return user
170
- return user;
163
+ return dbObject;
171
164
  },
172
- { serviceOptions }
165
+ { dbObject, serviceOptions }
173
166
  );
174
167
  }
175
168
 
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export * from './core/common/services/module.service';
52
52
  export * from './core/common/services/template.service';
53
53
  export * from './core/common/types/core-model-constructor.type';
54
54
  export * from './core/common/types/field-selection.type';
55
+ export * from './core/common/types/ids.type';
55
56
  export * from './core/common/types/plain-input.type';
56
57
  export * from './core/common/types/string-or-object-id.type';
57
58
 
@@ -22,7 +22,7 @@ export class AvatarController {
22
22
  /**
23
23
  * Upload files
24
24
  */
25
- @Roles(RoleEnum.USER)
25
+ @Roles(RoleEnum.S_USER)
26
26
  @Post('upload')
27
27
  @UseInterceptors(
28
28
  FileInterceptor(
@@ -6,7 +6,6 @@ import { FilterArgs } from '../../../core/common/args/filter.args';
6
6
  import { GraphQLUser } from '../../../core/common/decorators/graphql-user.decorator';
7
7
  import { Roles } from '../../../core/common/decorators/roles.decorator';
8
8
  import { RoleEnum } from '../../../core/common/enums/role.enum';
9
- import { check } from '../../../core/common/helpers/input.helper';
10
9
  import { UserCreateInput } from './inputs/user-create.input';
11
10
  import { UserInput } from './inputs/user.input';
12
11
  import { User } from './user.model';
@@ -32,16 +31,23 @@ export class UserResolver {
32
31
  @Roles(RoleEnum.ADMIN)
33
32
  @Query((returns) => [User], { description: 'Find users (via filter)' })
34
33
  async findUsers(@Info() info: GraphQLResolveInfo, @Args() args?: FilterArgs) {
35
- return await this.userService.find(args, { fieldSelection: { info, select: 'findUsers' } });
34
+ return await this.userService.find(args, {
35
+ fieldSelection: { info, select: 'findUsers' },
36
+ inputType: FilterArgs,
37
+ });
36
38
  }
37
39
 
38
40
  /**
39
41
  * Get user via ID
40
42
  */
41
- @Roles(RoleEnum.OWNER, RoleEnum.ADMIN)
43
+ @Roles(RoleEnum.S_USER)
42
44
  @Query((returns) => User, { description: 'Get user with specified ID' })
43
- async getUser(@Args('id') id: string, @Info() info: GraphQLResolveInfo): Promise<User> {
44
- return await this.userService.get(id, { fieldSelection: { info, select: 'getUser' } });
45
+ async getUser(@Args('id') id: string, @Info() info: GraphQLResolveInfo, @GraphQLUser() user: User): Promise<User> {
46
+ return await this.userService.get(id, {
47
+ currentUser: user,
48
+ fieldSelection: { info, select: 'getUser' },
49
+ roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR],
50
+ });
45
51
  }
46
52
 
47
53
  /**
@@ -73,21 +79,24 @@ export class UserResolver {
73
79
  @GraphQLUser() user: User,
74
80
  @Info() info: GraphQLResolveInfo
75
81
  ): Promise<User> {
76
- // Check input
77
- // Hint: necessary as long as global CheckInputPipe can't access context for current user
78
- // (see https://github.com/nestjs/graphql/issues/325)
79
- input = await check(input, user, UserCreateInput);
80
-
81
- return await this.userService.create(input, { currentUser: user, fieldSelection: { info, select: 'createUser' } });
82
+ return await this.userService.create(input, {
83
+ currentUser: user,
84
+ fieldSelection: { info, select: 'createUser' },
85
+ inputType: UserCreateInput,
86
+ });
82
87
  }
83
88
 
84
89
  /**
85
90
  * Delete existing user
86
91
  */
87
- @Roles(RoleEnum.ADMIN, RoleEnum.OWNER)
92
+ @Roles(RoleEnum.S_USER)
88
93
  @Mutation((returns) => User, { description: 'Delete existing user' })
89
- async deleteUser(@Args('id') id: string, @Info() info: GraphQLResolveInfo): Promise<User> {
90
- return await this.userService.delete(id, { fieldSelection: { info, select: 'deleteUser' } });
94
+ async deleteUser(@Args('id') id: string, @Info() info: GraphQLResolveInfo, @GraphQLUser() user: User): Promise<User> {
95
+ return await this.userService.delete(id, {
96
+ currentUser: user,
97
+ fieldSelection: { info, select: 'deleteUser' },
98
+ roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR],
99
+ });
91
100
  }
92
101
 
93
102
  /**
@@ -101,7 +110,7 @@ export class UserResolver {
101
110
  /**
102
111
  * Update existing user
103
112
  */
104
- @Roles(RoleEnum.ADMIN, RoleEnum.OWNER)
113
+ @Roles(RoleEnum.S_USER)
105
114
  @Mutation((returns) => User, { description: 'Update existing user' })
106
115
  async updateUser(
107
116
  @Args('input') input: UserInput,
@@ -109,15 +118,12 @@ export class UserResolver {
109
118
  @GraphQLUser() user: User,
110
119
  @Info() info: GraphQLResolveInfo
111
120
  ): Promise<User> {
112
- // Check input
113
- // Hint: necessary as long as global CheckInputPipe can't access context for current user
114
- // (see https://github.com/nestjs/graphql/issues/325)
115
- input = await check(input, user, UserInput);
116
-
117
121
  // Update user
118
122
  return await this.userService.update(id, input, {
119
123
  currentUser: user,
120
124
  fieldSelection: { info, select: 'updateUser' },
125
+ inputType: UserInput,
126
+ roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR],
121
127
  });
122
128
  }
123
129
 
@@ -44,7 +44,14 @@ export class UserService extends CoreUserService<User, UserInput, UserCreateInpu
44
44
  */
45
45
  async create(input: UserCreateInput, serviceOptions?: ServiceOptions): Promise<User> {
46
46
  // Get prepared user
47
- const user = await super.create(input, serviceOptions);
47
+ let user = await super.create(input, serviceOptions);
48
+
49
+ // Add the createdBy information in an additional step if it was not set by the system,
50
+ // because the user created himself and could not exist as currentUser before
51
+ if (!user.createdBy) {
52
+ await this.mainDbModel.findByIdAndUpdate(user.id, { createdBy: user.id });
53
+ user = await this.get(user.id, serviceOptions);
54
+ }
48
55
 
49
56
  // Publish action
50
57
  if (serviceOptions?.pubSub === undefined || serviceOptions.pubSub) {