rl-core-api 0.13.1 → 0.14.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.
@@ -6,6 +6,6 @@ export declare class AllExceptionsFilter implements ExceptionFilter {
6
6
  private readonly env;
7
7
  private readonly logger;
8
8
  constructor(events: EventEmitter2, env: EnvService);
9
- catch(exception: unknown, host: ArgumentsHost): void;
9
+ catch(rawException: unknown, host: ArgumentsHost): void;
10
10
  private publish;
11
11
  }
@@ -16,13 +16,25 @@ const env_service_1 = require("../config/env.service");
16
16
  const requestLog_context_1 = require("../context/requestLog.context");
17
17
  const appEvent_enum_1 = require("../events/appEvent.enum");
18
18
  const ip_util_1 = require("../utils/ip.util");
19
+ const asHttpException = (exception) => {
20
+ if (!(exception instanceof Error) ||
21
+ exception.name !== "MulterError") {
22
+ return exception;
23
+ }
24
+ const code = exception.code;
25
+ if (code === "LIMIT_FILE_SIZE") {
26
+ return new common_1.PayloadTooLargeException("Arquivo excede o tamanho permitido");
27
+ }
28
+ return new common_1.BadRequestException("Envio de arquivo inválido");
29
+ };
19
30
  let AllExceptionsFilter = class AllExceptionsFilter {
20
31
  constructor(events, env) {
21
32
  this.events = events;
22
33
  this.env = env;
23
34
  this.logger = new common_1.Logger("Exception");
24
35
  }
25
- catch(exception, host) {
36
+ catch(rawException, host) {
37
+ const exception = asHttpException(rawException);
26
38
  const ctx = host.switchToHttp();
27
39
  const response = ctx.getResponse();
28
40
  const request = ctx.getRequest();
@@ -103,6 +103,9 @@ let AuthService = class AuthService {
103
103
  async verifyTwoFactor(dto, ctx) {
104
104
  const payload = await this.decodePending(dto.pendingToken);
105
105
  const user = await this.users.findById(payload.sub);
106
+ if (!user.isActive) {
107
+ throw accountInactive();
108
+ }
106
109
  const maxAttempts = this.env.get("LOGIN_MAX_ATTEMPTS");
107
110
  if (user.isLocked(maxAttempts)) {
108
111
  throw accountLocked();
@@ -67,13 +67,7 @@ let NotificationsService = class NotificationsService {
67
67
  async cleanupOldRead(retentionDays) {
68
68
  const limit = new Date();
69
69
  limit.setDate(limit.getDate() - retentionDays);
70
- const result = await this.notifications
71
- .createQueryBuilder()
72
- .softDelete()
73
- .where("created_at < :limit", { limit })
74
- .andWhere(`EXISTS (SELECT 1 FROM notification_reads r WHERE r.notification_id = id)`)
75
- .execute();
76
- return result.affected ?? 0;
70
+ return await this.notifications.softDeleteReadByAllBefore(limit);
77
71
  }
78
72
  visibilityOf(user) {
79
73
  return { userId: user.id, roleNames: user.roles ?? [] };
@@ -10,5 +10,6 @@ export declare class NotificationRepository extends Repository<Notification> {
10
10
  countUnread(params: VisibilityParams): Promise<number>;
11
11
  isVisibleToUser(notificationId: string, params: VisibilityParams): Promise<boolean>;
12
12
  findUnreadIds(params: VisibilityParams): Promise<string[]>;
13
+ softDeleteReadByAllBefore(limit: Date): Promise<number>;
13
14
  private baseVisibleQuery;
14
15
  }
@@ -25,6 +25,28 @@ const VISIBLE_TO_USER = `EXISTS (
25
25
  )
26
26
  )`;
27
27
  const NO_ROLES = ["-"];
28
+ const HAS_PENDING_READER = `EXISTS (
29
+ SELECT 1
30
+ FROM users u
31
+ JOIN notification_scopes s
32
+ ON s.notification_id = notifications.id
33
+ AND s.deleted_at IS NULL
34
+ AND (
35
+ (s.scope_type = :userScope AND s.scope_key = u.id)
36
+ OR (s.scope_type = :roleScope AND EXISTS (
37
+ SELECT 1
38
+ FROM user_roles ur
39
+ JOIN roles r ON r.id = ur.role_id AND r.deleted_at IS NULL
40
+ WHERE ur.user_id = u.id AND r.name = s.scope_key))
41
+ OR s.scope_type = :allScope
42
+ )
43
+ WHERE u.deleted_at IS NULL
44
+ AND u.is_active = 1
45
+ AND NOT EXISTS (
46
+ SELECT 1 FROM notification_reads nr
47
+ WHERE nr.notification_id = notifications.id AND nr.user_id = u.id
48
+ )
49
+ )`;
28
50
  let NotificationRepository = class NotificationRepository extends typeorm_1.Repository {
29
51
  constructor(dataSource) {
30
52
  super(notification_schema_1.Notification, dataSource.createEntityManager());
@@ -54,6 +76,18 @@ let NotificationRepository = class NotificationRepository extends typeorm_1.Repo
54
76
  .getRawMany();
55
77
  return rows.map((row) => row.id);
56
78
  }
79
+ async softDeleteReadByAllBefore(limit) {
80
+ const result = await this.createQueryBuilder()
81
+ .softDelete()
82
+ .where("created_at < :limit", { limit })
83
+ .andWhere(`NOT ${HAS_PENDING_READER}`, {
84
+ userScope: scopeType_enum_1.ScopeType.USER,
85
+ roleScope: scopeType_enum_1.ScopeType.ROLE,
86
+ allScope: scopeType_enum_1.ScopeType.ALL,
87
+ })
88
+ .execute();
89
+ return result.affected ?? 0;
90
+ }
57
91
  baseVisibleQuery(params) {
58
92
  return this.createQueryBuilder("notification")
59
93
  .leftJoinAndSelect("notification.reads", "read", "read.user_id = :userId", { userId: params.userId })
@@ -54,7 +54,7 @@ export declare class RbacService {
54
54
  deletePermission(id: string): Promise<void>;
55
55
  listRoles(): Promise<Role[]>;
56
56
  getRole(id: string): Promise<Role>;
57
- createRole(dto: CreateRoleCommand): Promise<Role>;
57
+ createRole(dto: CreateRoleCommand, actorPermissions?: string[]): Promise<Role>;
58
58
  updateRole(id: string, dto: UpdateRoleCommand, actorPermissions?: string[]): Promise<Role>;
59
59
  private assertGrantableByActor;
60
60
  deleteRole(id: string): Promise<void>;
@@ -71,8 +71,10 @@ export declare class RbacService {
71
71
  listGroups(): Promise<Group[]>;
72
72
  listManagedGroups(managerId: string): Promise<Group[]>;
73
73
  getGroup(id: string): Promise<Group>;
74
- createGroup(dto: CreateGroupCommand): Promise<Group>;
75
- updateGroup(id: string, dto: UpdateGroupCommand): Promise<Group>;
74
+ createGroup(dto: CreateGroupCommand, actorPermissions?: string[]): Promise<Group>;
75
+ updateGroup(id: string, dto: UpdateGroupCommand, actorPermissions?: string[]): Promise<Group>;
76
+ private resolveGrantableRoles;
77
+ private assertParentIsValid;
76
78
  deleteGroup(id: string): Promise<void>;
77
79
  addUserToGroup(groupId: string, userId: string): Promise<void>;
78
80
  removeUserFromGroup(groupId: string, userId: string): Promise<void>;
@@ -21,6 +21,7 @@ const resource_repository_1 = require("../infra/repositories/resource.repository
21
21
  const role_repository_1 = require("../infra/repositories/role.repository");
22
22
  const scope_repository_1 = require("../infra/repositories/scope.repository");
23
23
  const user_repository_1 = require("../../users/infra/repositories/user.repository");
24
+ const isEnabled = (row) => row.isActive !== false;
24
25
  let RbacService = class RbacService {
25
26
  constructor(resourceRepo, actionRepo, scopeRepo, permissionRepo, roleRepo, groupRepo, userRepo, events) {
26
27
  this.resourceRepo = resourceRepo;
@@ -45,9 +46,16 @@ let RbacService = class RbacService {
45
46
  return [];
46
47
  }
47
48
  const codes = new Set();
48
- user.directPermissions?.forEach((p) => codes.add(p.code));
49
- user.roles?.forEach((r) => r.permissions?.forEach((p) => codes.add(p.code)));
50
- user.groups?.forEach((g) => g.roles?.forEach((r) => r.permissions?.forEach((p) => codes.add(p.code))));
49
+ const addFrom = (permissions) => {
50
+ permissions
51
+ ?.filter((p) => isEnabled(p))
52
+ .forEach((p) => codes.add(p.code));
53
+ };
54
+ addFrom(user.directPermissions);
55
+ user.roles?.filter(isEnabled).forEach((r) => addFrom(r.permissions));
56
+ user.groups
57
+ ?.filter(isEnabled)
58
+ .forEach((g) => g.roles?.filter(isEnabled).forEach((r) => addFrom(r.permissions)));
51
59
  return [...codes];
52
60
  }
53
61
  async getUserRoleNames(userId) {
@@ -276,7 +284,7 @@ let RbacService = class RbacService {
276
284
  }
277
285
  return role;
278
286
  }
279
- async createRole(dto) {
287
+ async createRole(dto, actorPermissions) {
280
288
  const exists = await this.roleRepo.findOne({
281
289
  where: { name: dto.name },
282
290
  withDeleted: true,
@@ -287,6 +295,7 @@ let RbacService = class RbacService {
287
295
  const permissions = dto.permissionIds?.length
288
296
  ? await this.permissionRepo.findBy({ id: (0, typeorm_1.In)(dto.permissionIds) })
289
297
  : [];
298
+ this.assertGrantableByActor(permissions, actorPermissions);
290
299
  const role = this.roleRepo.create({
291
300
  name: dto.name,
292
301
  description: dto.description,
@@ -440,22 +449,25 @@ let RbacService = class RbacService {
440
449
  }
441
450
  return group;
442
451
  }
443
- async createGroup(dto) {
444
- const roles = dto.roleIds?.length
445
- ? await this.roleRepo.findBy({ id: (0, typeorm_1.In)(dto.roleIds) })
446
- : [];
452
+ async createGroup(dto, actorPermissions) {
453
+ const roles = await this.resolveGrantableRoles(dto.roleIds, actorPermissions);
454
+ await this.assertParentIsValid(null, dto.parentId ?? null);
447
455
  const group = this.groupRepo.create({
448
456
  name: dto.name,
449
457
  description: dto.description,
450
458
  type: dto.type ?? "team",
451
459
  parentId: dto.parentId ?? null,
452
460
  managerId: dto.managerId ?? null,
453
- roles,
461
+ roles: roles ?? [],
454
462
  });
455
463
  return this.groupRepo.save(group);
456
464
  }
457
- async updateGroup(id, dto) {
465
+ async updateGroup(id, dto, actorPermissions) {
458
466
  const group = await this.getGroup(id);
467
+ const roles = await this.resolveGrantableRoles(dto.roleIds, actorPermissions);
468
+ if (dto.parentId !== undefined) {
469
+ await this.assertParentIsValid(id, dto.parentId ?? null);
470
+ }
459
471
  if (dto.name !== undefined) {
460
472
  group.name = dto.name;
461
473
  }
@@ -474,11 +486,43 @@ let RbacService = class RbacService {
474
486
  if (dto.isActive !== undefined) {
475
487
  group.isActive = dto.isActive;
476
488
  }
477
- if (dto.roleIds) {
478
- group.roles = await this.roleRepo.findBy({ id: (0, typeorm_1.In)(dto.roleIds) });
489
+ if (roles) {
490
+ group.roles = roles;
479
491
  }
480
492
  return this.groupRepo.save(group);
481
493
  }
494
+ async resolveGrantableRoles(roleIds, actorPermissions) {
495
+ if (!roleIds) {
496
+ return undefined;
497
+ }
498
+ if (roleIds.length === 0) {
499
+ return [];
500
+ }
501
+ const roles = await this.roleRepo.find({
502
+ where: { id: (0, typeorm_1.In)(roleIds) },
503
+ relations: { permissions: true },
504
+ });
505
+ if (roles.length !== roleIds.length) {
506
+ throw new common_1.NotFoundException("Algum dos papéis informados não existe");
507
+ }
508
+ this.assertGrantableByActor(roles.flatMap((role) => role.permissions ?? []), actorPermissions);
509
+ return roles;
510
+ }
511
+ async assertParentIsValid(groupId, parentId) {
512
+ if (!parentId) {
513
+ return;
514
+ }
515
+ if (parentId === groupId) {
516
+ throw new common_1.BadRequestException("Um grupo não pode ser pai de si mesmo");
517
+ }
518
+ const ancestorIds = await this.groupRepo.findAncestorIds(parentId);
519
+ if (ancestorIds.length === 0) {
520
+ throw new common_1.NotFoundException("Grupo pai não encontrado");
521
+ }
522
+ if (groupId && ancestorIds.includes(groupId)) {
523
+ throw new common_1.BadRequestException("O grupo pai não pode estar abaixo do próprio grupo");
524
+ }
525
+ }
482
526
  async deleteGroup(id) {
483
527
  const group = await this.getGroup(id);
484
528
  await this.groupRepo.softRemove(group);
@@ -3,5 +3,6 @@ import { Group } from "../schema/group.schema";
3
3
  export declare class GroupRepository extends Repository<Group> {
4
4
  constructor(dataSource: DataSource);
5
5
  findManagedGroupIds(managerId: string): Promise<string[]>;
6
+ findAncestorIds(groupId: string): Promise<string[]>;
6
7
  findMemberIds(groupIds: string[]): Promise<string[]>;
7
8
  }
@@ -32,6 +32,21 @@ let GroupRepository = class GroupRepository extends typeorm_1.Repository {
32
32
  `, [managerId]);
33
33
  return rows.map((row) => row.id);
34
34
  }
35
+ async findAncestorIds(groupId) {
36
+ const rows = await this.query(`
37
+ WITH RECURSIVE ancestors AS (
38
+ SELECT id, parent_id FROM \`groups\`
39
+ WHERE id = ? AND deleted_at IS NULL
40
+ UNION ALL
41
+ SELECT parent.id, parent.parent_id
42
+ FROM \`groups\` parent
43
+ INNER JOIN ancestors ON ancestors.parent_id = parent.id
44
+ WHERE parent.deleted_at IS NULL
45
+ )
46
+ SELECT id FROM ancestors
47
+ `, [groupId]);
48
+ return rows.map((row) => row.id);
49
+ }
35
50
  async findMemberIds(groupIds) {
36
51
  if (groupIds.length === 0) {
37
52
  return [];
@@ -41,14 +41,14 @@ export declare class RbacController {
41
41
  groups(): Promise<Group[]>;
42
42
  managedGroups(actor: AuthenticatedUser): Promise<Group[]>;
43
43
  group(id: string): Promise<Group>;
44
- createGroup(dto: CreateGroupCommand): Promise<Group>;
45
- updateGroup(id: string, dto: UpdateGroupCommand): Promise<Group>;
44
+ createGroup(dto: CreateGroupCommand, actor: AuthenticatedUser): Promise<Group>;
45
+ updateGroup(id: string, dto: UpdateGroupCommand, actor: AuthenticatedUser): Promise<Group>;
46
46
  deleteGroup(id: string): Promise<void>;
47
47
  addUserToGroup(id: string, userId: string): Promise<void>;
48
48
  removeUserFromGroup(id: string, userId: string): Promise<void>;
49
49
  roles(): Promise<Role[]>;
50
50
  role(id: string): Promise<Role>;
51
- createRole(dto: CreateRoleCommand): Promise<Role>;
51
+ createRole(dto: CreateRoleCommand, actor: AuthenticatedUser): Promise<Role>;
52
52
  updateRole(id: string, dto: UpdateRoleCommand, actor: AuthenticatedUser): Promise<Role>;
53
53
  deleteRole(id: string): Promise<void>;
54
54
  addPermissionToRole(id: string, permissionId: string, actor: AuthenticatedUser): Promise<Role>;
@@ -95,11 +95,11 @@ let RbacController = class RbacController {
95
95
  group(id) {
96
96
  return this.rbac.getGroup(id);
97
97
  }
98
- createGroup(dto) {
99
- return this.rbac.createGroup(dto);
98
+ createGroup(dto, actor) {
99
+ return this.rbac.createGroup(dto, actor.permissions);
100
100
  }
101
- updateGroup(id, dto) {
102
- return this.rbac.updateGroup(id, dto);
101
+ updateGroup(id, dto, actor) {
102
+ return this.rbac.updateGroup(id, dto, actor.permissions);
103
103
  }
104
104
  deleteGroup(id) {
105
105
  return this.rbac.deleteGroup(id);
@@ -116,8 +116,8 @@ let RbacController = class RbacController {
116
116
  role(id) {
117
117
  return this.rbac.getRole(id);
118
118
  }
119
- createRole(dto) {
120
- return this.rbac.createRole(dto);
119
+ createRole(dto, actor) {
120
+ return this.rbac.createRole(dto, actor.permissions);
121
121
  }
122
122
  updateRole(id, dto, actor) {
123
123
  return this.rbac.updateRole(id, dto, actor.permissions);
@@ -319,8 +319,9 @@ __decorate([
319
319
  (0, requirePermission_decorator_1.RequirePermission)("groups:create:any"),
320
320
  (0, swagger_1.ApiOkResponse)({ type: rbac_response_1.GroupResponse }),
321
321
  __param(0, (0, common_1.Body)()),
322
+ __param(1, (0, currentUser_decorator_1.CurrentUser)()),
322
323
  __metadata("design:type", Function),
323
- __metadata("design:paramtypes", [createGroup_command_1.CreateGroupCommand]),
324
+ __metadata("design:paramtypes", [createGroup_command_1.CreateGroupCommand, Object]),
324
325
  __metadata("design:returntype", Promise)
325
326
  ], RbacController.prototype, "createGroup", null);
326
327
  __decorate([
@@ -329,8 +330,9 @@ __decorate([
329
330
  (0, swagger_1.ApiOkResponse)({ type: rbac_response_1.GroupResponse }),
330
331
  __param(0, (0, common_1.Param)("id", common_1.ParseUUIDPipe)),
331
332
  __param(1, (0, common_1.Body)()),
333
+ __param(2, (0, currentUser_decorator_1.CurrentUser)()),
332
334
  __metadata("design:type", Function),
333
- __metadata("design:paramtypes", [String, updateGroup_command_1.UpdateGroupCommand]),
335
+ __metadata("design:paramtypes", [String, updateGroup_command_1.UpdateGroupCommand, Object]),
334
336
  __metadata("design:returntype", Promise)
335
337
  ], RbacController.prototype, "updateGroup", null);
336
338
  __decorate([
@@ -386,8 +388,9 @@ __decorate([
386
388
  (0, requirePermission_decorator_1.RequirePermission)("roles:create:any"),
387
389
  (0, swagger_1.ApiOkResponse)({ type: rbac_response_1.RoleResponse }),
388
390
  __param(0, (0, common_1.Body)()),
391
+ __param(1, (0, currentUser_decorator_1.CurrentUser)()),
389
392
  __metadata("design:type", Function),
390
- __metadata("design:paramtypes", [createRole_command_1.CreateRoleCommand]),
393
+ __metadata("design:paramtypes", [createRole_command_1.CreateRoleCommand, Object]),
391
394
  __metadata("design:returntype", Promise)
392
395
  ], RbacController.prototype, "createRole", null);
393
396
  __decorate([
@@ -1,3 +1,4 @@
1
+ export declare const AVATAR_MAX_BYTES: number;
1
2
  export declare class AvatarService {
2
3
  processAndStore(userId: string, file?: Express.Multer.File): Promise<string>;
3
4
  remove(userId: string): Promise<void>;
@@ -9,14 +9,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  return (mod && mod.__esModule) ? mod : { "default": mod };
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.AvatarService = void 0;
12
+ exports.AvatarService = exports.AVATAR_MAX_BYTES = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const fs_1 = require("fs");
15
15
  const path_1 = require("path");
16
16
  const sharp_1 = __importDefault(require("sharp"));
17
17
  const UPLOAD_DIR = (0, path_1.join)(process.cwd(), "uploads", "avatars");
18
18
  const ALLOWED = ["image/jpeg", "image/png", "image/webp"];
19
- const MAX_BYTES = 5 * 1024 * 1024;
19
+ exports.AVATAR_MAX_BYTES = 5 * 1024 * 1024;
20
20
  const MAGIC_BYTES_CHECK = {
21
21
  "image/jpeg": (buf) => buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff,
22
22
  "image/png": (buf) => buf.length >= 8 &&
@@ -38,7 +38,7 @@ let AvatarService = class AvatarService {
38
38
  if (!MAGIC_BYTES_CHECK[file.mimetype]?.(file.buffer)) {
39
39
  throw new common_1.BadRequestException("O conteúdo do arquivo não corresponde ao formato declarado");
40
40
  }
41
- if (file.size > MAX_BYTES) {
41
+ if (file.size > exports.AVATAR_MAX_BYTES) {
42
42
  throw new common_1.BadRequestException("Imagem excede o limite de 5MB");
43
43
  }
44
44
  await fs_1.promises.mkdir(UPLOAD_DIR, { recursive: true });
@@ -83,7 +83,7 @@ let UsersController = class UsersController {
83
83
  }
84
84
  async remove(id, current) {
85
85
  if (current.id === id) {
86
- return;
86
+ throw new common_1.BadRequestException("Você não pode excluir a própria conta");
87
87
  }
88
88
  await this.users.remove(id);
89
89
  }
@@ -120,7 +120,10 @@ __decorate([
120
120
  (0, common_1.Post)("me/avatar"),
121
121
  (0, swagger_1.ApiConsumes)("multipart/form-data"),
122
122
  (0, swagger_1.ApiOkResponse)({ type: user_response_1.UserResponse }),
123
- (0, common_1.UseInterceptors)((0, platform_express_1.FileInterceptor)("file", { storage: (0, multer_1.memoryStorage)() })),
123
+ (0, common_1.UseInterceptors)((0, platform_express_1.FileInterceptor)("file", {
124
+ storage: (0, multer_1.memoryStorage)(),
125
+ limits: { fileSize: avatar_service_1.AVATAR_MAX_BYTES, files: 1 },
126
+ })),
124
127
  __param(0, (0, currentUser_decorator_1.CurrentUser)("id")),
125
128
  __param(1, (0, common_1.UploadedFile)()),
126
129
  __metadata("design:type", Function),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rl-core-api",
3
- "version": "0.13.1",
3
+ "version": "0.14.1",
4
4
  "description": "Core NestJS: autenticação com 2FA, RBAC, auditoria, notificações e listagens com filtro dinâmico",
5
5
  "author": "Rodrigo Liberti",
6
6
  "license": "MIT",