@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.
- package/dist/core/common/decorators/restricted.decorator.d.ts +1 -0
- package/dist/core/common/decorators/restricted.decorator.js +4 -1
- package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
- package/dist/core/common/enums/role.enum.d.ts +3 -2
- package/dist/core/common/enums/role.enum.js +3 -2
- package/dist/core/common/enums/role.enum.js.map +1 -1
- package/dist/core/common/helpers/db.helper.d.ts +2 -0
- package/dist/core/common/helpers/db.helper.js +13 -2
- package/dist/core/common/helpers/db.helper.js.map +1 -1
- package/dist/core/common/helpers/input.helper.d.ts +2 -0
- package/dist/core/common/helpers/input.helper.js +5 -2
- package/dist/core/common/helpers/input.helper.js.map +1 -1
- package/dist/core/common/services/module.service.d.ts +1 -0
- package/dist/core/common/services/module.service.js +15 -11
- package/dist/core/common/services/module.service.js.map +1 -1
- package/dist/core/modules/auth/guards/roles.guard.js +1 -2
- package/dist/core/modules/auth/guards/roles.guard.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +1 -6
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/server/modules/user/avatar.controller.js +1 -1
- package/dist/server/modules/user/avatar.controller.js.map +1 -1
- package/dist/server/modules/user/user.resolver.js +6 -6
- package/dist/server/modules/user/user.resolver.js.map +1 -1
- package/dist/server/modules/user/user.service.js +5 -1
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/core/common/decorators/restricted.decorator.ts +11 -5
- package/src/core/common/enums/role.enum.ts +23 -5
- package/src/core/common/helpers/db.helper.ts +16 -1
- package/src/core/common/helpers/input.helper.ts +7 -5
- package/src/core/common/services/module.service.ts +17 -9
- package/src/core/modules/auth/guards/roles.guard.ts +4 -6
- package/src/core/modules/user/core-user.service.ts +1 -6
- package/src/server/modules/user/avatar.controller.ts +1 -1
- package/src/server/modules/user/user.resolver.ts +6 -6
- 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.
|
|
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.
|
|
65
|
-
"apollo-server-express": "3.6.
|
|
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.
|
|
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.
|
|
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
|
|
80
|
-
if (user && roles.includes(RoleEnum.
|
|
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
|
-
//
|
|
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
|
-
//
|
|
9
|
-
|
|
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
|
|
12
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
@@ -40,13 +40,13 @@ export class UserResolver {
|
|
|
40
40
|
/**
|
|
41
41
|
* Get user via ID
|
|
42
42
|
*/
|
|
43
|
-
@Roles(RoleEnum.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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) {
|