@hed-hog/core 0.0.370 → 0.0.373

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 +156 -38
  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 +9 -4
  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
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.370",
3
+ "version": "0.0.373",
4
+ "license": "MIT",
4
5
  "main": "dist/index.js",
5
6
  "types": "dist/index.d.ts",
6
7
  "dependencies": {
7
8
  "@aws-sdk/client-lambda": "3.1014.0",
8
9
  "@aws-sdk/client-s3": "^3.1014.0",
9
10
  "@aws-sdk/client-sts": "3.1014.0",
11
+ "@aws-sdk/lib-storage": "3.1014.0",
10
12
  "@aws-sdk/s3-request-presigner": "^3.1014.0",
11
13
  "@azure/storage-blob": "^12.27.0",
12
14
  "@google-cloud/storage": "^7.16.0",
@@ -19,6 +21,7 @@
19
21
  "@nestjs/jwt": "^11",
20
22
  "@nestjs/mapped-types": "*",
21
23
  "@nestjs/schedule": "^6.0.1",
24
+ "@nestjs/throttler": "^6.4.0",
22
25
  "@simplewebauthn/server": "^13.2.2",
23
26
  "argon2": "^0.44.0",
24
27
  "axios": "^1.9.0",
@@ -40,10 +43,10 @@
40
43
  "uuid": "^11.1.0",
41
44
  "@hed-hog/api": "0.0.8",
42
45
  "@hed-hog/api-locale": "0.0.14",
46
+ "@hed-hog/api-mail": "0.0.9",
43
47
  "@hed-hog/api-pagination": "0.0.7",
44
- "@hed-hog/api-prisma": "0.0.6",
45
48
  "@hed-hog/api-types": "0.0.1",
46
- "@hed-hog/api-mail": "0.0.9"
49
+ "@hed-hog/api-prisma": "0.0.6"
47
50
  },
48
51
  "exports": {
49
52
  ".": {
@@ -85,10 +88,12 @@
85
88
  "scripts": {
86
89
  "_dev": "pnpm build --watch",
87
90
  "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
91
+ "test": "jest --config jest.config.ts --runInBand --passWithNoTests",
88
92
  "clean": "rimraf dist tsconfig.production.tsbuildinfo",
89
93
  "prebuild": "pnpm run clean && pnpm --dir ../.. exec ts-node ./scripts/build-dependencies.ts libraries/core",
90
- "build": "tsc --project tsconfig.production.json && pnpm run copy:templates",
94
+ "build": "tsc --project tsconfig.production.json && pnpm run copy:templates && pnpm run copy:language",
91
95
  "copy:templates": "node -e \"require('fs').cpSync('src/file/templates', 'dist/file/templates', {recursive: true})\"",
96
+ "copy:language": "node -e \"const fs=require('fs');if(fs.existsSync('src/language'))fs.cpSync('src/language','dist/language',{recursive:true})\"",
92
97
  "patch": "pnpm exec ts-node ../../scripts/patch.ts libraries/core",
93
98
  "prod": "pnpm run patch && pnpm run build && pnpm publish --access public --no-git-checks"
94
99
  }
@@ -62,7 +62,10 @@ export class AiService implements OnModuleInit {
62
62
  'ai-openai-profile-id',
63
63
  'ai-gemini-profile-id',
64
64
  ]);
65
- if (!Number(s['ai-openai-profile-id']) && !Number(s['ai-gemini-profile-id'])) {
65
+ if (
66
+ !String(s['ai-openai-profile-id'] ?? '').trim() &&
67
+ !String(s['ai-gemini-profile-id'] ?? '').trim()
68
+ ) {
66
69
  this.logger.warn('No AI profiles configured. Set profiles in Settings → AI.');
67
70
  }
68
71
  } catch { /* settings not available yet */ }
@@ -442,21 +445,21 @@ export class AiService implements OnModuleInit {
442
445
  private async getAiApiKey(provider: AiProvider): Promise<string> {
443
446
  const slug = provider === 'openai' ? 'ai-openai-profile-id' : 'ai-gemini-profile-id';
444
447
  const settings = await this.settingService.getSettingValues([slug]);
445
- const profileId = Number(settings[slug]);
448
+ const profileSlug = String(settings[slug] ?? '').trim();
446
449
 
447
- if (!profileId) {
450
+ if (!profileSlug) {
448
451
  throw new BadRequestException(
449
452
  `No ${provider} AI profile configured. Set one in Settings → AI.`,
450
453
  );
451
454
  }
452
455
 
453
456
  const profile = await this.prismaService.integration_profile.findUnique({
454
- where: { id: profileId },
457
+ where: { slug: profileSlug },
455
458
  include: { integration_provider: { select: { slug: true } } },
456
459
  });
457
460
 
458
461
  if (!profile) {
459
- throw new BadRequestException(`AI profile ${profileId} not found.`);
462
+ throw new BadRequestException(`AI profile "${profileSlug}" not found.`);
460
463
  }
461
464
 
462
465
  const { apiKey } = buildAiConfigFromIntegration(
@@ -465,7 +468,7 @@ export class AiService implements OnModuleInit {
465
468
  );
466
469
 
467
470
  if (!apiKey) {
468
- throw new BadRequestException(`AI profile ${profileId} has no API key configured.`);
471
+ throw new BadRequestException(`AI profile "${profileSlug}" has no API key configured.`);
469
472
  }
470
473
 
471
474
  return apiKey;
@@ -0,0 +1,9 @@
1
+ import { Global, Module } from '@nestjs/common';
2
+ import { AppCommandRegistry } from './app-command.registry';
3
+
4
+ @Global()
5
+ @Module({
6
+ providers: [AppCommandRegistry],
7
+ exports: [AppCommandRegistry],
8
+ })
9
+ export class AppCommandModule {}
@@ -1,6 +1,6 @@
1
1
  import { Injectable } from '@nestjs/common';
2
2
 
3
- export interface WebhookCommandContext extends Record<string, unknown> {
3
+ export interface AppCommandContext extends Record<string, unknown> {
4
4
  webhookBody?: unknown;
5
5
  webhookHeaders?: Record<string, unknown>;
6
6
  remoteIp?: string | null;
@@ -8,37 +8,37 @@ export interface WebhookCommandContext extends Record<string, unknown> {
8
8
  integrationId?: number;
9
9
  }
10
10
 
11
- export interface WebhookCommand {
11
+ export interface AppCommand {
12
12
  slug: string;
13
13
  name: string;
14
14
  description: string;
15
15
  inputSchema: Record<string, unknown>;
16
16
  handler: (
17
17
  params: Record<string, unknown>,
18
- context?: WebhookCommandContext,
18
+ context?: AppCommandContext,
19
19
  ) => Promise<unknown>;
20
20
  }
21
21
 
22
22
  @Injectable()
23
- export class WebhookCommandRegistry {
24
- private readonly commands = new Map<string, WebhookCommand>();
23
+ export class AppCommandRegistry {
24
+ private readonly commands = new Map<string, AppCommand>();
25
25
 
26
- register(command: WebhookCommand): void {
26
+ register(command: AppCommand): void {
27
27
  this.commands.set(command.slug, command);
28
28
  }
29
29
 
30
- find(slug: string): WebhookCommand | undefined {
30
+ find(slug: string): AppCommand | undefined {
31
31
  return this.commands.get(slug);
32
32
  }
33
33
 
34
- list(): WebhookCommand[] {
34
+ list(): AppCommand[] {
35
35
  return Array.from(this.commands.values());
36
36
  }
37
37
 
38
38
  async dispatch(
39
39
  slug: string,
40
40
  params: Record<string, unknown>,
41
- context?: WebhookCommandContext,
41
+ context?: AppCommandContext,
42
42
  ): Promise<unknown> {
43
43
  const command = this.find(slug);
44
44
  if (!command) {
@@ -12,8 +12,10 @@ import {
12
12
  Post,
13
13
  Req,
14
14
  Res,
15
- UnauthorizedException
15
+ UnauthorizedException,
16
+ UseGuards
16
17
  } from '@nestjs/common';
18
+ import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
17
19
  import { TokenService } from '../token/token.service';
18
20
  import { CreateWithEmailAndPasswordDTO } from '../user/dto/create-with-email-and-password.dto';
19
21
  import { UserService } from '../user/user.service';
@@ -87,7 +89,11 @@ export class AuthController {
87
89
  };
88
90
  }
89
91
 
92
+ // Rate-limit contra brute-force de credenciais: 10 tentativas/min por IP
93
+ // (ajustável; valor acomoda a suíte E2E de auth que faz vários logins seguidos).
90
94
  @Public()
95
+ @UseGuards(ThrottlerGuard)
96
+ @Throttle({ default: { limit: 10, ttl: 60_000 } })
91
97
  @Post('login')
92
98
  async login(
93
99
  @Locale() locale: string,
@@ -101,6 +107,8 @@ export class AuthController {
101
107
  }
102
108
 
103
109
  @Public()
110
+ @UseGuards(ThrottlerGuard)
111
+ @Throttle({ default: { limit: 10, ttl: 60_000 } })
104
112
  @Post('login-email-verification')
105
113
  async emailVerificationLogin(
106
114
  @Locale() locale: string,
@@ -132,6 +140,8 @@ export class AuthController {
132
140
  }
133
141
 
134
142
  @Public()
143
+ @UseGuards(ThrottlerGuard)
144
+ @Throttle({ default: { limit: 10, ttl: 60_000 } })
135
145
  @Post('login-code')
136
146
  async loginCode(
137
147
  @Locale() locale: string,
@@ -156,6 +166,8 @@ export class AuthController {
156
166
  }
157
167
 
158
168
  @Public()
169
+ @UseGuards(ThrottlerGuard)
170
+ @Throttle({ default: { limit: 10, ttl: 60_000 } })
159
171
  @Post('login-recovery-code')
160
172
  async loginRecoveryCode(
161
173
  @Locale() locale: string,
@@ -191,9 +203,10 @@ export class AuthController {
191
203
  @Post('webauthn/generate')
192
204
  async generateWebAuthnOptions(
193
205
  @Locale() locale: string,
194
- @Body() { mfaToken }: { mfaToken: string }
206
+ @Body() { mfaToken }: { mfaToken: string },
207
+ @Headers('origin') origin: string
195
208
  ) {
196
- return this.service.generateWebAuthnAuthenticationOptions(locale, mfaToken);
209
+ return this.service.generateWebAuthnAuthenticationOptions(locale, mfaToken, origin);
197
210
  }
198
211
 
199
212
  @Public()
@@ -203,6 +216,7 @@ export class AuthController {
203
216
  @Body() { mfaToken, assertionResponse }: { mfaToken: string; assertionResponse: any },
204
217
  @Ip() ipAddress: string,
205
218
  @Headers('user-agent') userAgent: string,
219
+ @Headers('origin') origin: string,
206
220
  @Res({ passthrough: true }) res,
207
221
  ) {
208
222
  const { accessToken, refreshToken, session, requiresPasswordReset } = await this.service.verifyWebAuthnAuthentication(
@@ -210,7 +224,9 @@ export class AuthController {
210
224
  mfaToken,
211
225
  assertionResponse,
212
226
  ipAddress,
213
- userAgent
227
+ userAgent,
228
+ undefined,
229
+ origin,
214
230
  );
215
231
 
216
232
  await this.token.setRefreshTokenCookie(locale, res, refreshToken, session.expires_at);
@@ -17,6 +17,7 @@ import { SessionService } from '../session/session.service';
17
17
  import { SettingService } from '../setting/setting.service';
18
18
  import { TokenService } from '../token/token.service';
19
19
  import { UserService } from '../user/user.service';
20
+ import { resolveWebAuthnOrigin } from '../utils/webauthn-origin';
20
21
  import { ForgetDTO } from './dto/forget.dto';
21
22
  import { LoginDTO } from './dto/login.dto';
22
23
 
@@ -508,12 +509,12 @@ export class AuthService {
508
509
 
509
510
  }
510
511
 
511
- async forgotPassword(locale: string, { email }: ForgetDTO) {
512
+ async forgotPassword(locale: string, { email, app }: ForgetDTO) {
512
513
  const user = await this.user.findUserByEmail(locale, email);
513
514
  if (user){
514
515
  await this.user.registerUserActivity(user?.id, "forgotPassword")
515
516
  }
516
- await this.challenge.forgotEmail(locale, email);
517
+ await this.challenge.forgotEmail(locale, email, app);
517
518
  return { success: true };
518
519
  }
519
520
 
@@ -530,41 +531,17 @@ export class AuthService {
530
531
 
531
532
  async forgotResetPassword(locale: string, password: string, code: string, ipAddress: string, userAgent: string) {
532
533
  const challenge = await this.challenge.verifyCodePassword(locale, code);
534
+ const userIdentifier = challenge.user_identifier;
533
535
  const passwordHash = await this.security.hashArgon2(password);
534
536
 
535
537
  await this.prisma.user_credential.updateMany({
536
538
  where: {
537
539
  type: 'password',
538
- user: {
539
- user_mfa: {
540
- some: {
541
- user_mfa_challenge: {
542
- some: { id: challenge.id }
543
- }
544
- }
545
- }
546
- }
540
+ user_id: userIdentifier.user_id,
547
541
  },
548
542
  data: { hash: passwordHash, requires_reset: false },
549
543
  });
550
544
 
551
- const userIdentifier = await this.prisma.user_identifier.findFirst({
552
- where: {
553
- type: 'email',
554
- user_id: challenge.user_mfa.user_id,
555
- enabled: true
556
- },
557
- include: {
558
- user: {
559
- select: { name: true }
560
- }
561
- }
562
- });
563
-
564
- if (!userIdentifier) {
565
- throw new BadRequestException(getLocaleText('userIdentifierNotFound', locale, 'User identifier not found.'));
566
- }
567
-
568
545
  this.mail.sendTemplatedMail(
569
546
  locale,
570
547
  {
@@ -576,7 +553,7 @@ export class AuthService {
576
553
  }
577
554
  );
578
555
 
579
- const { accessToken, refreshToken, session } = await this.getAuthenticationPayload(locale, challenge.user_mfa.user_id, ipAddress, userAgent);
556
+ const { accessToken, refreshToken, session } = await this.getAuthenticationPayload(locale, userIdentifier.user_id, ipAddress, userAgent);
580
557
  await this.user.registerUserActivity(userIdentifier.id, "resetPassword")
581
558
  return { accessToken, refreshToken, session }
582
559
  }
@@ -780,7 +757,7 @@ export class AuthService {
780
757
  };
781
758
  }
782
759
 
783
- async generateWebAuthnAuthenticationOptions(locale: string, mfaToken: string) {
760
+ async generateWebAuthnAuthenticationOptions(locale: string, mfaToken: string, requestOrigin?: string) {
784
761
  const { generateAuthenticationOptions } = await import('@simplewebauthn/server');
785
762
  const payload = await this.token.verifyMfaChallengeToken(mfaToken);
786
763
 
@@ -798,8 +775,7 @@ export class AuthService {
798
775
  throw new BadRequestException(getLocaleText('webauthnNotConfigured', locale, 'WebAuthn not configured.'));
799
776
  }
800
777
 
801
- const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
802
- const rpID = new URL(primaryOrigin).hostname;
778
+ const { rpID } = resolveWebAuthnOrigin(requestOrigin);
803
779
 
804
780
  const allowCredentials = user.user_mfa
805
781
  .filter(mfa => mfa.user_mfa_webauthn && mfa.user_mfa_webauthn.length > 0)
@@ -825,7 +801,7 @@ export class AuthService {
825
801
  return options;
826
802
  }
827
803
 
828
- async verifyWebAuthnAuthentication(locale: string, mfaToken: string, assertionResponse: any, ipAddress: string, userAgent: string, browserId?: string | null) {
804
+ async verifyWebAuthnAuthentication(locale: string, mfaToken: string, assertionResponse: any, ipAddress: string, userAgent: string, browserId?: string | null, requestOrigin?: string) {
829
805
  const { verifyAuthenticationResponse } = await import('@simplewebauthn/server');
830
806
  const payload = await this.token.verifyMfaChallengeToken(mfaToken);
831
807
  const userId = payload.userId;
@@ -853,9 +829,7 @@ export class AuthService {
853
829
  }
854
830
 
855
831
  const authenticator = challengeRecord.user_mfa.user_mfa_webauthn[0];
856
- const primaryOrigin = (process.env.CORS_ALLOWED_ORIGINS ?? '').split(/[\n,;]+/).map(u => u.trim()).filter(Boolean)[0] ?? 'http://localhost:3200';
857
- const rpID = new URL(primaryOrigin).hostname;
858
- const expectedOrigin = primaryOrigin;
832
+ const { rpID, expectedOrigin } = resolveWebAuthnOrigin(requestOrigin);
859
833
 
860
834
  const verification = await verifyAuthenticationResponse({
861
835
  response: assertionResponse,
@@ -1,5 +1,5 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
- import { IsNotEmpty } from 'class-validator';
2
+ import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
3
3
  import { IsEmailWithSettings } from '../../validators/is-email-with-settings.validator';
4
4
 
5
5
  export class ForgetDTO {
@@ -12,4 +12,8 @@ export class ForgetDTO {
12
12
  return this._email;
13
13
  }
14
14
  private _email: string;
15
+
16
+ @IsOptional()
17
+ @IsString()
18
+ app?: string;
15
19
  }
@@ -83,7 +83,10 @@ export class AuthGuard implements CanActivate {
83
83
  } else {
84
84
  const errName = (error as any)?.name ?? 'UnknownError';
85
85
  const message = (error as any)?.message || errName || 'Unauthorized';
86
- this.logger.warn(`[AuthGuard] Token rejected ${request.method} ${request.url} error=${errName}: ${message}`);
86
+ // Token expirado é rotineiro (cliente renova); só alerta para rejeições genuínas.
87
+ if (!/expired/i.test(message)) {
88
+ this.logger.warn(`[AuthGuard] Token rejected ${request.method} ${request.url} error=${errName}: ${message}`);
89
+ }
87
90
  throw new UnauthorizedException(message);
88
91
  }
89
92
  }
@@ -185,13 +185,12 @@ export class ChallengeService {
185
185
  return { codeHash };
186
186
  }
187
187
 
188
- async forgotEmail(locale: string, email: string) {
189
- const openChallengesCount = await this.prisma.user_mfa_challenge.count({
188
+ async forgotEmail(locale: string, email: string, app?: string) {
189
+ const openChallengesCount = await this.prisma.user_identifier_challenge.count({
190
190
  where: {
191
- user_mfa: {
192
- user_mfa_email: {
193
- some: { email }
194
- }
191
+ user_identifier: {
192
+ type: 'email',
193
+ value: email,
195
194
  },
196
195
  expires_at: { gt: new Date() }
197
196
  }
@@ -201,27 +200,26 @@ export class ChallengeService {
201
200
  throw new BadRequestException(getLocaleText('forgot.rateLimit', locale, 'Too many password reset requests. Please try again later.'));
202
201
  }
203
202
 
204
- const userMfaEmail = await this.prisma.user_mfa_email.findFirst({
205
- where: { email },
206
- include: {
207
- user_mfa: { include: { user: true } },
208
- },
203
+ const userIdentifier = await this.prisma.user_identifier.findFirst({
204
+ where: { type: 'email', value: email, enabled: true },
205
+ include: { user: true },
209
206
  });
210
207
 
211
- if (!userMfaEmail) throw new BadRequestException(getLocaleText('forgot.userNotFound', locale, 'User not found'));
208
+ if (!userIdentifier) throw new BadRequestException(getLocaleText('forgot.userNotFound', locale, 'User not found'));
212
209
 
213
210
  const settings = await this.setting.getSettingValues([
214
211
  'mfa-challenge-expiration-minutes',
215
- 'url'
212
+ 'url',
213
+ 'app-urls'
216
214
  ]);
217
215
 
218
216
  const token = await this.token.createOpaqueToken(16);
219
217
  const hash = this.security.hashWithPepper(token);
220
- const challenge = await this.prisma.user_mfa_challenge.create({
218
+ const challenge = await this.prisma.user_identifier_challenge.create({
221
219
  data: {
222
220
  hash,
223
221
  expires_at: new Date(Date.now() + (settings['mfa-challenge-expiration-minutes'] || 15) * 60000),
224
- user_mfa_id: userMfaEmail.user_mfa_id,
222
+ user_identifier_id: userIdentifier.id,
225
223
  },
226
224
  });
227
225
 
@@ -232,8 +230,8 @@ export class ChallengeService {
232
230
  email,
233
231
  slug: 'auth-forget-password',
234
232
  variables: {
235
- link: `${settings['url']}/reset-password?token=${tokenBase64}`,
236
- name: userMfaEmail.user_mfa.user.name,
233
+ link: `${this.resolveAppUrl(settings, app)}/reset-password?token=${tokenBase64}`,
234
+ name: userIdentifier.user.name,
237
235
  }
238
236
  }
239
237
  );
@@ -241,15 +239,28 @@ export class ChallengeService {
241
239
  return challenge;
242
240
  }
243
241
 
242
+ private resolveAppUrl(settings: Record<string, any>, app?: string): string {
243
+ const appUrls: string[] = Array.isArray(settings['app-urls']) ? settings['app-urls'] : [];
244
+
245
+ if (app) {
246
+ const entry = appUrls.find((item) => item.split('=')[0]?.trim() === app);
247
+ if (entry) {
248
+ return entry.slice(entry.indexOf('=') + 1).trim();
249
+ }
250
+ }
251
+
252
+ return settings['url'];
253
+ }
254
+
244
255
  async verifyCodePassword(locale: string, code: string) {
245
256
  const hash = this.security.hashWithPepper(code);
246
- const challenge = await this.prisma.user_mfa_challenge.findFirst({
257
+ const challenge = await this.prisma.user_identifier_challenge.findFirst({
247
258
  where: {
248
259
  hash,
249
260
  expires_at: { gt: new Date() }
250
261
  },
251
262
  include: {
252
- user_mfa: { include: { user: true } },
263
+ user_identifier: { include: { user: true } },
253
264
  },
254
265
  });
255
266
 
@@ -1,8 +1,32 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
- import { IsOptional, IsString } from 'class-validator';
2
+ import { Type } from 'class-transformer';
3
+ import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
3
4
 
4
5
  export class UploadFileDTO {
5
6
  @IsString({ message: (args) => getLocaleText('validation.stringRequired', args.value) })
6
7
  @IsOptional()
7
8
  destination?: string;
9
+
10
+ // Optional informational tag for the kind of image being uploaded
11
+ // (e.g. "course-banner", "course-logo", "course-square").
12
+ @IsString()
13
+ @IsOptional()
14
+ imageType?: string;
15
+
16
+ // Target dimensions for a precise resize on upload. When provided, the image
17
+ // is resized to fit these bounds before compression/WebP conversion. Sent via
18
+ // multipart form fields, so they arrive as strings and are coerced to numbers.
19
+ @Type(() => Number)
20
+ @IsInt()
21
+ @Min(1)
22
+ @Max(8000)
23
+ @IsOptional()
24
+ maxWidth?: number;
25
+
26
+ @Type(() => Number)
27
+ @IsInt()
28
+ @Min(1)
29
+ @Max(8000)
30
+ @IsOptional()
31
+ maxHeight?: number;
8
32
  }
@@ -147,10 +147,32 @@ export class FileController {
147
147
  }
148
148
  }
149
149
 
150
+ @Public()
151
+ @Get('image/:id')
152
+ async openImage(
153
+ @Param('id', ParseIntPipe) id: number,
154
+ @Req() req: Request,
155
+ @Res() res: Response,
156
+ @Locale() locale: string,
157
+ ) {
158
+ try {
159
+ await this.fileService.serveImageById(
160
+ locale,
161
+ id,
162
+ req.headers['if-none-match'],
163
+ res,
164
+ );
165
+ } catch (error: any) {
166
+ const html = this.renderDownloadErrorHtml(locale, error);
167
+ res.status(400).set('Content-Type', 'text/html').send(html);
168
+ }
169
+ }
170
+
150
171
  @Public()
151
172
  @Get('open/:token')
152
173
  async open(
153
174
  @Param('token') token: string,
175
+ @Req() req: Request,
154
176
  @Res() res: Response,
155
177
  @Locale() locale: string,
156
178
  ) {
@@ -161,23 +183,12 @@ export class FileController {
161
183
  const isNumericId = Number.isInteger(numericId) && String(numericId) === token;
162
184
 
163
185
  if (isNumericId && numericId > 0) {
164
- const { stream, filename, mimetype, size } =
165
- await this.fileService.openById(locale, numericId);
166
-
167
- const encodedFilename = encodeURIComponent(filename);
168
-
169
- res.set({
170
- 'Content-Type': mimetype || 'application/octet-stream',
171
- 'Content-Disposition':
172
- `inline; filename="${filename}"; ` +
173
- `filename*=UTF-8''${encodedFilename}`,
174
- });
175
-
176
- if (typeof size === 'number') {
177
- res.set('Content-Length', `${size}`);
178
- }
179
-
180
- stream.pipe(res);
186
+ await this.fileService.serveFileById(
187
+ locale,
188
+ numericId,
189
+ req.headers['if-none-match'],
190
+ res,
191
+ );
181
192
  return;
182
193
  }
183
194
 
@@ -215,7 +226,9 @@ export class FileController {
215
226
  @Body() body: UploadFileDTO,
216
227
  ) {
217
228
  const destination = body.destination || 'files';
218
- return this.fileService.upload(destination, file);
229
+ const maxWidth = Number(body.maxWidth) || undefined;
230
+ const maxHeight = Number(body.maxHeight) || undefined;
231
+ return this.fileService.upload(destination, file, { maxWidth, maxHeight });
219
232
  }
220
233
 
221
234
  @Delete()