@lenne.tech/nest-server 8.2.0 → 8.3.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 (37) hide show
  1. package/dist/core/common/decorators/restricted.decorator.d.ts +1 -0
  2. package/dist/core/common/decorators/restricted.decorator.js +4 -1
  3. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  4. package/dist/core/common/enums/role.enum.d.ts +3 -2
  5. package/dist/core/common/enums/role.enum.js +3 -2
  6. package/dist/core/common/enums/role.enum.js.map +1 -1
  7. package/dist/core/common/helpers/db.helper.d.ts +2 -0
  8. package/dist/core/common/helpers/db.helper.js +13 -2
  9. package/dist/core/common/helpers/db.helper.js.map +1 -1
  10. package/dist/core/common/helpers/input.helper.d.ts +2 -0
  11. package/dist/core/common/helpers/input.helper.js +5 -2
  12. package/dist/core/common/helpers/input.helper.js.map +1 -1
  13. package/dist/core/common/services/module.service.d.ts +1 -0
  14. package/dist/core/common/services/module.service.js +15 -11
  15. package/dist/core/common/services/module.service.js.map +1 -1
  16. package/dist/core/modules/auth/guards/roles.guard.js +1 -2
  17. package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
  18. package/dist/core/modules/user/core-user.service.js +1 -6
  19. package/dist/core/modules/user/core-user.service.js.map +1 -1
  20. package/dist/server/modules/user/avatar.controller.js +1 -1
  21. package/dist/server/modules/user/avatar.controller.js.map +1 -1
  22. package/dist/server/modules/user/user.resolver.js +6 -6
  23. package/dist/server/modules/user/user.resolver.js.map +1 -1
  24. package/dist/server/modules/user/user.service.js +5 -1
  25. package/dist/server/modules/user/user.service.js.map +1 -1
  26. package/dist/tsconfig.build.tsbuildinfo +1 -1
  27. package/package.json +4 -4
  28. package/src/core/common/decorators/restricted.decorator.ts +11 -5
  29. package/src/core/common/enums/role.enum.ts +23 -5
  30. package/src/core/common/helpers/db.helper.ts +16 -1
  31. package/src/core/common/helpers/input.helper.ts +7 -5
  32. package/src/core/common/services/module.service.ts +17 -9
  33. package/src/core/modules/auth/guards/roles.guard.ts +4 -6
  34. package/src/core/modules/user/core-user.service.ts +1 -6
  35. package/src/server/modules/user/avatar.controller.ts +1 -1
  36. package/src/server/modules/user/user.resolver.ts +6 -6
  37. package/src/server/modules/user/user.service.ts +8 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "8.2.0",
3
+ "version": "8.3.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",
@@ -61,8 +61,8 @@
61
61
  "@nestjs/mongoose": "9.0.3",
62
62
  "@nestjs/passport": "8.2.1",
63
63
  "@nestjs/platform-express": "8.4.4",
64
- "apollo-server-core": "3.6.7",
65
- "apollo-server-express": "3.6.7",
64
+ "apollo-server-core": "3.6.8",
65
+ "apollo-server-express": "3.6.8",
66
66
  "bcrypt": "5.0.1",
67
67
  "class-transformer": "0.5.1",
68
68
  "class-validator": "0.13.2",
@@ -75,7 +75,7 @@
75
75
  "mongodb": "4.5.0",
76
76
  "mongoose": "6.3.2",
77
77
  "multer": "1.4.4",
78
- "node-mailjet": "3.3.13",
78
+ "node-mailjet": "3.4.1",
79
79
  "nodemailer": "6.7.5",
80
80
  "nodemon": "2.0.16",
81
81
  "passport": "0.5.2",
@@ -1,7 +1,7 @@
1
1
  import 'reflect-metadata';
2
2
  import { UnauthorizedException } from '@nestjs/common';
3
3
  import { RoleEnum } from '../enums/role.enum';
4
- import { getStringIds } from '../helpers/db.helper';
4
+ import { equalIds, getStringIds } from '../helpers/db.helper';
5
5
  import { IdsType } from '../types/ids.type';
6
6
 
7
7
  /**
@@ -31,12 +31,13 @@ export const getRestricted = (object: unknown, propertyKey: string) => {
31
31
  /**
32
32
  * Check data for restricted properties (properties with `Restricted` decorator)
33
33
  *
34
- * If restricted roles includes RoleEnum.OWNER, ownerId(s) from current data (in DB) must be set in options.
34
+ * If restricted roles includes RoleEnum.S_CREATOR, `creator` (createdBy) from current (DB) data must be set in options
35
+ * If restricted roles includes RoleEnum.S_OWNER, `ownerIds` from current (DB) data must be set in options
35
36
  */
36
37
  export const checkRestricted = (
37
38
  data: any,
38
39
  user: { id: any; hasRole: (roles: string[]) => boolean },
39
- options: { ignoreUndefined?: boolean; ownerIds?: IdsType; throwError?: boolean } = {},
40
+ options: { creator?: IdsType; ignoreUndefined?: boolean; ownerIds?: IdsType; throwError?: boolean } = {},
40
41
  processedObjects: any[] = []
41
42
  ) => {
42
43
  const config = {
@@ -76,8 +77,13 @@ export const checkRestricted = (
76
77
  if (roles && roles.some((value) => !!value)) {
77
78
  // Check user and user roles
78
79
  if (!user || !user.hasRole(roles)) {
79
- // Check special role for owner
80
- if (user && roles.includes(RoleEnum.OWNER)) {
80
+ // Check special creator role
81
+ if (user?.id && roles.includes(RoleEnum.S_CREATOR) && equalIds(user.id, config.creator)) {
82
+ continue;
83
+ }
84
+
85
+ // Check special owner role
86
+ else if (user && roles.includes(RoleEnum.S_OWNER)) {
81
87
  const userId = getStringIds(user);
82
88
  const ownerIds = config.ownerIds ? getStringIds(config.ownerIds) : null;
83
89
 
@@ -2,12 +2,30 @@
2
2
  * Enums for role decorator
3
3
  */
4
4
  export enum RoleEnum {
5
- // User must be an administrator
5
+ // ===================================================================================================================
6
+ // Real roles (integrated into user.roles), which can be used via @Roles for Models (properties)
7
+ // and Resolvers (methods)
8
+ // ===================================================================================================================
9
+
10
+ // User must be an administrator (see roles of user)
6
11
  ADMIN = 'admin',
7
12
 
8
- // User must be the owner of the processed object(s)
9
- OWNER = 'owner',
13
+ // ===================================================================================================================
14
+ // Special system roles, which can be used via @Roles for Models (properties) and Resolvers (methods)
15
+ // and via ServiceOptions for Resolver methods. This roles should not be integrated into user.roles!
16
+ // ===================================================================================================================
17
+
18
+ // User must be signed in (see context user, e.g. @GraphQLUser)
19
+ S_USER = 's_user',
20
+
21
+ // ===================================================================================================================
22
+ // Special system roles that check rights for DB objects and can be used via @Roles for Models (properties)
23
+ // and via ServiceOptions for Resolver methods. These roles should not be integrated in user.roles!
24
+ // ===================================================================================================================
25
+
26
+ // User must be the creator of the processed object(s) (see createdBy property of object(s))
27
+ S_CREATOR = 's_creator',
10
28
 
11
- // User must be signed in
12
- USER = 'user',
29
+ // User must be an owner of the processed object(s) (see owners property of object(s))
30
+ S_OWNER = 's_owner',
13
31
  }
@@ -4,6 +4,7 @@ import { Document, Model, PopulateOptions, Query, SchemaType, Types } from 'mong
4
4
  import { ResolveSelector } from '../interfaces/resolve-selector.interface';
5
5
  import { CoreModel } from '../models/core-model.model';
6
6
  import { FieldSelection } from '../types/field-selection.type';
7
+ import { IdsType } from '../types/ids.type';
7
8
  import { StringOrObjectId } from '../types/string-or-object-id.type';
8
9
 
9
10
  // =====================================================================================================================
@@ -78,6 +79,20 @@ export function addIds(
78
79
  return result;
79
80
  }
80
81
 
82
+ /**
83
+ * Checks if all IDs are equal
84
+ */
85
+ export function equalIds(...ids: IdsType[]): boolean {
86
+ if (!ids) {
87
+ return true;
88
+ }
89
+ const compare = getStringIds(ids[0]);
90
+ if (!compare) {
91
+ return false;
92
+ }
93
+ return ids.every((id) => getStringIds(id) === compare);
94
+ }
95
+
81
96
  /**
82
97
  * Get indexes of IDs in an array
83
98
  */
@@ -97,7 +112,7 @@ export function getIndexesViaIds(ids: any | any[], array: any[]): number[] {
97
112
  const indexes: number[] = [];
98
113
  ids.forEach((id) => {
99
114
  array.forEach((element, index) => {
100
- if (getStringIds(id) === getStringIds(element)) {
115
+ if (equalIds(id, element)) {
101
116
  indexes.push(index);
102
117
  }
103
118
  });
@@ -5,7 +5,7 @@ import * as _ from 'lodash';
5
5
  import { checkRestricted } from '../decorators/restricted.decorator';
6
6
  import { RoleEnum } from '../enums/role.enum';
7
7
  import { IdsType } from '../types/ids.type';
8
- import { getStringIds } from './db.helper';
8
+ import { equalIds, getStringIds } from './db.helper';
9
9
 
10
10
  /**
11
11
  * Helper class for inputs
@@ -18,7 +18,7 @@ export default class InputHelper {
18
18
  public static async check(
19
19
  value: any,
20
20
  user: { id: any; hasRole: (roles: string[]) => boolean },
21
- options?: { metatype?: any; ownerIds?: IdsType; roles?: string | string[] }
21
+ options?: { creator?: IdsType; metatype?: any; ownerIds?: IdsType; roles?: string | string[] }
22
22
  ): Promise<any> {
23
23
  return check(value, user, options);
24
24
  }
@@ -182,7 +182,7 @@ export default class InputHelper {
182
182
  export async function check(
183
183
  value: any,
184
184
  user: { id: any; hasRole: (roles: string[]) => boolean },
185
- options?: { metatype?: any; ownerIds?: IdsType; roles?: string | string[]; throwError?: boolean }
185
+ options?: { creator?: IdsType; metatype?: any; ownerIds?: IdsType; roles?: string | string[]; throwError?: boolean }
186
186
  ): Promise<any> {
187
187
  const config = {
188
188
  throwError: true,
@@ -196,11 +196,13 @@ export async function check(
196
196
  roles = [roles];
197
197
  }
198
198
  let valid = false;
199
- if (roles.includes(RoleEnum.USER) && user?.id) {
199
+ if (roles.includes(RoleEnum.S_USER) && user?.id) {
200
200
  valid = true;
201
201
  } else if (user.hasRole(roles)) {
202
202
  valid = true;
203
- } else if (roles.includes(RoleEnum.OWNER) && user?.id && config.ownerIds) {
203
+ } else if (roles.includes(RoleEnum.S_CREATOR) && user?.id && equalIds(user.id, config.creator)) {
204
+ valid = true;
205
+ } else if (roles.includes(RoleEnum.S_OWNER) && user?.id && config.ownerIds) {
204
206
  let ownerIds: string | string[] = getStringIds(config.ownerIds);
205
207
  if (!Array.isArray(ownerIds)) {
206
208
  ownerIds = [ownerIds];
@@ -39,6 +39,7 @@ export abstract class ModuleService<T extends CoreModel = any> {
39
39
  input: any,
40
40
  currentUser: { id: any; hasRole: (roles: string[]) => boolean },
41
41
  options?: {
42
+ creator?: IdsType;
42
43
  metatype?: any;
43
44
  ownerIds?: IdsType;
44
45
  roles?: string | string[];
@@ -86,24 +87,31 @@ export abstract class ModuleService<T extends CoreModel = any> {
86
87
  await this.prepareInput(config.input, config.prepareInput);
87
88
  }
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
+
89
103
  // Get owner IDs
90
104
  let ownerIds = undefined;
91
105
  if (config.checkRights && this.checkRights) {
92
106
  ownerIds = getStringIds(config.ownerIds);
93
107
  if (!ownerIds?.length) {
94
- if (config.dbObject) {
95
- if (typeof config.dbObject === 'string' || config.dbObject instanceof Types.ObjectId) {
96
- ownerIds = (await this.get(getStringIds(config.dbObject)))?.ownerIds;
97
- } else {
98
- ownerIds = config.dbObject.ownerIds;
99
- }
100
- }
108
+ ownerIds = (await getDbObject())?.ownerIds;
101
109
  }
102
110
  }
103
111
 
104
112
  // Check rights for input
105
113
  if (config.input && config.checkRights && this.checkRights) {
106
- const opts: any = { ownerIds, roles: config.roles };
114
+ const opts: any = { creator: (await getDbObject())?.createdBy, ownerIds, roles: config.roles };
107
115
  if (config.inputType) {
108
116
  opts.metatype = config.resultType;
109
117
  }
@@ -129,7 +137,7 @@ export abstract class ModuleService<T extends CoreModel = any> {
129
137
 
130
138
  // Check output rights
131
139
  if (config.checkRights && this.checkRights) {
132
- const opts: any = { ownerIds, roles: config.roles, throwError: false };
140
+ const opts: any = { creator: (await getDbObject())?.createdBy, ownerIds, roles: config.roles, throwError: false };
133
141
  if (config.resultType) {
134
142
  opts.metatype = config.resultType;
135
143
  }
@@ -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
@@ -47,11 +47,6 @@ export abstract class CoreUserService<
47
47
  // Distinguish between different error messages when saving
48
48
  try {
49
49
  await createdUser.save();
50
- if (!createdUser.ownerIds) {
51
- createdUser.ownerIds = [];
52
- }
53
- createdUser.ownerIds.push(createdUser.id);
54
- await createdUser.save();
55
50
  } catch (error) {
56
51
  if (error.code === 11000) {
57
52
  throw new UnprocessableEntityException(
@@ -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(
@@ -40,13 +40,13 @@ export class UserResolver {
40
40
  /**
41
41
  * Get user via ID
42
42
  */
43
- @Roles(RoleEnum.USER)
43
+ @Roles(RoleEnum.S_USER)
44
44
  @Query((returns) => User, { description: 'Get user with specified ID' })
45
45
  async getUser(@Args('id') id: string, @Info() info: GraphQLResolveInfo, @GraphQLUser() user: User): Promise<User> {
46
46
  return await this.userService.get(id, {
47
47
  currentUser: user,
48
48
  fieldSelection: { info, select: 'getUser' },
49
- roles: [RoleEnum.OWNER, RoleEnum.ADMIN],
49
+ roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR],
50
50
  });
51
51
  }
52
52
 
@@ -89,13 +89,13 @@ export class UserResolver {
89
89
  /**
90
90
  * Delete existing user
91
91
  */
92
- @Roles(RoleEnum.USER)
92
+ @Roles(RoleEnum.S_USER)
93
93
  @Mutation((returns) => User, { description: 'Delete existing user' })
94
94
  async deleteUser(@Args('id') id: string, @Info() info: GraphQLResolveInfo, @GraphQLUser() user: User): Promise<User> {
95
95
  return await this.userService.delete(id, {
96
96
  currentUser: user,
97
97
  fieldSelection: { info, select: 'deleteUser' },
98
- roles: [RoleEnum.ADMIN, RoleEnum.OWNER],
98
+ roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR],
99
99
  });
100
100
  }
101
101
 
@@ -110,7 +110,7 @@ export class UserResolver {
110
110
  /**
111
111
  * Update existing user
112
112
  */
113
- @Roles(RoleEnum.USER)
113
+ @Roles(RoleEnum.S_USER)
114
114
  @Mutation((returns) => User, { description: 'Update existing user' })
115
115
  async updateUser(
116
116
  @Args('input') input: UserInput,
@@ -123,7 +123,7 @@ export class UserResolver {
123
123
  currentUser: user,
124
124
  fieldSelection: { info, select: 'updateUser' },
125
125
  inputType: UserInput,
126
- roles: [RoleEnum.ADMIN, RoleEnum.OWNER],
126
+ roles: [RoleEnum.ADMIN, RoleEnum.S_CREATOR],
127
127
  });
128
128
  }
129
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) {