@hed-hog/core 0.0.368 → 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 (271) 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_event_catalog.yaml +118 -118
  158. package/hedhog/data/integration_provider.yaml +9 -0
  159. package/hedhog/data/integration_type.yaml +6 -0
  160. package/hedhog/data/menu.yaml +6 -0
  161. package/hedhog/data/role.yaml +7 -0
  162. package/hedhog/data/route.yaml +192 -207
  163. package/hedhog/data/setting_group.yaml +119 -13
  164. package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
  165. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
  166. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
  167. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
  168. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
  169. package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
  170. package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
  171. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
  172. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
  173. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
  174. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
  175. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
  176. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
  177. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
  178. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +141 -29
  179. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
  180. package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
  181. package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
  182. package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
  183. package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
  184. package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
  185. package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
  186. package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
  187. package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
  188. package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
  189. package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
  190. package/hedhog/frontend/messages/en.json +69 -4
  191. package/hedhog/frontend/messages/pt.json +69 -4
  192. package/hedhog/table/event_webhook.yaml +55 -55
  193. package/hedhog/table/event_webhook_delivery_log.yaml +66 -66
  194. package/hedhog/table/event_webhook_event.yaml +26 -26
  195. package/hedhog/table/integration_event_catalog.yaml +41 -41
  196. package/hedhog/table/mcp_conversation.yaml +12 -12
  197. package/hedhog/table/mcp_conversation_message.yaml +23 -23
  198. package/hedhog/table/mcp_tool_call_log.yaml +22 -22
  199. package/hedhog/table/route.yaml +33 -33
  200. package/hedhog/table/setting.yaml +2 -0
  201. package/hedhog/table/webhook_call_log.yaml +3 -0
  202. package/hedhog/table/webhook_integration.yaml +43 -43
  203. package/package.json +9 -4
  204. package/src/ai/ai.service.ts +9 -6
  205. package/src/app-command/app-command.module.ts +9 -0
  206. package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
  207. package/src/auth/auth.controller.ts +20 -4
  208. package/src/auth/auth.service.ts +10 -36
  209. package/src/auth/dto/forget.dto.ts +5 -1
  210. package/src/auth/guards/auth.guard.ts +4 -1
  211. package/src/challenge/challenge.service.ts +30 -19
  212. package/src/file/dto/upload.dto.ts +25 -1
  213. package/src/file/file.controller.ts +31 -18
  214. package/src/file/file.service.ts +341 -16
  215. package/src/file/image-optimization.service.ts +111 -46
  216. package/src/file/provider/s3.provider.ts +16 -7
  217. package/src/file/provider/spaces.provider.ts +15 -5
  218. package/src/index.ts +4 -1
  219. package/src/integration/services/event-subscriber.registry.ts +15 -0
  220. package/src/integration/services/outbox.processor.ts +2 -5
  221. package/src/integration/services/outbox.service.ts +35 -17
  222. package/src/integration/types/event.types.ts +4 -4
  223. package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
  224. package/src/integration-profile/integration-profile.controller.ts +2 -0
  225. package/src/integration-profile/integration-profile.module.ts +5 -2
  226. package/src/integration-profile/integration-profile.secrets.ts +51 -0
  227. package/src/integration-profile/integration-profile.service.ts +335 -54
  228. package/src/integration-profile/integration-profile.utils.ts +56 -0
  229. package/src/language/en.json +5 -1
  230. package/src/language/pt.json +5 -1
  231. package/src/mail/mail.controller.ts +5 -29
  232. package/src/mail/mail.module.ts +5 -5
  233. package/src/mail/mail.service.ts +37 -13
  234. package/src/mcp/decorators/mcp-tool.decorator.ts +7 -7
  235. package/src/mcp/guards/mcp-permission.guard.ts +51 -51
  236. package/src/mcp/interfaces/mcp-tool-definition.interface.ts +6 -6
  237. package/src/mcp/mcp-server.service.ts +87 -87
  238. package/src/mcp/types/mcp-context.type.ts +6 -6
  239. package/src/mcp-chat/dto/create-conversation.dto.ts +7 -7
  240. package/src/mcp-chat/dto/send-message.dto.ts +7 -7
  241. package/src/mcp-chat/mcp-chat.service.ts +5 -5
  242. package/src/notification/events/notification-stale-progress-check.event.ts +5 -5
  243. package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
  244. package/src/oauth/providers/facebook.provider.ts +3 -3
  245. package/src/oauth/providers/github.provider.ts +3 -3
  246. package/src/oauth/providers/google.provider.ts +3 -3
  247. package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
  248. package/src/oauth/providers/microsoft.provider.ts +3 -3
  249. package/src/pdf/pdf.service.ts +19 -6
  250. package/src/profile/profile.controller.ts +21 -14
  251. package/src/profile/profile.service.ts +23 -20
  252. package/src/route/dto/create.dto.ts +32 -32
  253. package/src/route/dto/update.dto.ts +31 -31
  254. package/src/security/security.service.spec.ts +222 -0
  255. package/src/setting/setting.service.ts +15 -2
  256. package/src/token/token.service.ts +2 -1
  257. package/src/user/user.controller.ts +26 -16
  258. package/src/user/user.service.ts +25 -0
  259. package/src/utils/webauthn-origin.ts +38 -0
  260. package/src/webhook-integration/integration-event-catalog.controller.ts +50 -50
  261. package/src/webhook-integration/integration-event-catalog.service.ts +117 -117
  262. package/src/webhook-integration/webhook-integration.controller.ts +23 -0
  263. package/src/webhook-integration/webhook-integration.module.ts +3 -3
  264. package/src/webhook-integration/webhook-integration.service.ts +115 -5
  265. package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
  266. package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
  267. package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
  268. package/src/menu/menu.service.spec.ts +0 -247
  269. package/src/route/route.service.spec.ts +0 -300
  270. package/src/screen/screen.service.spec.ts +0 -298
  271. package/src/user/user.service.spec.ts +0 -294
@@ -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
+ }
@@ -1,50 +1,50 @@
1
- import { DeleteDTO, Role } from '@hed-hog/api';
2
- import { Pagination } from '@hed-hog/api-pagination';
3
- import {
4
- Body,
5
- Controller,
6
- Delete,
7
- Get,
8
- Param,
9
- ParseIntPipe,
10
- Patch,
11
- Post,
12
- } from '@nestjs/common';
13
- import { IntegrationEventCatalogService } from './integration-event-catalog.service';
14
-
15
- @Role()
16
- @Controller('integration-event-catalog')
17
- export class IntegrationEventCatalogController {
18
- constructor(private readonly service: IntegrationEventCatalogService) {}
19
-
20
- @Get()
21
- list(@Pagination() paginationParams) {
22
- return this.service.list(paginationParams);
23
- }
24
-
25
- @Get('options')
26
- options() {
27
- return this.service.options();
28
- }
29
-
30
- @Get(':id')
31
- get(@Param('id', ParseIntPipe) id: number) {
32
- return this.service.get(id);
33
- }
34
-
35
- @Post()
36
- create(@Body() data: any) {
37
- return this.service.create(data);
38
- }
39
-
40
- @Patch(':id')
41
- update(@Param('id', ParseIntPipe) id: number, @Body() data: any) {
42
- return this.service.update(id, data);
43
- }
44
-
45
- @Delete()
46
- delete(@Body() data: DeleteDTO) {
47
- return this.service.delete(data);
48
- }
49
- }
50
-
1
+ import { DeleteDTO, Role } from '@hed-hog/api';
2
+ import { Pagination } from '@hed-hog/api-pagination';
3
+ import {
4
+ Body,
5
+ Controller,
6
+ Delete,
7
+ Get,
8
+ Param,
9
+ ParseIntPipe,
10
+ Patch,
11
+ Post,
12
+ } from '@nestjs/common';
13
+ import { IntegrationEventCatalogService } from './integration-event-catalog.service';
14
+
15
+ @Role()
16
+ @Controller('integration-event-catalog')
17
+ export class IntegrationEventCatalogController {
18
+ constructor(private readonly service: IntegrationEventCatalogService) {}
19
+
20
+ @Get()
21
+ list(@Pagination() paginationParams) {
22
+ return this.service.list(paginationParams);
23
+ }
24
+
25
+ @Get('options')
26
+ options() {
27
+ return this.service.options();
28
+ }
29
+
30
+ @Get(':id')
31
+ get(@Param('id', ParseIntPipe) id: number) {
32
+ return this.service.get(id);
33
+ }
34
+
35
+ @Post()
36
+ create(@Body() data: any) {
37
+ return this.service.create(data);
38
+ }
39
+
40
+ @Patch(':id')
41
+ update(@Param('id', ParseIntPipe) id: number, @Body() data: any) {
42
+ return this.service.update(id, data);
43
+ }
44
+
45
+ @Delete()
46
+ delete(@Body() data: DeleteDTO) {
47
+ return this.service.delete(data);
48
+ }
49
+ }
50
+