@hed-hog/core 0.0.370 → 0.0.371

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 (248) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +2 -0
  3. package/dist/ai/ai.service.d.ts.map +1 -1
  4. package/dist/ai/ai.service.js +10 -7
  5. package/dist/ai/ai.service.js.map +1 -1
  6. package/dist/app-command/app-command.module.d.ts +3 -0
  7. package/dist/app-command/app-command.module.d.ts.map +1 -0
  8. package/dist/app-command/app-command.module.js +22 -0
  9. package/dist/app-command/app-command.module.js.map +1 -0
  10. package/dist/app-command/app-command.registry.d.ts +22 -0
  11. package/dist/app-command/app-command.registry.d.ts.map +1 -0
  12. package/dist/{webhook-integration/webhook-command.registry.js → app-command/app-command.registry.js} +6 -6
  13. package/dist/app-command/app-command.registry.js.map +1 -0
  14. package/dist/auth/auth.controller.d.ts +2 -2
  15. package/dist/auth/auth.controller.d.ts.map +1 -1
  16. package/dist/auth/auth.controller.js +20 -7
  17. package/dist/auth/auth.controller.js.map +1 -1
  18. package/dist/auth/auth.service.d.ts +3 -3
  19. package/dist/auth/auth.service.d.ts.map +1 -1
  20. package/dist/auth/auth.service.js +10 -36
  21. package/dist/auth/auth.service.js.map +1 -1
  22. package/dist/auth/dto/forget.dto.d.ts +1 -0
  23. package/dist/auth/dto/forget.dto.d.ts.map +1 -1
  24. package/dist/auth/dto/forget.dto.js +5 -0
  25. package/dist/auth/dto/forget.dto.js.map +1 -1
  26. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  27. package/dist/auth/guards/auth.guard.js +4 -1
  28. package/dist/auth/guards/auth.guard.js.map +1 -1
  29. package/dist/challenge/challenge.service.d.ts +8 -7
  30. package/dist/challenge/challenge.service.d.ts.map +1 -1
  31. package/dist/challenge/challenge.service.js +27 -19
  32. package/dist/challenge/challenge.service.js.map +1 -1
  33. package/dist/file/dto/upload.dto.d.ts +3 -0
  34. package/dist/file/dto/upload.dto.d.ts.map +1 -1
  35. package/dist/file/dto/upload.dto.js +22 -0
  36. package/dist/file/dto/upload.dto.js.map +1 -1
  37. package/dist/file/file.controller.d.ts +2 -1
  38. package/dist/file/file.controller.d.ts.map +1 -1
  39. package/dist/file/file.controller.js +29 -16
  40. package/dist/file/file.controller.js.map +1 -1
  41. package/dist/file/file.service.d.ts +51 -1
  42. package/dist/file/file.service.d.ts.map +1 -1
  43. package/dist/file/file.service.js +250 -18
  44. package/dist/file/file.service.js.map +1 -1
  45. package/dist/file/image-optimization.service.d.ts +17 -4
  46. package/dist/file/image-optimization.service.d.ts.map +1 -1
  47. package/dist/file/image-optimization.service.js +101 -46
  48. package/dist/file/image-optimization.service.js.map +1 -1
  49. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  50. package/dist/file/provider/s3.provider.js +15 -7
  51. package/dist/file/provider/s3.provider.js.map +1 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +18 -8
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +4 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration/services/event-subscriber.registry.d.ts +5 -0
  60. package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -1
  61. package/dist/integration/services/event-subscriber.registry.js +15 -0
  62. package/dist/integration/services/event-subscriber.registry.js.map +1 -1
  63. package/dist/integration/services/outbox.processor.d.ts.map +1 -1
  64. package/dist/integration/services/outbox.processor.js +2 -4
  65. package/dist/integration/services/outbox.processor.js.map +1 -1
  66. package/dist/integration/services/outbox.service.d.ts +4 -2
  67. package/dist/integration/services/outbox.service.d.ts.map +1 -1
  68. package/dist/integration/services/outbox.service.js +26 -15
  69. package/dist/integration/services/outbox.service.js.map +1 -1
  70. package/dist/integration-profile/integration-credential-crypto.service.d.ts +43 -0
  71. package/dist/integration-profile/integration-credential-crypto.service.d.ts.map +1 -0
  72. package/dist/integration-profile/integration-credential-crypto.service.js +142 -0
  73. package/dist/integration-profile/integration-credential-crypto.service.js.map +1 -0
  74. package/dist/integration-profile/integration-profile.controller.d.ts +6 -7
  75. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  76. package/dist/integration-profile/integration-profile.controller.js +4 -3
  77. package/dist/integration-profile/integration-profile.controller.js.map +1 -1
  78. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
  79. package/dist/integration-profile/integration-profile.module.js +5 -2
  80. package/dist/integration-profile/integration-profile.module.js.map +1 -1
  81. package/dist/integration-profile/integration-profile.secrets.d.ts +16 -0
  82. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -0
  83. package/dist/integration-profile/integration-profile.secrets.js +56 -0
  84. package/dist/integration-profile/integration-profile.secrets.js.map +1 -0
  85. package/dist/integration-profile/integration-profile.service.d.ts +125 -8
  86. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  87. package/dist/integration-profile/integration-profile.service.js +257 -68
  88. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  89. package/dist/integration-profile/integration-profile.utils.d.ts +17 -0
  90. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  91. package/dist/integration-profile/integration-profile.utils.js +31 -0
  92. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  93. package/dist/language/en.json +178 -0
  94. package/dist/language/pt.json +178 -0
  95. package/dist/mail/mail.controller.d.ts.map +1 -1
  96. package/dist/mail/mail.controller.js +5 -28
  97. package/dist/mail/mail.controller.js.map +1 -1
  98. package/dist/mail/mail.module.js +18 -18
  99. package/dist/mail/mail.module.js.map +1 -1
  100. package/dist/mail/mail.service.d.ts +13 -2
  101. package/dist/mail/mail.service.d.ts.map +1 -1
  102. package/dist/mail/mail.service.js +25 -15
  103. package/dist/mail/mail.service.js.map +1 -1
  104. package/dist/mcp-chat/mcp-chat.service.js +6 -5
  105. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  106. package/dist/oauth/providers/facebook.provider.js +5 -5
  107. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  108. package/dist/oauth/providers/github.provider.js +5 -5
  109. package/dist/oauth/providers/github.provider.js.map +1 -1
  110. package/dist/oauth/providers/google.provider.js +5 -5
  111. package/dist/oauth/providers/google.provider.js.map +1 -1
  112. package/dist/oauth/providers/microsoft-entra-id.provider.js +5 -5
  113. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  114. package/dist/oauth/providers/microsoft.provider.js +5 -5
  115. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  116. package/dist/pdf/pdf.service.d.ts +4 -0
  117. package/dist/pdf/pdf.service.d.ts.map +1 -1
  118. package/dist/pdf/pdf.service.js +16 -9
  119. package/dist/pdf/pdf.service.js.map +1 -1
  120. package/dist/profile/profile.controller.d.ts +7 -7
  121. package/dist/profile/profile.controller.d.ts.map +1 -1
  122. package/dist/profile/profile.controller.js +28 -21
  123. package/dist/profile/profile.controller.js.map +1 -1
  124. package/dist/profile/profile.service.d.ts +7 -7
  125. package/dist/profile/profile.service.d.ts.map +1 -1
  126. package/dist/profile/profile.service.js +21 -25
  127. package/dist/profile/profile.service.js.map +1 -1
  128. package/dist/setting/setting.service.d.ts +1 -0
  129. package/dist/setting/setting.service.d.ts.map +1 -1
  130. package/dist/setting/setting.service.js +15 -2
  131. package/dist/setting/setting.service.js.map +1 -1
  132. package/dist/token/token.service.d.ts.map +1 -1
  133. package/dist/token/token.service.js +2 -1
  134. package/dist/token/token.service.js.map +1 -1
  135. package/dist/user/user.controller.d.ts +9 -0
  136. package/dist/user/user.controller.d.ts.map +1 -1
  137. package/dist/user/user.controller.js +13 -0
  138. package/dist/user/user.controller.js.map +1 -1
  139. package/dist/user/user.service.d.ts +7 -0
  140. package/dist/user/user.service.d.ts.map +1 -1
  141. package/dist/user/user.service.js +19 -0
  142. package/dist/user/user.service.js.map +1 -1
  143. package/dist/utils/webauthn-origin.d.ts +11 -0
  144. package/dist/utils/webauthn-origin.d.ts.map +1 -0
  145. package/dist/utils/webauthn-origin.js +37 -0
  146. package/dist/utils/webauthn-origin.js.map +1 -0
  147. package/dist/webhook-integration/webhook-integration.controller.d.ts +13 -0
  148. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  149. package/dist/webhook-integration/webhook-integration.controller.js +29 -0
  150. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  151. package/dist/webhook-integration/webhook-integration.module.js +3 -3
  152. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  153. package/dist/webhook-integration/webhook-integration.service.d.ts +18 -2
  154. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  155. package/dist/webhook-integration/webhook-integration.service.js +110 -6
  156. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  157. package/hedhog/data/integration_provider.yaml +9 -0
  158. package/hedhog/data/integration_type.yaml +6 -0
  159. package/hedhog/data/menu.yaml +6 -0
  160. package/hedhog/data/role.yaml +7 -0
  161. package/hedhog/data/route.yaml +36 -0
  162. package/hedhog/data/setting_group.yaml +119 -13
  163. package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
  164. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
  165. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
  166. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
  167. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
  168. package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
  169. package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
  170. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
  171. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
  172. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
  173. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
  174. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
  175. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
  176. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
  177. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +141 -29
  178. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
  179. package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
  180. package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
  181. package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
  182. package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
  183. package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
  184. package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
  185. package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
  186. package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
  187. package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
  188. package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
  189. package/hedhog/frontend/messages/en.json +69 -4
  190. package/hedhog/frontend/messages/pt.json +69 -4
  191. package/hedhog/table/setting.yaml +2 -0
  192. package/hedhog/table/webhook_call_log.yaml +49 -46
  193. package/package.json +8 -3
  194. package/src/ai/ai.service.ts +9 -6
  195. package/src/app-command/app-command.module.ts +9 -0
  196. package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
  197. package/src/auth/auth.controller.ts +20 -4
  198. package/src/auth/auth.service.ts +10 -36
  199. package/src/auth/dto/forget.dto.ts +5 -1
  200. package/src/auth/guards/auth.guard.ts +4 -1
  201. package/src/challenge/challenge.service.ts +30 -19
  202. package/src/file/dto/upload.dto.ts +25 -1
  203. package/src/file/file.controller.ts +31 -18
  204. package/src/file/file.service.ts +341 -16
  205. package/src/file/image-optimization.service.ts +111 -46
  206. package/src/file/provider/s3.provider.ts +16 -7
  207. package/src/file/provider/spaces.provider.ts +15 -5
  208. package/src/index.ts +4 -1
  209. package/src/integration/services/event-subscriber.registry.ts +15 -0
  210. package/src/integration/services/outbox.processor.ts +2 -5
  211. package/src/integration/services/outbox.service.ts +35 -17
  212. package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
  213. package/src/integration-profile/integration-profile.controller.ts +2 -0
  214. package/src/integration-profile/integration-profile.module.ts +5 -2
  215. package/src/integration-profile/integration-profile.secrets.ts +51 -0
  216. package/src/integration-profile/integration-profile.service.ts +335 -54
  217. package/src/integration-profile/integration-profile.utils.ts +56 -0
  218. package/src/language/en.json +5 -1
  219. package/src/language/pt.json +5 -1
  220. package/src/mail/mail.controller.ts +5 -29
  221. package/src/mail/mail.module.ts +5 -5
  222. package/src/mail/mail.service.ts +37 -13
  223. package/src/mcp-chat/mcp-chat.service.ts +5 -5
  224. package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
  225. package/src/oauth/providers/facebook.provider.ts +3 -3
  226. package/src/oauth/providers/github.provider.ts +3 -3
  227. package/src/oauth/providers/google.provider.ts +3 -3
  228. package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
  229. package/src/oauth/providers/microsoft.provider.ts +3 -3
  230. package/src/pdf/pdf.service.ts +19 -6
  231. package/src/profile/profile.controller.ts +21 -14
  232. package/src/profile/profile.service.ts +23 -20
  233. package/src/security/security.service.spec.ts +222 -0
  234. package/src/setting/setting.service.ts +15 -2
  235. package/src/token/token.service.ts +2 -1
  236. package/src/user/user.controller.ts +26 -16
  237. package/src/user/user.service.ts +25 -0
  238. package/src/utils/webauthn-origin.ts +38 -0
  239. package/src/webhook-integration/webhook-integration.controller.ts +23 -0
  240. package/src/webhook-integration/webhook-integration.module.ts +3 -3
  241. package/src/webhook-integration/webhook-integration.service.ts +115 -5
  242. package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
  243. package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
  244. package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
  245. package/src/menu/menu.service.spec.ts +0 -247
  246. package/src/route/route.service.spec.ts +0 -300
  247. package/src/screen/screen.service.spec.ts +0 -298
  248. package/src/user/user.service.spec.ts +0 -294
@@ -22,6 +22,7 @@ import { SecurityService } from '../security/security.service';
22
22
  import { SettingService } from '../setting/setting.service';
23
23
  import { TokenService } from '../token/token.service';
24
24
  import { UserService } from '../user/user.service';
25
+ import { resolveWebAuthnOrigin } from '../utils/webauthn-origin';
25
26
  import { ChangeEmailDto } from './dto/change-email.dto';
26
27
  import { ChangePasswordDto } from './dto/change-password.dto';
27
28
  import { EmailVerificationDto } from './dto/email-verification-confirm.dto';
@@ -294,6 +295,8 @@ export class ProfileService {
294
295
  this.eventEmitter.emit(PROFILE_EMAIL_CHANGED, new ProfileEmailChangedEvent(userId, confirmedIdentifier.value));
295
296
  }
296
297
 
298
+ await this.user.registerUserActivity(userId, 'changeEmail');
299
+
297
300
  return { success: true };
298
301
  }
299
302
 
@@ -349,6 +352,7 @@ export class ProfileService {
349
352
  });
350
353
  if (name) {
351
354
  this.eventEmitter.emit(PROFILE_UPDATED, new ProfileUpdatedEvent(userid, name));
355
+ await this.user.registerUserActivity(userid, 'updateProfile');
352
356
  }
353
357
  return result;
354
358
  }
@@ -356,6 +360,7 @@ export class ProfileService {
356
360
  async updateAvatar(locale: string, userId: number, file: MulterFile) {
357
361
  const result = await this.user.changeAvatar(locale, userId, file);
358
362
  this.eventEmitter.emit(PROFILE_AVATAR_UPDATED, new ProfileAvatarUpdatedEvent(userId, result?.id ?? null));
363
+ await this.user.registerUserActivity(userId, 'avatar_changed');
359
364
  return result;
360
365
  }
361
366
 
@@ -405,6 +410,8 @@ export class ProfileService {
405
410
  });
406
411
  }
407
412
 
413
+ await this.user.registerUserActivity(userId, 'changePassword');
414
+
408
415
  return { success: true };
409
416
  }
410
417
 
@@ -656,7 +663,7 @@ export class ProfileService {
656
663
  }
657
664
  }
658
665
 
659
- async removeMfa(locale: string, userId: number, mfaId: number, data: { token?: string; hash?: string; verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn'; assertionResponse?: any }) {
666
+ async removeMfa(locale: string, userId: number, mfaId: number, data: { token?: string; hash?: string; verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn'; assertionResponse?: any }, requestOrigin?: string) {
660
667
  try {
661
668
  const user = await this.prisma.user.findUnique({
662
669
  where: { id: userId },
@@ -700,7 +707,7 @@ export class ProfileService {
700
707
  }
701
708
 
702
709
  try {
703
- await this.verifyWebAuthnAuthentication(userId, data.assertionResponse);
710
+ await this.verifyWebAuthnAuthentication(userId, data.assertionResponse, requestOrigin);
704
711
  } catch (error) {
705
712
  throw new BadRequestException(
706
713
  getLocaleText('invalidWebAuthn', locale, 'WebAuthn verification failed.')
@@ -899,7 +906,8 @@ export class ProfileService {
899
906
  async verifyBeforeAddMfa(
900
907
  locale: string,
901
908
  userId: number,
902
- data: { verificationCode?: string; hash?: string; verificationType: 'totp' | 'email' | 'recovery' | 'webauthn'; assertionResponse?: any }
909
+ data: { verificationCode?: string; hash?: string; verificationType: 'totp' | 'email' | 'recovery' | 'webauthn'; assertionResponse?: any },
910
+ requestOrigin?: string
903
911
  ) {
904
912
  try {
905
913
  const user = await this.prisma.user.findUnique({
@@ -935,7 +943,7 @@ export class ProfileService {
935
943
  }
936
944
 
937
945
  try {
938
- await this.verifyWebAuthnAuthentication(userId, data.assertionResponse);
946
+ await this.verifyWebAuthnAuthentication(userId, data.assertionResponse, requestOrigin);
939
947
  } catch (error) {
940
948
  throw new BadRequestException(
941
949
  getLocaleText('invalidWebAuthn', locale, 'WebAuthn verification failed.')
@@ -1087,7 +1095,7 @@ export class ProfileService {
1087
1095
  }
1088
1096
  }
1089
1097
 
1090
- async regenerateRecoveryCodes(locale: string, userId: number, data: { verificationCode?: string; hash?: string; verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn'; assertionResponse?: any }) {
1098
+ async regenerateRecoveryCodes(locale: string, userId: number, data: { verificationCode?: string; hash?: string; verificationType?: 'totp' | 'email' | 'recovery' | 'webauthn'; assertionResponse?: any }, requestOrigin?: string) {
1091
1099
  try {
1092
1100
  const user = await this.prisma.user.findUnique({
1093
1101
  where: { id: userId },
@@ -1125,7 +1133,7 @@ export class ProfileService {
1125
1133
  }
1126
1134
 
1127
1135
  try {
1128
- await this.verifyWebAuthnAuthentication(userId, data.assertionResponse);
1136
+ await this.verifyWebAuthnAuthentication(userId, data.assertionResponse, requestOrigin);
1129
1137
  } catch (error) {
1130
1138
  throw new BadRequestException(
1131
1139
  getLocaleText('invalidWebAuthn', locale, 'WebAuthn verification failed.')
@@ -1213,7 +1221,7 @@ export class ProfileService {
1213
1221
  }
1214
1222
  }
1215
1223
 
1216
- async generateWebAuthnRegistrationOptions(locale: string, userId: number, name: string) {
1224
+ async generateWebAuthnRegistrationOptions(locale: string, userId: number, name: string, requestOrigin?: string) {
1217
1225
  try {
1218
1226
  const user = await this.prisma.user.findFirst({
1219
1227
  where: { id: userId },
@@ -1244,8 +1252,7 @@ export class ProfileService {
1244
1252
  ]);
1245
1253
 
1246
1254
  const rpName = settings['system-name'] || 'HedHog';
1247
- const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
1248
- const rpID = new URL(primaryOrigin).hostname;
1255
+ const { rpID } = resolveWebAuthnOrigin(requestOrigin);
1249
1256
  const userEmail = user.user_identifier.find(ui => ui.type === 'email')?.value || `user${userId}@example.com`;
1250
1257
  const existingAuthenticators = user.user_mfa
1251
1258
  .filter(mfa => mfa.user_mfa_webauthn && mfa.user_mfa_webauthn.length > 0)
@@ -1295,7 +1302,8 @@ export class ProfileService {
1295
1302
  locale: string,
1296
1303
  userId: number,
1297
1304
  name: string,
1298
- attestationResponse: any
1305
+ attestationResponse: any,
1306
+ requestOrigin?: string
1299
1307
  ) {
1300
1308
  try {
1301
1309
  const challengeRecord = await this.prisma.user_mfa_challenge.findFirst({
@@ -1317,9 +1325,7 @@ export class ProfileService {
1317
1325
  );
1318
1326
  }
1319
1327
 
1320
- const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
1321
- const rpID = new URL(primaryOrigin).hostname;
1322
- const expectedOrigin = primaryOrigin;
1328
+ const { rpID, expectedOrigin } = resolveWebAuthnOrigin(requestOrigin);
1323
1329
  const verification: VerifiedRegistrationResponse = await verifyRegistrationResponse({
1324
1330
  response: attestationResponse,
1325
1331
  expectedChallenge: challengeRecord.hash,
@@ -1361,7 +1367,7 @@ export class ProfileService {
1361
1367
  }
1362
1368
  }
1363
1369
 
1364
- async generateWebAuthnAuthenticationOptions(locale: string, userId: number) {
1370
+ async generateWebAuthnAuthenticationOptions(locale: string, userId: number, requestOrigin?: string) {
1365
1371
  try {
1366
1372
  const user = await this.prisma.user.findFirst({
1367
1373
  where: { id: userId },
@@ -1379,8 +1385,7 @@ export class ProfileService {
1379
1385
  );
1380
1386
  }
1381
1387
 
1382
- const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
1383
- const rpID = new URL(primaryOrigin).hostname;
1388
+ const { rpID } = resolveWebAuthnOrigin(requestOrigin);
1384
1389
  const allowCredentials = user.user_mfa
1385
1390
  .filter(mfa => mfa.user_mfa_webauthn && mfa.user_mfa_webauthn.length > 0)
1386
1391
  .map(mfa => ({
@@ -1408,7 +1413,7 @@ export class ProfileService {
1408
1413
  }
1409
1414
  }
1410
1415
 
1411
- async verifyWebAuthnAuthentication(userId: number, assertionResponse: any) {
1416
+ async verifyWebAuthnAuthentication(userId: number, assertionResponse: any, requestOrigin?: string) {
1412
1417
  try {
1413
1418
  const challengeRecord = await this.prisma.user_mfa_challenge.findFirst({
1414
1419
  where: {
@@ -1433,9 +1438,7 @@ export class ProfileService {
1433
1438
  }
1434
1439
 
1435
1440
  const authenticator = challengeRecord.user_mfa.user_mfa_webauthn[0];
1436
- const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
1437
- const rpID = new URL(primaryOrigin).hostname;
1438
- const expectedOrigin = primaryOrigin;
1441
+ const { rpID, expectedOrigin } = resolveWebAuthnOrigin(requestOrigin);
1439
1442
  const verification: VerifiedAuthenticationResponse = await verifyAuthenticationResponse({
1440
1443
  response: assertionResponse,
1441
1444
  expectedChallenge: challengeRecord.hash,
@@ -0,0 +1,222 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
3
+
4
+ jest.mock('@hed-hog/api-locale', () => ({
5
+ getLocaleText: (_key: string, _locale: string, fallback: string) => fallback,
6
+ }));
7
+
8
+ jest.mock('argon2', () => ({
9
+ __esModule: true,
10
+ hash: jest.fn(),
11
+ verify: jest.fn(),
12
+ }));
13
+
14
+ import * as argon2 from 'argon2';
15
+ import { SecurityService } from './security.service';
16
+
17
+ const makeService = (env: Record<string, string | undefined> = {}) => {
18
+ const configService = {
19
+ get: jest.fn((key: string) => env[key]),
20
+ };
21
+ const service = new SecurityService(configService as any);
22
+ return { service, configService };
23
+ };
24
+
25
+ describe('SecurityService', () => {
26
+ afterEach(() => {
27
+ jest.clearAllMocks();
28
+ });
29
+
30
+ describe('getValueWithPepper', () => {
31
+ it('uses the explicit pepper when provided', () => {
32
+ const { service, configService } = makeService();
33
+ expect(service.getValueWithPepper('value', 'PEP')).toBe('valuePEP');
34
+ expect(configService.get).not.toHaveBeenCalled();
35
+ });
36
+
37
+ it('falls back to the configured PEPPER', () => {
38
+ const { service } = makeService({ PEPPER: 'secret-pepper' });
39
+ expect(service.getValueWithPepper('value')).toBe('valuesecret-pepper');
40
+ });
41
+ });
42
+
43
+ describe('getJwtSecret', () => {
44
+ it('reads JWT_SECRET from config', () => {
45
+ const { service } = makeService({ JWT_SECRET: 'jwt-secret' });
46
+ expect(service.getJwtSecret()).toBe('jwt-secret');
47
+ });
48
+ });
49
+
50
+ describe('hashSha256', () => {
51
+ it('is deterministic and emits url-safe base64 without padding', () => {
52
+ const { service } = makeService();
53
+ const a = service.hashSha256('hello');
54
+ const b = service.hashSha256('hello');
55
+ const c = service.hashSha256('world');
56
+ expect(a).toBe(b);
57
+ expect(a).not.toBe(c);
58
+ expect(a).toMatch(/^[A-Za-z0-9_-]+$/);
59
+ });
60
+ });
61
+
62
+ describe('hashWithPepper', () => {
63
+ it('peppers the value before hashing', () => {
64
+ const { service } = makeService({ PEPPER: 'p' });
65
+ const expected = service.hashSha256('valuep');
66
+ expect(service.hashWithPepper('value')).toBe(expected);
67
+ });
68
+ });
69
+
70
+ describe('signHmacSha256 / verifyHmacSha256', () => {
71
+ it('verifies a signature produced with the default JWT secret', () => {
72
+ const { service } = makeService({ JWT_SECRET: 'jwt-secret' });
73
+ const sig = service.signHmacSha256('payload');
74
+ expect(service.verifyHmacSha256('payload', sig)).toBe(true);
75
+ });
76
+
77
+ it('rejects a tampered signature', () => {
78
+ const { service } = makeService({ JWT_SECRET: 'jwt-secret' });
79
+ const sig = service.signHmacSha256('payload');
80
+ expect(service.verifyHmacSha256('other-payload', sig)).toBe(false);
81
+ });
82
+
83
+ it('rejects an empty signature without throwing', () => {
84
+ const { service } = makeService({ JWT_SECRET: 'jwt-secret' });
85
+ expect(service.verifyHmacSha256('payload', '')).toBe(false);
86
+ });
87
+
88
+ it('honors an explicit signing secret', () => {
89
+ const { service } = makeService({ JWT_SECRET: 'jwt-secret' });
90
+ const sig = service.signHmacSha256('payload', 'other-secret');
91
+ expect(service.verifyHmacSha256('payload', sig, 'other-secret')).toBe(true);
92
+ // wrong secret -> mismatch
93
+ expect(service.verifyHmacSha256('payload', sig)).toBe(false);
94
+ });
95
+
96
+ it('returns false on a length mismatch instead of throwing', () => {
97
+ const { service } = makeService({ JWT_SECRET: 'jwt-secret' });
98
+ expect(service.verifyHmacSha256('payload', 'short')).toBe(false);
99
+ });
100
+ });
101
+
102
+ describe('encrypt / decrypt', () => {
103
+ it('round-trips using an explicit secret', () => {
104
+ const { service } = makeService();
105
+ const cipher = service.encrypt('super secret', 'the-key');
106
+ expect(cipher.split(':')).toHaveLength(4);
107
+ expect(service.decrypt(cipher, 'the-key')).toBe('super secret');
108
+ });
109
+
110
+ it('round-trips using the configured ENCRYPTION_SECRET', () => {
111
+ const { service } = makeService({ ENCRYPTION_SECRET: 'env-key' });
112
+ const cipher = service.encrypt('payload');
113
+ expect(service.decrypt(cipher)).toBe('payload');
114
+ });
115
+
116
+ it('throws when no encryption secret is configured', () => {
117
+ const { service } = makeService();
118
+ expect(() => service.encrypt('payload')).toThrow(
119
+ 'ENCRYPTION_SECRET is not configured.',
120
+ );
121
+ });
122
+
123
+ it('throws on a malformed ciphertext', () => {
124
+ const { service } = makeService();
125
+ expect(() => service.decrypt('not-valid', 'the-key')).toThrow(
126
+ /Failed to decrypt value/,
127
+ );
128
+ });
129
+
130
+ it('throws when decrypting with the wrong secret', () => {
131
+ const { service } = makeService();
132
+ const cipher = service.encrypt('payload', 'right-key');
133
+ expect(() => service.decrypt(cipher, 'wrong-key')).toThrow(
134
+ /Failed to decrypt value/,
135
+ );
136
+ });
137
+ });
138
+
139
+ describe('validatePassword', () => {
140
+ it('returns true when one of the credentials verifies', async () => {
141
+ const { service } = makeService({ PEPPER: 'p' });
142
+ (argon2.verify as jest.Mock)
143
+ .mockResolvedValueOnce(false as never)
144
+ .mockResolvedValueOnce(true as never);
145
+
146
+ const result = await service.validatePassword(
147
+ 'en',
148
+ [{ hash: 'h1' }, { hash: 'h2' }],
149
+ 'pw',
150
+ );
151
+
152
+ expect(result).toBe(true);
153
+ expect(argon2.verify).toHaveBeenCalledWith('h1', 'pwp');
154
+ });
155
+
156
+ it('returns false when no credential verifies', async () => {
157
+ const { service } = makeService();
158
+ (argon2.verify as jest.Mock).mockResolvedValue(false as never);
159
+
160
+ const result = await service.validatePassword('en', [{ hash: 'h1' }], 'pw');
161
+
162
+ expect(result).toBe(false);
163
+ });
164
+
165
+ it('throws BadRequestException when verification errors', async () => {
166
+ const { service } = makeService();
167
+ (argon2.verify as jest.Mock).mockRejectedValue(new Error('boom') as never);
168
+
169
+ await expect(
170
+ service.validatePassword('en', [{ hash: 'h1' }], 'pw'),
171
+ ).rejects.toThrow(BadRequestException);
172
+ });
173
+ });
174
+
175
+ describe('verifyArgon2', () => {
176
+ it('returns true when argon2 verifies', async () => {
177
+ const { service } = makeService();
178
+ (argon2.verify as jest.Mock).mockResolvedValue(true as never);
179
+ await expect(service.verifyArgon2('v', 'hash')).resolves.toBe(true);
180
+ });
181
+
182
+ it('returns false when argon2 throws', async () => {
183
+ const { service } = makeService();
184
+ (argon2.verify as jest.Mock).mockRejectedValue(new Error('bad') as never);
185
+ await expect(service.verifyArgon2('v', 'hash')).resolves.toBe(false);
186
+ });
187
+ });
188
+
189
+ describe('hashArgon2', () => {
190
+ it('hashes the peppered value', async () => {
191
+ const { service } = makeService({ PEPPER: 'p' });
192
+ (argon2.hash as jest.Mock).mockResolvedValue('hashed' as never);
193
+ const result = await service.hashArgon2('value');
194
+ expect(result).toBe('hashed');
195
+ expect(argon2.hash).toHaveBeenCalledWith('valuep');
196
+ });
197
+ });
198
+
199
+ describe('generateCode', () => {
200
+ it('produces a numeric code of the requested length', () => {
201
+ const { service } = makeService();
202
+ const code = service.generateCode(8);
203
+ expect(code).toHaveLength(8);
204
+ expect(code).toMatch(/^[0-9]+$/);
205
+ });
206
+
207
+ it('defaults to length 6 when given a falsy length', () => {
208
+ const { service } = makeService();
209
+ expect(service.generateCode(0)).toHaveLength(6);
210
+ });
211
+ });
212
+
213
+ describe('randomOpaque', () => {
214
+ it('produces distinct url-safe tokens', () => {
215
+ const { service } = makeService();
216
+ const a = service.randomOpaque();
217
+ const b = service.randomOpaque();
218
+ expect(a).not.toBe(b);
219
+ expect(a).toMatch(/^[A-Za-z0-9_-]+$/);
220
+ });
221
+ });
222
+ });
@@ -116,10 +116,22 @@ export class SettingService implements OnModuleInit, OnModuleDestroy {
116
116
  this.lastRedisPublisherErrorLogAt = now;
117
117
  }
118
118
 
119
- const message = error instanceof Error ? error.message : String(error);
119
+ const message = this.extractRedisErrorMessage(error);
120
120
  this.logger.warn(`Redis settings ${kind} error: ${message}`);
121
121
  }
122
122
 
123
+ private extractRedisErrorMessage(error: unknown): string {
124
+ if (error instanceof AggregateError && error.errors?.length) {
125
+ return error.errors
126
+ .map((e) => (e instanceof Error ? e.message || e.toString() : String(e)))
127
+ .join('; ');
128
+ }
129
+ if (error instanceof Error) {
130
+ return error.message || error.toString();
131
+ }
132
+ return String(error);
133
+ }
134
+
123
135
  private async initializeRedisSync() {
124
136
  if (!this.redisUrl) {
125
137
  this.logger.log(
@@ -160,7 +172,7 @@ export class SettingService implements OnModuleInit, OnModuleDestroy {
160
172
  );
161
173
  } catch (error) {
162
174
  this.logger.warn(
163
- `Failed to initialize Redis settings sync: ${error instanceof Error ? error.message : String(error)}`,
175
+ `Failed to initialize Redis settings sync: ${this.extractRedisErrorMessage(error)}`,
164
176
  );
165
177
 
166
178
  await Promise.allSettled([
@@ -477,6 +489,7 @@ export class SettingService implements OnModuleInit, OnModuleDestroy {
477
489
  'operations.comment-edit-window',
478
490
  'lms-video-conversion-enabled',
479
491
  'lms-video-frame-capture-interval-seconds',
492
+ 'lms-error-modal-enabled',
480
493
  'url'
481
494
  ];
482
495
  const setting = await this.getSettingValues(slugs);
@@ -68,7 +68,8 @@ export class TokenService {
68
68
  } catch (error) {
69
69
  const errName = (error as any)?.name ?? 'UnknownError';
70
70
  const errMsg = (error as any)?.message ?? '';
71
- if (!(error instanceof UnauthorizedException)) {
71
+ // Token expirado é um evento rotineiro (cliente renova automaticamente); não polui o log.
72
+ if (!(error instanceof UnauthorizedException) && errName !== 'TokenExpiredError') {
72
73
  this.logger.debug(`[TokenVerify] JWT error=${errName}: ${errMsg}`);
73
74
  }
74
75
  // Return 401 for JWT errors (expired, invalid, etc.)
@@ -1,22 +1,22 @@
1
- import { Public, Role, User } from '@hed-hog/api';
1
+ import { NoRole, Public, Role, User } from '@hed-hog/api';
2
2
  import { Locale } from '@hed-hog/api-locale';
3
3
  import { Pagination } from '@hed-hog/api-pagination';
4
4
  import {
5
- BadRequestException,
6
- Body,
7
- Controller,
8
- Delete,
9
- Get,
10
- Inject,
11
- Param,
12
- ParseIntPipe,
13
- Patch,
14
- Post,
15
- Req,
16
- Res,
17
- UploadedFile,
18
- UseInterceptors,
19
- forwardRef,
5
+ BadRequestException,
6
+ Body,
7
+ Controller,
8
+ Delete,
9
+ Get,
10
+ Inject,
11
+ Param,
12
+ ParseIntPipe,
13
+ Patch,
14
+ Post,
15
+ Req,
16
+ Res,
17
+ UploadedFile,
18
+ UseInterceptors,
19
+ forwardRef,
20
20
  } from '@nestjs/common';
21
21
  import { FileInterceptor } from '@nestjs/platform-express';
22
22
  import { Request } from 'express';
@@ -135,6 +135,16 @@ export class UserController {
135
135
  return this.userService.getUserRoutes(locale, userId);
136
136
  }
137
137
 
138
+ @NoRole()
139
+ @Get('me/activity')
140
+ async listMyActivities(
141
+ @User() { id }: { id: number },
142
+ @Pagination() paginationParams,
143
+ @Locale() locale: string,
144
+ ) {
145
+ return this.userService.listUserActivities(locale, id, paginationParams);
146
+ }
147
+
138
148
  @Get(':userId/log')
139
149
  async listLogs(
140
150
  @Param('userId', ParseIntPipe) userId: number,
@@ -749,6 +749,31 @@ export class UserService {
749
749
  };
750
750
  }
751
751
 
752
+ async listUserActivities(locale: string, userId: number, paginationParams) {
753
+ await this.validateUserExists(locale, userId);
754
+
755
+ const paginated = await this.paginationService.paginatePrismaModel(
756
+ this.prismaService.user_activity,
757
+ {
758
+ page: paginationParams?.page,
759
+ pageSize: paginationParams?.pageSize,
760
+ sortField: paginationParams?.sortField || 'created_at',
761
+ sortOrder: paginationParams?.sortOrder || 'desc',
762
+ validSortFields: ['id', 'created_at', 'action'],
763
+ where: { user_id: userId },
764
+ },
765
+ );
766
+
767
+ return {
768
+ ...paginated,
769
+ data: (paginated.data || []).map((activity: any) => ({
770
+ id: String(activity.id),
771
+ action: activity.action,
772
+ createdAt: activity.created_at?.toISOString?.() || null,
773
+ })),
774
+ };
775
+ }
776
+
752
777
  // ==========================================
753
778
  // Private Helper Methods
754
779
  // ==========================================
@@ -0,0 +1,38 @@
1
+ function normalizeOrigin(value?: string | null): string | null {
2
+ const trimmed = String(value ?? '').trim().replace(/\/+$/, '');
3
+ if (!trimmed) return null;
4
+
5
+ try {
6
+ return new URL(trimmed).origin;
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ function getAllowedOrigins(): string[] {
13
+ return (process.env.CORS_ALLOWED_ORIGINS ?? '')
14
+ .split(/[\n,;]+/)
15
+ .map((url) => normalizeOrigin(url))
16
+ .filter((url): url is string => Boolean(url));
17
+ }
18
+
19
+ /**
20
+ * WebAuthn binds a credential to the exact origin (scheme + host + port) used
21
+ * during registration/authentication. With multiple frontends (e.g. admin on
22
+ * :3200, training on :3001) sharing the same backend, the relying party origin
23
+ * must follow the requesting browser, not a single hardcoded CORS entry.
24
+ */
25
+ export function resolveWebAuthnOrigin(requestOrigin?: string | null): {
26
+ rpID: string;
27
+ expectedOrigin: string;
28
+ } {
29
+ const allowedOrigins = getAllowedOrigins();
30
+ const normalizedRequestOrigin = normalizeOrigin(requestOrigin);
31
+
32
+ const expectedOrigin =
33
+ (normalizedRequestOrigin && allowedOrigins.includes(normalizedRequestOrigin)
34
+ ? normalizedRequestOrigin
35
+ : allowedOrigins[0]) ?? 'http://localhost:3200';
36
+
37
+ return { rpID: new URL(expectedOrigin).hostname, expectedOrigin };
38
+ }
@@ -129,6 +129,14 @@ export class WebhookIntegrationController {
129
129
  ) {
130
130
  return this.service.streamLogs(id, res);
131
131
  }
132
+
133
+ @Post(':id/log/:logId/retry')
134
+ retryLog(
135
+ @Param('id', ParseIntPipe) id: number,
136
+ @Param('logId', ParseIntPipe) logId: number,
137
+ ) {
138
+ return this.service.retryCall(id, logId);
139
+ }
132
140
  }
133
141
 
134
142
  @Public()
@@ -145,5 +153,20 @@ export class PublicWebhookController {
145
153
  ) {
146
154
  return this.service.executePublic(uuid, body, headers, req);
147
155
  }
156
+
157
+ /**
158
+ * GET variant so a webhook can be triggered by a plain clickable link (e.g.
159
+ * approve/reject links sent by email/WhatsApp). Query params are used as the
160
+ * payload for action templates (`{{body.decision}}` ← `?decision=approve`).
161
+ */
162
+ @Get(':uuid')
163
+ executeGet(
164
+ @Param('uuid') uuid: string,
165
+ @Query() query: any,
166
+ @Headers() headers: any,
167
+ @Req() req: Request,
168
+ ) {
169
+ return this.service.executePublic(uuid, query ?? {}, headers, req);
170
+ }
148
171
  }
149
172
 
@@ -3,6 +3,7 @@ import { PaginationModule } from '@hed-hog/api-pagination';
3
3
  import { PrismaModule } from '@hed-hog/api-prisma';
4
4
  import { HttpModule } from '@nestjs/axios';
5
5
  import { Module, forwardRef } from '@nestjs/common';
6
+ import { AppCommandModule } from '../app-command/app-command.module';
6
7
  import { IntegrationModule } from '../integration/integration.module';
7
8
  import { MailModule } from '../mail/mail.module';
8
9
  import { McpModule } from '../mcp/mcp.module';
@@ -10,7 +11,6 @@ import { EventWebhookController } from './event-webhook.controller';
10
11
  import { EventWebhookService } from './event-webhook.service';
11
12
  import { IntegrationEventCatalogController } from './integration-event-catalog.controller';
12
13
  import { IntegrationEventCatalogService } from './integration-event-catalog.service';
13
- import { WebhookCommandRegistry } from './webhook-command.registry';
14
14
  import {
15
15
  PublicWebhookController,
16
16
  WebhookIntegrationController,
@@ -26,6 +26,7 @@ import { WebhookIntegrationService } from './webhook-integration.service';
26
26
  forwardRef(() => MailModule),
27
27
  forwardRef(() => MailMainModule),
28
28
  forwardRef(() => McpModule),
29
+ AppCommandModule,
29
30
  ],
30
31
  controllers: [
31
32
  IntegrationEventCatalogController,
@@ -35,13 +36,12 @@ import { WebhookIntegrationService } from './webhook-integration.service';
35
36
  ],
36
37
  providers: [
37
38
  IntegrationEventCatalogService,
38
- WebhookCommandRegistry,
39
39
  WebhookIntegrationService,
40
40
  EventWebhookService,
41
41
  ],
42
42
  exports: [
43
43
  IntegrationEventCatalogService,
44
- WebhookCommandRegistry,
44
+ AppCommandModule,
45
45
  WebhookIntegrationService,
46
46
  EventWebhookService,
47
47
  ],