@hed-hog/core 0.0.367 → 0.0.368

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 (39) hide show
  1. package/dist/file/file.service.d.ts +1 -0
  2. package/dist/file/file.service.d.ts.map +1 -1
  3. package/dist/file/file.service.js +1 -1
  4. package/dist/file/file.service.js.map +1 -1
  5. package/dist/notification/events/notification-stale-progress-check.event.d.ts +6 -0
  6. package/dist/notification/events/notification-stale-progress-check.event.d.ts.map +1 -0
  7. package/dist/notification/events/notification-stale-progress-check.event.js +11 -0
  8. package/dist/notification/events/notification-stale-progress-check.event.js.map +1 -0
  9. package/dist/notification/notification.service.d.ts +4 -1
  10. package/dist/notification/notification.service.d.ts.map +1 -1
  11. package/dist/notification/notification.service.js +9 -26
  12. package/dist/notification/notification.service.js.map +1 -1
  13. package/dist/task/task.service.d.ts +19 -0
  14. package/dist/task/task.service.d.ts.map +1 -1
  15. package/dist/task/task.service.js +46 -0
  16. package/dist/task/task.service.js.map +1 -1
  17. package/dist/user/user.controller.d.ts +23 -5
  18. package/dist/user/user.controller.d.ts.map +1 -1
  19. package/dist/user/user.controller.js +32 -15
  20. package/dist/user/user.controller.js.map +1 -1
  21. package/dist/user/user.service.d.ts +28 -8
  22. package/dist/user/user.service.d.ts.map +1 -1
  23. package/dist/user/user.service.js +120 -8
  24. package/dist/user/user.service.js.map +1 -1
  25. package/hedhog/data/route.yaml +9 -0
  26. package/hedhog/data/setting_group.yaml +14 -0
  27. package/hedhog/frontend/app/roles/page.tsx.ejs +4 -1
  28. package/hedhog/frontend/app/users/user-logs.tsx.ejs +190 -0
  29. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +15 -1
  30. package/hedhog/frontend/messages/en.json +13 -0
  31. package/hedhog/frontend/messages/pt.json +13 -0
  32. package/hedhog/table/user_change_log.yaml +40 -0
  33. package/package.json +5 -5
  34. package/src/file/file.service.ts +2 -1
  35. package/src/notification/events/notification-stale-progress-check.event.ts +5 -0
  36. package/src/notification/notification.service.ts +12 -25
  37. package/src/task/task.service.ts +52 -0
  38. package/src/user/user.controller.ts +23 -9
  39. package/src/user/user.service.ts +183 -8
@@ -1,6 +1,7 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { PaginationService } from '@hed-hog/api-pagination';
3
3
  import { PrismaService } from '@hed-hog/api-prisma';
4
+ import type { Prisma, user_change_log } from '@hed-hog/api-prisma';
4
5
  import {
5
6
  BadRequestException,
6
7
  Inject,
@@ -55,6 +56,18 @@ const USER_SORT_FIELDS = [
55
56
 
56
57
  const USER_SEARCH_FIELDS = ['name'] as const;
57
58
 
59
+ type UserChangeLog = user_change_log;
60
+ type UserChangeLogActor = Prisma.userGetPayload<{
61
+ select: {
62
+ id: true;
63
+ name: true;
64
+ user_identifier: {
65
+ select: { value: true };
66
+ take: 1;
67
+ };
68
+ };
69
+ }>;
70
+
58
71
  @Injectable()
59
72
  export class UserService {
60
73
  constructor(
@@ -213,7 +226,12 @@ export class UserService {
213
226
  return { user, challenge };
214
227
  }
215
228
 
216
- async update(locale: string, userId: number, { name }: UpdateDTO) {
229
+ async update(
230
+ locale: string,
231
+ userId: number,
232
+ { name }: UpdateDTO,
233
+ actorUserId?: number,
234
+ ) {
217
235
  const user = await this.findUserById(locale, userId);
218
236
 
219
237
  const result = await this.prismaService.user.update({
@@ -221,6 +239,17 @@ export class UserService {
221
239
  data: { name: name ?? user.name },
222
240
  });
223
241
 
242
+ if (result.name !== user.name) {
243
+ await this.recordUserChange({
244
+ targetUserId: userId,
245
+ actorUserId,
246
+ action: 'update',
247
+ summary: `name: "${user.name ?? ''}" → "${result.name ?? ''}"`,
248
+ beforeData: { name: user.name },
249
+ afterData: { name: result.name },
250
+ });
251
+ }
252
+
224
253
  await this.integrationApi.publishEvent({
225
254
  eventName: 'core.user.updated',
226
255
  sourceModule: 'core',
@@ -236,6 +265,7 @@ export class UserService {
236
265
  locale: string,
237
266
  userId: number,
238
267
  { password }: ResetPasswordDTO,
268
+ actorUserId?: number,
239
269
  ) {
240
270
  await this.validateUserExists(locale, userId);
241
271
 
@@ -266,6 +296,13 @@ export class UserService {
266
296
 
267
297
  await this.registerUserActivity(userId, 'resetPassword');
268
298
 
299
+ await this.recordUserChange({
300
+ targetUserId: userId,
301
+ actorUserId,
302
+ action: 'reset_password',
303
+ summary: 'Password reset and flagged for required change on next login',
304
+ });
305
+
269
306
  return {
270
307
  password: nextPassword,
271
308
  requiresReset: true,
@@ -305,7 +342,12 @@ export class UserService {
305
342
  // Avatar Management
306
343
  // ==========================================
307
344
 
308
- async changeAvatar(locale: string, userId: number, avatar: MulterFile) {
345
+ async changeAvatar(
346
+ locale: string,
347
+ userId: number,
348
+ avatar: MulterFile,
349
+ actorUserId?: number,
350
+ ) {
309
351
  const user = await this.validateUserExists(locale, userId);
310
352
 
311
353
  const newFile = await this.file.upload(USER_AVATAR_UPLOAD_DESTINATION, avatar);
@@ -317,6 +359,15 @@ export class UserService {
317
359
  data: { photo_id: newFile.id },
318
360
  });
319
361
 
362
+ await this.recordUserChange({
363
+ targetUserId: userId,
364
+ actorUserId,
365
+ action: 'avatar_changed',
366
+ summary: 'Avatar updated',
367
+ beforeData: { photo_id: user.photo_id },
368
+ afterData: { photo_id: newFile.id },
369
+ });
370
+
320
371
  return {
321
372
  id: newFile.id,
322
373
  path: newFile.path,
@@ -518,8 +569,13 @@ export class UserService {
518
569
  }));
519
570
  }
520
571
 
521
- async assignRoleToUser(locale: string, userId: number, roleId: number) {
522
- await Promise.all([
572
+ async assignRoleToUser(
573
+ locale: string,
574
+ userId: number,
575
+ roleId: number,
576
+ actorUserId?: number,
577
+ ) {
578
+ const [, role] = await Promise.all([
523
579
  this.validateUserExists(locale, userId),
524
580
  this.validateRoleExists(locale, roleId),
525
581
  ]);
@@ -530,16 +586,31 @@ export class UserService {
530
586
  return existingRoleUser;
531
587
  }
532
588
 
533
- return this.prismaService.role_user.create({
589
+ const result = await this.prismaService.role_user.create({
534
590
  data: {
535
591
  user_id: userId,
536
592
  role_id: roleId,
537
593
  },
538
594
  });
595
+
596
+ await this.recordUserChange({
597
+ targetUserId: userId,
598
+ actorUserId,
599
+ action: 'role_assigned',
600
+ summary: `Role assigned: ${role.slug}`,
601
+ afterData: { role_id: roleId, slug: role.slug },
602
+ });
603
+
604
+ return result;
539
605
  }
540
606
 
541
- async removeRoleFromUser(locale: string, userId: number, roleId: number) {
542
- await Promise.all([
607
+ async removeRoleFromUser(
608
+ locale: string,
609
+ userId: number,
610
+ roleId: number,
611
+ actorUserId?: number,
612
+ ) {
613
+ const [, role] = await Promise.all([
543
614
  this.validateUserExists(locale, userId),
544
615
  this.validateRoleExists(locale, roleId),
545
616
  ]);
@@ -554,6 +625,14 @@ export class UserService {
554
625
  where: { id: roleUser.id },
555
626
  });
556
627
 
628
+ await this.recordUserChange({
629
+ targetUserId: userId,
630
+ actorUserId,
631
+ action: 'role_removed',
632
+ summary: `Role removed: ${role.slug}`,
633
+ beforeData: { role_id: roleId, slug: role.slug },
634
+ });
635
+
557
636
  return { count: 1 };
558
637
  }
559
638
 
@@ -574,6 +653,102 @@ export class UserService {
574
653
  });
575
654
  }
576
655
 
656
+ // ==========================================
657
+ // Change Log (administrative audit trail)
658
+ // ==========================================
659
+
660
+ private async recordUserChange(data: {
661
+ targetUserId: number;
662
+ actorUserId?: number | null;
663
+ action: string;
664
+ summary?: string;
665
+ beforeData?: unknown;
666
+ afterData?: unknown;
667
+ }) {
668
+ return this.prismaService.user_change_log
669
+ .create({
670
+ data: {
671
+ target_user_id: data.targetUserId,
672
+ actor_user_id: data.actorUserId ?? null,
673
+ action: data.action,
674
+ summary: data.summary ?? null,
675
+ before_data:
676
+ data.beforeData === undefined
677
+ ? null
678
+ : JSON.stringify(data.beforeData),
679
+ after_data:
680
+ data.afterData === undefined
681
+ ? null
682
+ : JSON.stringify(data.afterData),
683
+ },
684
+ })
685
+ .catch(() => null);
686
+ }
687
+
688
+ async listUserChangeLogs(locale: string, userId: number, paginationParams) {
689
+ await this.validateUserExists(locale, userId);
690
+
691
+ const paginated = await this.paginationService.paginatePrismaModel(
692
+ this.prismaService.user_change_log,
693
+ {
694
+ page: paginationParams?.page,
695
+ pageSize: paginationParams?.pageSize,
696
+ sortField: paginationParams?.sortField || 'created_at',
697
+ sortOrder: paginationParams?.sortOrder || 'desc',
698
+ validSortFields: ['id', 'created_at', 'action'],
699
+ where: { target_user_id: userId },
700
+ },
701
+ );
702
+
703
+ const logs = (paginated.data || []) as UserChangeLog[];
704
+ const actorIds = [
705
+ ...new Set(
706
+ logs
707
+ .map((log) => log.actor_user_id)
708
+ .filter((id): id is number => Boolean(id)),
709
+ ),
710
+ ];
711
+
712
+ const actors: UserChangeLogActor[] = actorIds.length
713
+ ? await this.prismaService.user.findMany({
714
+ where: { id: { in: actorIds } },
715
+ select: {
716
+ id: true,
717
+ name: true,
718
+ user_identifier: {
719
+ where: { type: IDENTIFIER_TYPE.EMAIL, enabled: true },
720
+ select: { value: true },
721
+ take: 1,
722
+ },
723
+ },
724
+ })
725
+ : [];
726
+
727
+ const actorMap = new Map(actors.map((actor) => [actor.id, actor]));
728
+
729
+ return {
730
+ ...paginated,
731
+ data: logs.map((log) => {
732
+ const actor = log.actor_user_id
733
+ ? actorMap.get(log.actor_user_id)
734
+ : null;
735
+
736
+ return {
737
+ id: String(log.id),
738
+ actorUserId: log.actor_user_id ? String(log.actor_user_id) : null,
739
+ actorName: actor?.name ?? null,
740
+ actorEmail: actor?.user_identifier?.[0]?.value ?? null,
741
+ action: log.action,
742
+ summary: log.summary || '',
743
+ beforeData: log.before_data || '',
744
+ afterData: log.after_data || '',
745
+ ipAddress: log.ip_address || '',
746
+ createdAt: log.created_at?.toISOString?.() || null,
747
+ };
748
+ }),
749
+ };
750
+ }
751
+
577
752
  // ==========================================
578
753
  // Private Helper Methods
579
754
  // ==========================================
@@ -761,4 +936,4 @@ export class UserService {
761
936
  },
762
937
  });
763
938
  }
764
- }
939
+ }