@hed-hog/core 0.0.355 → 0.0.358

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 (287) hide show
  1. package/dist/ai/ai.service.d.ts +3 -12
  2. package/dist/ai/ai.service.d.ts.map +1 -1
  3. package/dist/ai/ai.service.js +34 -125
  4. package/dist/ai/ai.service.js.map +1 -1
  5. package/dist/auth/guards/auth.guard.d.ts +6 -1
  6. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  7. package/dist/auth/guards/auth.guard.js +33 -2
  8. package/dist/auth/guards/auth.guard.js.map +1 -1
  9. package/dist/core.module.d.ts.map +1 -1
  10. package/dist/core.module.js +3 -3
  11. package/dist/core.module.js.map +1 -1
  12. package/dist/file/file.controller.d.ts +4 -2
  13. package/dist/file/file.controller.d.ts.map +1 -1
  14. package/dist/file/file.controller.js +44 -38
  15. package/dist/file/file.controller.js.map +1 -1
  16. package/dist/file/file.service.d.ts +2 -2
  17. package/dist/file/file.service.d.ts.map +1 -1
  18. package/dist/file/file.service.js +58 -89
  19. package/dist/file/file.service.js.map +1 -1
  20. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  21. package/dist/file/provider/azure.provider.js +10 -15
  22. package/dist/file/provider/azure.provider.js.map +1 -1
  23. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  24. package/dist/file/provider/gcs.provider.js +8 -9
  25. package/dist/file/provider/gcs.provider.js.map +1 -1
  26. package/dist/file/provider/local.provider.d.ts.map +1 -1
  27. package/dist/file/provider/local.provider.js +11 -10
  28. package/dist/file/provider/local.provider.js.map +1 -1
  29. package/dist/file/provider/provider.enum.d.ts +2 -1
  30. package/dist/file/provider/provider.enum.d.ts.map +1 -1
  31. package/dist/file/provider/provider.enum.js +1 -0
  32. package/dist/file/provider/provider.enum.js.map +1 -1
  33. package/dist/file/provider/provider.factory.d.ts +1 -1
  34. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  35. package/dist/file/provider/provider.factory.js +18 -5
  36. package/dist/file/provider/provider.factory.js.map +1 -1
  37. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  38. package/dist/file/provider/s3.provider.js +4 -20
  39. package/dist/file/provider/s3.provider.js.map +1 -1
  40. package/dist/file/provider/spaces.provider.d.ts +27 -0
  41. package/dist/file/provider/spaces.provider.d.ts.map +1 -0
  42. package/dist/file/provider/spaces.provider.js +203 -0
  43. package/dist/file/provider/spaces.provider.js.map +1 -0
  44. package/dist/index.d.ts +4 -3
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +4 -3
  47. package/dist/index.js.map +1 -1
  48. package/dist/integration-profile/dto/create.dto.d.ts +9 -0
  49. package/dist/integration-profile/dto/create.dto.d.ts.map +1 -0
  50. package/dist/integration-profile/dto/create.dto.js +54 -0
  51. package/dist/integration-profile/dto/create.dto.js.map +1 -0
  52. package/dist/integration-profile/dto/export.dto.d.ts +5 -0
  53. package/dist/integration-profile/dto/export.dto.d.ts.map +1 -0
  54. package/dist/integration-profile/dto/export.dto.js +29 -0
  55. package/dist/integration-profile/dto/export.dto.js.map +1 -0
  56. package/dist/integration-profile/dto/import-confirm.dto.d.ts +17 -0
  57. package/dist/integration-profile/dto/import-confirm.dto.d.ts.map +1 -0
  58. package/dist/integration-profile/dto/import-confirm.dto.js +77 -0
  59. package/dist/integration-profile/dto/import-confirm.dto.js.map +1 -0
  60. package/dist/integration-profile/dto/update.dto.d.ts.map +1 -0
  61. package/dist/integration-profile/dto/update.dto.js.map +1 -0
  62. package/dist/integration-profile/integration-profile.controller.d.ts +142 -0
  63. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -0
  64. package/dist/integration-profile/integration-profile.controller.js +166 -0
  65. package/dist/integration-profile/integration-profile.controller.js.map +1 -0
  66. package/dist/integration-profile/integration-profile.module.d.ts +3 -0
  67. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -0
  68. package/dist/{mail-profile/mail-profile.module.js → integration-profile/integration-profile.module.js} +11 -13
  69. package/dist/integration-profile/integration-profile.module.js.map +1 -0
  70. package/dist/integration-profile/integration-profile.service.d.ts +160 -0
  71. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -0
  72. package/dist/integration-profile/integration-profile.service.js +609 -0
  73. package/dist/integration-profile/integration-profile.service.js.map +1 -0
  74. package/dist/integration-profile/integration-profile.utils.d.ts +30 -0
  75. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -0
  76. package/dist/integration-profile/integration-profile.utils.js +156 -0
  77. package/dist/integration-profile/integration-profile.utils.js.map +1 -0
  78. package/dist/mail/mail.module.d.ts.map +1 -1
  79. package/dist/mail/mail.module.js +36 -23
  80. package/dist/mail/mail.module.js.map +1 -1
  81. package/dist/mail/mail.service.d.ts +4 -2
  82. package/dist/mail/mail.service.d.ts.map +1 -1
  83. package/dist/mail/mail.service.js +33 -51
  84. package/dist/mail/mail.service.js.map +1 -1
  85. package/dist/mcp/guards/mcp-auth.guard.js +1 -1
  86. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
  87. package/dist/mcp/mcp-api-key.controller.d.ts +2 -0
  88. package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -1
  89. package/dist/mcp/mcp-api-key.controller.js +1 -1
  90. package/dist/mcp/mcp-api-key.controller.js.map +1 -1
  91. package/dist/mcp/mcp-api-key.service.d.ts +2 -1
  92. package/dist/mcp/mcp-api-key.service.d.ts.map +1 -1
  93. package/dist/mcp/mcp-api-key.service.js +6 -2
  94. package/dist/mcp/mcp-api-key.service.js.map +1 -1
  95. package/dist/mcp/mcp.module.d.ts.map +1 -1
  96. package/dist/mcp/mcp.module.js +3 -5
  97. package/dist/mcp/mcp.module.js.map +1 -1
  98. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  99. package/dist/mcp-chat/mcp-chat.service.js +17 -16
  100. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  101. package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
  102. package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
  103. package/dist/oauth/oauth.controller.d.ts +4 -1
  104. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  105. package/dist/oauth/oauth.controller.js +31 -5
  106. package/dist/oauth/oauth.controller.js.map +1 -1
  107. package/dist/oauth/oauth.service.d.ts +12 -1
  108. package/dist/oauth/oauth.service.d.ts.map +1 -1
  109. package/dist/oauth/oauth.service.js +125 -23
  110. package/dist/oauth/oauth.service.js.map +1 -1
  111. package/dist/oauth/providers/abstract.provider.d.ts +1 -1
  112. package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
  113. package/dist/oauth/providers/facebook.provider.d.ts +5 -2
  114. package/dist/oauth/providers/facebook.provider.d.ts.map +1 -1
  115. package/dist/oauth/providers/facebook.provider.js +34 -19
  116. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  117. package/dist/oauth/providers/github.provider.d.ts +6 -2
  118. package/dist/oauth/providers/github.provider.d.ts.map +1 -1
  119. package/dist/oauth/providers/github.provider.js +43 -20
  120. package/dist/oauth/providers/github.provider.js.map +1 -1
  121. package/dist/oauth/providers/google.provider.d.ts +5 -2
  122. package/dist/oauth/providers/google.provider.d.ts.map +1 -1
  123. package/dist/oauth/providers/google.provider.js +34 -19
  124. package/dist/oauth/providers/google.provider.js.map +1 -1
  125. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -2
  126. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  127. package/dist/oauth/providers/microsoft-entra-id.provider.js +43 -40
  128. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  129. package/dist/oauth/providers/microsoft.provider.d.ts +5 -2
  130. package/dist/oauth/providers/microsoft.provider.d.ts.map +1 -1
  131. package/dist/oauth/providers/microsoft.provider.js +34 -19
  132. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  133. package/dist/profile/profile.controller.js +1 -1
  134. package/dist/profile/profile.controller.js.map +1 -1
  135. package/dist/security/security.service.d.ts +2 -0
  136. package/dist/security/security.service.d.ts.map +1 -1
  137. package/dist/security/security.service.js +21 -0
  138. package/dist/security/security.service.js.map +1 -1
  139. package/dist/setting/setting.service.d.ts +26 -1
  140. package/dist/setting/setting.service.d.ts.map +1 -1
  141. package/dist/setting/setting.service.js +240 -7
  142. package/dist/setting/setting.service.js.map +1 -1
  143. package/dist/user/user.controller.d.ts +2 -1
  144. package/dist/user/user.controller.d.ts.map +1 -1
  145. package/dist/user/user.controller.js +5 -4
  146. package/dist/user/user.controller.js.map +1 -1
  147. package/dist/user/user.service.d.ts +4 -2
  148. package/dist/user/user.service.d.ts.map +1 -1
  149. package/dist/user/user.service.js +19 -4
  150. package/dist/user/user.service.js.map +1 -1
  151. package/dist/webhook-integration/dto/webhook-action.dto.d.ts +17 -2
  152. package/dist/webhook-integration/dto/webhook-action.dto.d.ts.map +1 -1
  153. package/dist/webhook-integration/dto/webhook-action.dto.js +93 -2
  154. package/dist/webhook-integration/dto/webhook-action.dto.js.map +1 -1
  155. package/dist/webhook-integration/event-webhook.controller.d.ts +1 -0
  156. package/dist/webhook-integration/event-webhook.controller.d.ts.map +1 -1
  157. package/dist/webhook-integration/event-webhook.controller.js +10 -0
  158. package/dist/webhook-integration/event-webhook.controller.js.map +1 -1
  159. package/dist/webhook-integration/event-webhook.service.d.ts +2 -0
  160. package/dist/webhook-integration/event-webhook.service.d.ts.map +1 -1
  161. package/dist/webhook-integration/event-webhook.service.js +30 -0
  162. package/dist/webhook-integration/event-webhook.service.js.map +1 -1
  163. package/dist/webhook-integration/webhook-command.registry.d.ts +15 -0
  164. package/dist/webhook-integration/webhook-command.registry.d.ts.map +1 -0
  165. package/dist/webhook-integration/webhook-command.registry.js +36 -0
  166. package/dist/webhook-integration/webhook-command.registry.js.map +1 -0
  167. package/dist/webhook-integration/webhook-integration.controller.d.ts +20 -0
  168. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  169. package/dist/webhook-integration/webhook-integration.controller.js +48 -0
  170. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  171. package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
  172. package/dist/webhook-integration/webhook-integration.module.js +5 -0
  173. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  174. package/dist/webhook-integration/webhook-integration.service.d.ts +27 -2
  175. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  176. package/dist/webhook-integration/webhook-integration.service.js +377 -50
  177. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  178. package/hedhog/data/file_provider.yaml +9 -1
  179. package/hedhog/data/integration_provider.yaml +148 -0
  180. package/hedhog/data/integration_type.yaml +29 -0
  181. package/hedhog/data/menu.yaml +6 -21
  182. package/hedhog/data/role.yaml +8 -0
  183. package/hedhog/data/route.yaml +73 -373
  184. package/hedhog/data/setting_group.yaml +209 -365
  185. package/hedhog/data/setting_subgroup.yaml +21 -13
  186. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +25 -20
  187. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +421 -0
  188. package/hedhog/frontend/app/account/layout.tsx.ejs +6 -3
  189. package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +2 -2
  190. package/hedhog/frontend/app/account/tokens/page.tsx.ejs +5 -0
  191. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +938 -225
  192. package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
  193. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +3 -1
  194. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +1830 -0
  195. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +829 -128
  196. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +1 -820
  197. package/hedhog/frontend/app/setting-list/page.tsx.ejs +229 -395
  198. package/hedhog/frontend/app/users/permissions.tsx.ejs +79 -2
  199. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +7 -2
  200. package/hedhog/frontend/messages/en.json +239 -3
  201. package/hedhog/frontend/messages/pt.json +239 -3
  202. package/hedhog/table/file.yaml +1 -1
  203. package/hedhog/table/integration_profile.yaml +34 -0
  204. package/hedhog/table/integration_provider.yaml +23 -0
  205. package/hedhog/table/integration_type.yaml +19 -0
  206. package/hedhog/table/mcp_api_key.yaml +4 -0
  207. package/hedhog/table/webhook_action.yaml +141 -85
  208. package/package.json +4 -3
  209. package/src/ai/ai.service.ts +35 -153
  210. package/src/auth/guards/auth.guard.ts +32 -0
  211. package/src/core.module.ts +3 -3
  212. package/src/file/file.controller.ts +51 -42
  213. package/src/file/file.service.ts +67 -104
  214. package/src/file/provider/azure.provider.ts +14 -28
  215. package/src/file/provider/gcs.provider.ts +9 -9
  216. package/src/file/provider/local.provider.ts +10 -13
  217. package/src/file/provider/provider.enum.ts +1 -0
  218. package/src/file/provider/provider.factory.ts +21 -6
  219. package/src/file/provider/s3.provider.ts +4 -20
  220. package/src/file/provider/spaces.provider.ts +246 -0
  221. package/src/index.ts +6 -3
  222. package/src/integration-profile/dto/create.dto.ts +44 -0
  223. package/src/integration-profile/dto/export.dto.ts +15 -0
  224. package/src/integration-profile/dto/import-confirm.dto.ts +61 -0
  225. package/src/integration-profile/integration-profile.controller.ts +131 -0
  226. package/src/integration-profile/integration-profile.module.ts +16 -0
  227. package/src/integration-profile/integration-profile.service.ts +793 -0
  228. package/src/integration-profile/integration-profile.utils.ts +173 -0
  229. package/src/mail/mail.module.ts +41 -22
  230. package/src/mail/mail.service.ts +40 -64
  231. package/src/mcp/guards/mcp-auth.guard.ts +1 -1
  232. package/src/mcp/mcp-api-key.controller.ts +2 -2
  233. package/src/mcp/mcp-api-key.service.ts +6 -2
  234. package/src/mcp/mcp.module.ts +3 -5
  235. package/src/mcp-chat/mcp-chat.service.ts +23 -20
  236. package/src/oauth/interfaces/OAuthProvider.ts +1 -1
  237. package/src/oauth/oauth.controller.ts +30 -2
  238. package/src/oauth/oauth.service.ts +194 -35
  239. package/src/oauth/providers/abstract.provider.ts +1 -1
  240. package/src/oauth/providers/facebook.provider.ts +36 -20
  241. package/src/oauth/providers/github.provider.ts +46 -23
  242. package/src/oauth/providers/google.provider.ts +36 -20
  243. package/src/oauth/providers/microsoft-entra-id.provider.ts +42 -46
  244. package/src/oauth/providers/microsoft.provider.ts +36 -21
  245. package/src/profile/profile.controller.ts +2 -2
  246. package/src/security/security.service.ts +34 -1
  247. package/src/setting/setting.service.ts +307 -9
  248. package/src/user/user.controller.ts +4 -1
  249. package/src/user/user.service.ts +20 -2
  250. package/src/webhook-integration/dto/webhook-action.dto.ts +85 -4
  251. package/src/webhook-integration/event-webhook.controller.ts +55 -50
  252. package/src/webhook-integration/event-webhook.service.ts +422 -384
  253. package/src/webhook-integration/webhook-command.registry.ts +42 -0
  254. package/src/webhook-integration/webhook-integration.controller.ts +25 -0
  255. package/src/webhook-integration/webhook-integration.module.ts +50 -45
  256. package/src/webhook-integration/webhook-integration.service.ts +444 -53
  257. package/dist/mail-profile/dto/create.dto.d.ts +0 -23
  258. package/dist/mail-profile/dto/create.dto.d.ts.map +0 -1
  259. package/dist/mail-profile/dto/create.dto.js +0 -139
  260. package/dist/mail-profile/dto/create.dto.js.map +0 -1
  261. package/dist/mail-profile/dto/update.dto.d.ts.map +0 -1
  262. package/dist/mail-profile/dto/update.dto.js.map +0 -1
  263. package/dist/mail-profile/mail-profile.controller.d.ts +0 -94
  264. package/dist/mail-profile/mail-profile.controller.d.ts.map +0 -1
  265. package/dist/mail-profile/mail-profile.controller.js +0 -95
  266. package/dist/mail-profile/mail-profile.controller.js.map +0 -1
  267. package/dist/mail-profile/mail-profile.module.d.ts +0 -3
  268. package/dist/mail-profile/mail-profile.module.d.ts.map +0 -1
  269. package/dist/mail-profile/mail-profile.module.js.map +0 -1
  270. package/dist/mail-profile/mail-profile.service.d.ts +0 -109
  271. package/dist/mail-profile/mail-profile.service.d.ts.map +0 -1
  272. package/dist/mail-profile/mail-profile.service.js +0 -278
  273. package/dist/mail-profile/mail-profile.service.js.map +0 -1
  274. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +0 -32
  275. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +0 -1
  276. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +0 -178
  277. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +0 -1
  278. package/hedhog/data/mail_profile.yaml +0 -18
  279. package/hedhog/table/mail_profile.yaml +0 -75
  280. package/src/mail-profile/dto/create.dto.ts +0 -145
  281. package/src/mail-profile/mail-profile.controller.ts +0 -56
  282. package/src/mail-profile/mail-profile.module.ts +0 -18
  283. package/src/mail-profile/mail-profile.service.ts +0 -308
  284. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +0 -128
  285. /package/dist/{mail-profile → integration-profile}/dto/update.dto.d.ts +0 -0
  286. /package/dist/{mail-profile → integration-profile}/dto/update.dto.js +0 -0
  287. /package/src/{mail-profile → integration-profile}/dto/update.dto.ts +0 -0
@@ -8,9 +8,13 @@ import {
8
8
  Inject,
9
9
  Injectable,
10
10
  Logger,
11
- NotFoundException
11
+ NotFoundException,
12
+ OnModuleDestroy,
13
+ OnModuleInit,
12
14
  } from '@nestjs/common';
15
+ import { randomUUID } from 'node:crypto';
13
16
  import * as pako from 'pako';
17
+ import { createClient, type RedisClientType } from 'redis';
14
18
  import { IntegrationDeveloperApiService } from '../integration/services/integration-developer-api.service';
15
19
  import { CreateDTO } from './dto/create.dto';
16
20
  import { DeleteDTO } from './dto/delete.dto';
@@ -27,11 +31,34 @@ type SettingChangeEventInput = {
27
31
  source: string;
28
32
  };
29
33
 
34
+ type SettingsVersionMessage = {
35
+ originInstanceId: string;
36
+ version: number;
37
+ slugs: string[];
38
+ };
39
+
30
40
  @Injectable()
31
- export class SettingService {
41
+ export class SettingService implements OnModuleInit, OnModuleDestroy {
32
42
 
33
43
  private readonly logger = new Logger(SettingService.name);
34
44
  private cachedSettings: Record<string, any> = {};
45
+ private readonly instanceId = randomUUID();
46
+ private readonly redisChannel = 'core:settings:version-changed';
47
+ private readonly redisUrl = String(process.env.REDIS_URL || '').trim();
48
+ private readonly settingsVersionCheckIntervalMs = Math.max(
49
+ 1000,
50
+ Number(process.env.SETTINGS_VERSION_CHECK_INTERVAL_MS || 5000),
51
+ );
52
+ private localSettingsVersion = 0;
53
+ private lastSettingsVersionCheckAt = 0;
54
+ private redisPublisher: RedisClientType | null = null;
55
+ private redisSubscriber: RedisClientType | null = null;
56
+ private readonly redisErrorLogCooldownMs = Math.max(
57
+ 1000,
58
+ Number(process.env.REDIS_SETTINGS_ERROR_LOG_COOLDOWN_MS || 30000),
59
+ );
60
+ private lastRedisSubscriberErrorLogAt = 0;
61
+ private lastRedisPublisherErrorLogAt = 0;
35
62
 
36
63
  constructor(
37
64
  @Inject(forwardRef(() => PrismaService))
@@ -44,6 +71,231 @@ export class SettingService {
44
71
  private readonly integrationApi: IntegrationDeveloperApiService,
45
72
  ) { }
46
73
 
74
+ async onModuleInit() {
75
+ await this.syncSettingsVersionFromDatabase();
76
+ await this.initializeRedisSync();
77
+ }
78
+
79
+ async onModuleDestroy() {
80
+ await Promise.allSettled([
81
+ this.safeCloseRedisClient(this.redisSubscriber),
82
+ this.safeCloseRedisClient(this.redisPublisher),
83
+ ]);
84
+ }
85
+
86
+ private async safeCloseRedisClient(client: RedisClientType | null) {
87
+ if (!client || !client.isOpen) {
88
+ return;
89
+ }
90
+
91
+ try {
92
+ await client.quit();
93
+ } catch {
94
+ try {
95
+ client.disconnect();
96
+ } catch {
97
+ // ignore shutdown errors
98
+ }
99
+ }
100
+ }
101
+
102
+ private logRedisClientError(kind: 'subscriber' | 'publisher', error: unknown) {
103
+ const now = Date.now();
104
+ const lastLogAt =
105
+ kind === 'subscriber'
106
+ ? this.lastRedisSubscriberErrorLogAt
107
+ : this.lastRedisPublisherErrorLogAt;
108
+
109
+ if (now - lastLogAt < this.redisErrorLogCooldownMs) {
110
+ return;
111
+ }
112
+
113
+ if (kind === 'subscriber') {
114
+ this.lastRedisSubscriberErrorLogAt = now;
115
+ } else {
116
+ this.lastRedisPublisherErrorLogAt = now;
117
+ }
118
+
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ this.logger.warn(`Redis settings ${kind} error: ${message}`);
121
+ }
122
+
123
+ private async initializeRedisSync() {
124
+ if (!this.redisUrl) {
125
+ this.logger.log(
126
+ 'Redis settings sync disabled because REDIS_URL is not configured.',
127
+ );
128
+ return;
129
+ }
130
+
131
+ try {
132
+ this.redisSubscriber = createClient({
133
+ url: this.redisUrl,
134
+ socket: {
135
+ reconnectStrategy: (retries) => Math.min(retries * 250, 5000),
136
+ },
137
+ });
138
+ this.redisPublisher = createClient({
139
+ url: this.redisUrl,
140
+ socket: {
141
+ reconnectStrategy: (retries) => Math.min(retries * 250, 5000),
142
+ },
143
+ });
144
+
145
+ this.redisSubscriber.on('error', (error) => {
146
+ this.logRedisClientError('subscriber', error);
147
+ });
148
+ this.redisPublisher.on('error', (error) => {
149
+ this.logRedisClientError('publisher', error);
150
+ });
151
+
152
+ await this.redisSubscriber.connect();
153
+ await this.redisPublisher.connect();
154
+ await this.redisSubscriber.subscribe(this.redisChannel, async (message) => {
155
+ await this.handleRedisSettingsVersionMessage(message);
156
+ });
157
+
158
+ this.logger.log(
159
+ `Redis settings sync enabled on channel ${this.redisChannel}`,
160
+ );
161
+ } catch (error) {
162
+ this.logger.warn(
163
+ `Failed to initialize Redis settings sync: ${error instanceof Error ? error.message : String(error)}`,
164
+ );
165
+
166
+ await Promise.allSettled([
167
+ this.safeCloseRedisClient(this.redisSubscriber),
168
+ this.safeCloseRedisClient(this.redisPublisher),
169
+ ]);
170
+
171
+ this.redisSubscriber = null;
172
+ this.redisPublisher = null;
173
+ }
174
+ }
175
+
176
+ private async handleRedisSettingsVersionMessage(message: string) {
177
+ let payload: SettingsVersionMessage;
178
+
179
+ try {
180
+ payload = JSON.parse(message) as SettingsVersionMessage;
181
+ } catch {
182
+ return;
183
+ }
184
+
185
+ if (!payload || payload.originInstanceId === this.instanceId) {
186
+ return;
187
+ }
188
+
189
+ if (!Number.isFinite(payload.version) || payload.version <= this.localSettingsVersion) {
190
+ return;
191
+ }
192
+
193
+ this.localSettingsVersion = payload.version;
194
+ this.lastSettingsVersionCheckAt = Date.now();
195
+ this.clearCache();
196
+ }
197
+
198
+ private async ensureSettingsVersionState() {
199
+ await this.prismaService.$executeRawUnsafe(`
200
+ INSERT INTO "setting_runtime_state" ("scope", "version", "updated_at")
201
+ VALUES ('global', 0, NOW())
202
+ ON CONFLICT ("scope") DO NOTHING
203
+ `);
204
+ }
205
+
206
+ private parseVersionValue(value: unknown): number {
207
+ if (typeof value === 'number') {
208
+ return value;
209
+ }
210
+
211
+ if (typeof value === 'bigint') {
212
+ return Number(value);
213
+ }
214
+
215
+ if (typeof value === 'string') {
216
+ return Number(value);
217
+ }
218
+
219
+ return 0;
220
+ }
221
+
222
+ private async getSettingsVersionFromDatabase(): Promise<number> {
223
+ await this.ensureSettingsVersionState();
224
+
225
+ const rows = await this.prismaService.$queryRawUnsafe<Array<{ version: unknown }>>(`
226
+ SELECT "version"
227
+ FROM "setting_runtime_state"
228
+ WHERE "scope" = 'global'
229
+ LIMIT 1
230
+ `);
231
+
232
+ return this.parseVersionValue(rows[0]?.version);
233
+ }
234
+
235
+ private async syncSettingsVersionFromDatabase() {
236
+ this.localSettingsVersion = await this.getSettingsVersionFromDatabase();
237
+ this.lastSettingsVersionCheckAt = Date.now();
238
+ }
239
+
240
+ private async bumpSettingsVersion(): Promise<number> {
241
+ await this.ensureSettingsVersionState();
242
+
243
+ const rows = await this.prismaService.$queryRawUnsafe<Array<{ version: unknown }>>(`
244
+ UPDATE "setting_runtime_state"
245
+ SET "version" = "version" + 1,
246
+ "updated_at" = NOW()
247
+ WHERE "scope" = 'global'
248
+ RETURNING "version"
249
+ `);
250
+
251
+ const nextVersion = this.parseVersionValue(rows[0]?.version);
252
+ this.localSettingsVersion = nextVersion;
253
+ this.lastSettingsVersionCheckAt = Date.now();
254
+ return nextVersion;
255
+ }
256
+
257
+ private async ensureCacheVersionFresh() {
258
+ const now = Date.now();
259
+ if (now - this.lastSettingsVersionCheckAt < this.settingsVersionCheckIntervalMs) {
260
+ return;
261
+ }
262
+
263
+ const currentVersion = await this.getSettingsVersionFromDatabase();
264
+ this.lastSettingsVersionCheckAt = now;
265
+
266
+ if (currentVersion > this.localSettingsVersion) {
267
+ this.localSettingsVersion = currentVersion;
268
+ this.clearCache();
269
+ }
270
+ }
271
+
272
+ private async publishSettingsVersionChange(slugs: string[]) {
273
+ const filteredSlugs = Array.from(new Set(slugs.filter(Boolean)));
274
+ if (filteredSlugs.length === 0) {
275
+ return;
276
+ }
277
+
278
+ const nextVersion = await this.bumpSettingsVersion();
279
+
280
+ if (!this.redisPublisher?.isOpen) {
281
+ return;
282
+ }
283
+
284
+ const payload: SettingsVersionMessage = {
285
+ originInstanceId: this.instanceId,
286
+ version: nextVersion,
287
+ slugs: filteredSlugs,
288
+ };
289
+
290
+ try {
291
+ await this.redisPublisher.publish(this.redisChannel, JSON.stringify(payload));
292
+ } catch (error) {
293
+ this.logger.warn(
294
+ `Failed to publish settings version change: ${error instanceof Error ? error.message : String(error)}`,
295
+ );
296
+ }
297
+ }
298
+
47
299
  async exportSettings(includeSecrets = false): Promise<Buffer> {
48
300
  const settings = await this.prismaService.setting.findMany({
49
301
  where: {
@@ -158,6 +410,7 @@ export class SettingService {
158
410
  if (transaction.length > 0) {
159
411
  await this.prismaService.$transaction(transaction);
160
412
  await this.emitSettingChangedEvents(changedSettings);
413
+ await this.publishSettingsVersionChange(changedSettings.map((item) => item.slug));
161
414
  }
162
415
 
163
416
  return {
@@ -206,14 +459,19 @@ export class SettingService {
206
459
  'password-min-uppercase',
207
460
  'password-min-length',
208
461
  'mfa-email-code-length',
209
- 'ai-openai-api-key-enabled',
210
- 'ai-gemini-api-key-enabled',
211
462
  'mcp-enabled',
212
463
  'date-format',
213
464
  'time-format',
214
465
  'timezone',
215
466
  'menu-width',
467
+ 'maintenance-mode-enabled',
216
468
  'providers',
469
+ 'oauth-facebook-enabled',
470
+ 'oauth-github-enabled',
471
+ 'oauth-google-enabled',
472
+ 'oauth-microsoft-enabled',
473
+ 'oauth-microsoft-entra-id-enabled',
474
+ 'admin-login-page-oauth-providers',
217
475
  'disable-authentication-with-email-and-password',
218
476
  'contact-allow-company-registration',
219
477
  'operations.comment-edit-window',
@@ -313,6 +571,7 @@ export class SettingService {
313
571
 
314
572
  await this.prismaService.$transaction(transaction);
315
573
  await this.emitSettingChangedEvents(changedSettings);
574
+ await this.publishSettingsVersionChange(changedSettings.map((item) => item.slug));
316
575
  return { success: true };
317
576
  }
318
577
 
@@ -657,6 +916,8 @@ export class SettingService {
657
916
  },
658
917
  ]);
659
918
 
919
+ await this.publishSettingsVersionChange([createdSetting.slug]);
920
+
660
921
  return createdSetting;
661
922
  }
662
923
 
@@ -728,6 +989,8 @@ export class SettingService {
728
989
  },
729
990
  ]);
730
991
 
992
+ await this.publishSettingsVersionChange([updatedSetting.slug]);
993
+
731
994
  // Garantir que o value sobrescreve o valor original
732
995
  const result = { ...updatedSetting };
733
996
  result.value = this.getValueFormattedByType(updatedSetting.type, updatedSetting.value);
@@ -772,6 +1035,8 @@ export class SettingService {
772
1035
  },
773
1036
  ]);
774
1037
 
1038
+ await this.publishSettingsVersionChange([updatedSetting.slug]);
1039
+
775
1040
  const result = { ...updatedSetting };
776
1041
  result.value = this.getValueFormattedByType(updatedSetting.type, updatedSetting.value);
777
1042
  return result;
@@ -857,8 +1122,18 @@ export class SettingService {
857
1122
  switch (type) {
858
1123
  case 'boolean':
859
1124
  return value === true || value === 'true' ? 'true' : 'false';
860
- case 'number':
861
- return Number(value).toString();
1125
+ case 'number': {
1126
+ if (value === undefined || value === null || String(value).trim() === '') {
1127
+ return '';
1128
+ }
1129
+
1130
+ const numericValue = Number(value);
1131
+ if (!Number.isFinite(numericValue)) {
1132
+ throw new BadRequestException(`Invalid numeric value for setting: ${value}`);
1133
+ }
1134
+
1135
+ return numericValue.toString();
1136
+ }
862
1137
  case 'array':
863
1138
  case 'json':
864
1139
  return typeof value === 'string' ? value : JSON.stringify(value);
@@ -871,8 +1146,18 @@ export class SettingService {
871
1146
  switch (type) {
872
1147
  case 'boolean':
873
1148
  return value === true || value === 'true' ? 'true' : 'false';
874
- case 'number':
875
- return Number(value).toString();
1149
+ case 'number': {
1150
+ if (value === undefined || value === null || String(value).trim() === '') {
1151
+ return '';
1152
+ }
1153
+
1154
+ const numericValue = Number(value);
1155
+ if (!Number.isFinite(numericValue)) {
1156
+ throw new BadRequestException(`Invalid numeric value for setting: ${value}`);
1157
+ }
1158
+
1159
+ return numericValue.toString();
1160
+ }
876
1161
  case 'array':
877
1162
  case 'json':
878
1163
  return typeof value === 'string' ? value : JSON.stringify(value);
@@ -897,6 +1182,7 @@ export class SettingService {
897
1182
  },
898
1183
  select: {
899
1184
  id: true,
1185
+ slug: true,
900
1186
  },
901
1187
  });
902
1188
 
@@ -906,13 +1192,19 @@ export class SettingService {
906
1192
  );
907
1193
  }
908
1194
 
909
- return this.prismaService.setting.deleteMany({
1195
+ const deleted = await this.prismaService.setting.deleteMany({
910
1196
  where: {
911
1197
  id: {
912
1198
  in: ids,
913
1199
  },
914
1200
  },
915
1201
  });
1202
+
1203
+ if (deleted.count > 0) {
1204
+ await this.publishSettingsVersionChange(existingSettings.map((item) => item.slug));
1205
+ }
1206
+
1207
+ return deleted;
916
1208
  }
917
1209
 
918
1210
  async setSettingUserValue(locale:string, user_id: number, slug: string, value: string) {
@@ -972,6 +1264,10 @@ export class SettingService {
972
1264
  ): Promise<Record<string, any>> {
973
1265
 
974
1266
  const slugs = Array.isArray(slug) ? slug : [slug];
1267
+ if (userId === undefined) {
1268
+ await this.ensureCacheVersionFresh();
1269
+ }
1270
+
975
1271
  if (
976
1272
  userId === undefined &&
977
1273
  this.cachedSettings &&
@@ -1144,6 +1440,8 @@ export class SettingService {
1144
1440
  },
1145
1441
  ]);
1146
1442
 
1443
+ await this.publishSettingsVersionChange([currentSetting.slug]);
1444
+
1147
1445
  return result;
1148
1446
  }
1149
1447
  }
@@ -12,12 +12,14 @@ import {
12
12
  ParseIntPipe,
13
13
  Patch,
14
14
  Post,
15
+ Req,
15
16
  Res,
16
17
  UploadedFile,
17
18
  UseInterceptors,
18
19
  forwardRef,
19
20
  } from '@nestjs/common';
20
21
  import { FileInterceptor } from '@nestjs/platform-express';
22
+ import { Request } from 'express';
21
23
  import { DeleteDTO } from '../dto/delete.dto';
22
24
  import { CreateWithEmailAndPasswordDTO } from './dto/create-with-email-and-password.dto';
23
25
  import { ResetPasswordDTO } from './dto/reset-password.dto';
@@ -47,9 +49,10 @@ export class UserController {
47
49
  async openPublicAvatar(
48
50
  @Param('fileId', ParseIntPipe) fileId: number,
49
51
  @Locale() locale: string,
52
+ @Req() req: Request,
50
53
  @Res() res,
51
54
  ) {
52
- return this.userService.openPublicAvatar(locale, fileId, res);
55
+ return this.userService.openPublicAvatar(locale, fileId, req.headers['if-none-match'] as string | undefined, res);
53
56
  }
54
57
 
55
58
  @Post()
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'crypto';
1
2
  import { getLocaleText } from '@hed-hog/api-locale';
2
3
  import { PaginationService } from '@hed-hog/api-pagination';
3
4
  import { PrismaService } from '@hed-hog/api-prisma';
@@ -13,6 +14,7 @@ import { DeleteDTO } from '../dto/delete.dto';
13
14
  import { FileService } from '../file/file.service';
14
15
  import { IntegrationDeveloperApiService } from '../integration/services/integration-developer-api.service';
15
16
  import { SecurityService } from '../security/security.service';
17
+ import { SettingService } from '../setting/setting.service';
16
18
  import { USER_AVATAR_UPLOAD_DESTINATION } from './constants/user.constants';
17
19
  import { CreateWithEmailAndPasswordDTO } from './dto/create-with-email-and-password.dto';
18
20
  import { ResetPasswordDTO } from './dto/reset-password.dto';
@@ -27,6 +29,7 @@ const CREDENTIAL_TYPE = {
27
29
  PASSWORD: 'password',
28
30
  } as const;
29
31
 
32
+ const USER_AVATAR_CACHE_MAX_AGE_SETTING = 'user-avatar-cache-max-age';
30
33
  const DEFAULT_ROLE_SLUG = 'user';
31
34
  const DEFAULT_LOCALE = 'en';
32
35
  const DAYS_IN_MS = 24 * 60 * 60 * 1000;
@@ -68,6 +71,8 @@ export class UserService {
68
71
  private readonly file: FileService,
69
72
  @Inject(forwardRef(() => IntegrationDeveloperApiService))
70
73
  private readonly integrationApi: IntegrationDeveloperApiService,
74
+ @Inject(forwardRef(() => SettingService))
75
+ private readonly settingService: SettingService,
71
76
  ) { }
72
77
 
73
78
  // ==========================================
@@ -319,7 +324,7 @@ export class UserService {
319
324
  };
320
325
  }
321
326
 
322
- async openPublicAvatar(locale: string, fileId: number, res: any) {
327
+ async openPublicAvatar(locale: string, fileId: number, ifNoneMatch: string | undefined, res: any) {
323
328
  const userWithAvatar = await this.prismaService.user.findFirst({
324
329
  where: {
325
330
  photo_id: fileId,
@@ -337,10 +342,23 @@ export class UserService {
337
342
 
338
343
  const { file, buffer } = await this.file.getBuffer(fileId);
339
344
 
345
+ const etag = `"${createHash('md5').update(buffer).digest('hex')}"`;
346
+
347
+ if (ifNoneMatch === etag) {
348
+ res.status(304).send();
349
+ return;
350
+ }
351
+
352
+ const settings = await this.settingService.getSettingValues(
353
+ USER_AVATAR_CACHE_MAX_AGE_SETTING,
354
+ );
355
+ const maxAge = Number(settings[USER_AVATAR_CACHE_MAX_AGE_SETTING] ?? 3600);
356
+
340
357
  res.set({
341
358
  'Content-Type': file.file_mimetype.name,
342
359
  'Content-Length': buffer.length,
343
- 'Cache-Control': 'public, max-age=3600',
360
+ 'Cache-Control': `public, max-age=${maxAge}`,
361
+ 'ETag': etag,
344
362
  });
345
363
 
346
364
  res.send(buffer);
@@ -1,6 +1,6 @@
1
1
  import { Transform } from 'class-transformer';
2
2
  import { PartialType } from '@nestjs/mapped-types';
3
- import { IsIn, IsInt, IsOptional, IsString } from 'class-validator';
3
+ import { IsIn, IsInt, IsObject, IsOptional, IsString } from 'class-validator';
4
4
 
5
5
  const toNullableInteger = ({ value }: { value: unknown }) => {
6
6
  if (value === undefined || value === null || value === '') {
@@ -19,8 +19,19 @@ const toOptionalTrimmedString = ({ value }: { value: unknown }) => {
19
19
  };
20
20
 
21
21
  export class CreateWebhookActionDTO {
22
- @IsIn(['email', 'whatsapp_evolution'])
23
- type: 'email' | 'whatsapp_evolution';
22
+ @IsIn([
23
+ 'email',
24
+ 'whatsapp_evolution',
25
+ 'http_request',
26
+ 'internal_api',
27
+ 'app_command',
28
+ ])
29
+ type:
30
+ | 'email'
31
+ | 'whatsapp_evolution'
32
+ | 'http_request'
33
+ | 'internal_api'
34
+ | 'app_command';
24
35
 
25
36
  @IsOptional()
26
37
  @Transform(toOptionalTrimmedString)
@@ -39,7 +50,7 @@ export class CreateWebhookActionDTO {
39
50
  @IsOptional()
40
51
  @Transform(toNullableInteger)
41
52
  @IsInt()
42
- mail_profile_id?: number;
53
+ integration_profile_id?: number;
43
54
 
44
55
  @IsOptional()
45
56
  @Transform(toNullableInteger)
@@ -89,6 +100,76 @@ export class CreateWebhookActionDTO {
89
100
  @Transform(toOptionalTrimmedString)
90
101
  @IsString()
91
102
  whatsapp_token?: string;
103
+
104
+ @IsOptional()
105
+ @Transform(toOptionalTrimmedString)
106
+ @IsString()
107
+ http_url?: string;
108
+
109
+ @IsOptional()
110
+ @Transform(toOptionalTrimmedString)
111
+ @IsString()
112
+ http_method?: string;
113
+
114
+ @IsOptional()
115
+ @IsObject()
116
+ http_headers?: Record<string, string>;
117
+
118
+ @IsOptional()
119
+ @IsObject()
120
+ http_query?: Record<string, string>;
121
+
122
+ @IsOptional()
123
+ @Transform(toOptionalTrimmedString)
124
+ @IsString()
125
+ http_body?: string;
126
+
127
+ @IsOptional()
128
+ @Transform(toNullableInteger)
129
+ @IsInt()
130
+ http_timeout_ms?: number;
131
+
132
+ @IsOptional()
133
+ @Transform(toNullableInteger)
134
+ @IsInt()
135
+ http_retry_count?: number;
136
+
137
+ @IsOptional()
138
+ @Transform(toOptionalTrimmedString)
139
+ @IsString()
140
+ internal_api_path?: string;
141
+
142
+ @IsOptional()
143
+ @Transform(toOptionalTrimmedString)
144
+ @IsString()
145
+ internal_api_method?: string;
146
+
147
+ @IsOptional()
148
+ @IsObject()
149
+ internal_api_query?: Record<string, unknown>;
150
+
151
+ @IsOptional()
152
+ @IsObject()
153
+ internal_api_body?: Record<string, unknown>;
154
+
155
+ @IsOptional()
156
+ @Transform(toOptionalTrimmedString)
157
+ @IsString()
158
+ internal_api_token?: string;
159
+
160
+ @IsOptional()
161
+ @Transform(toNullableInteger)
162
+ @IsInt()
163
+ internal_api_user_id?: number;
164
+
165
+ @IsOptional()
166
+ @Transform(toOptionalTrimmedString)
167
+ @IsString()
168
+ app_command_slug?: string;
169
+
170
+ @IsOptional()
171
+ @IsObject()
172
+ app_command_params?: Record<string, unknown>;
92
173
  }
93
174
 
94
175
  export class UpdateWebhookActionDTO extends PartialType(