@hed-hog/core 0.0.356 → 0.0.359

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 (250) hide show
  1. package/dist/auth/guards/auth.guard.d.ts +6 -1
  2. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  3. package/dist/auth/guards/auth.guard.js +33 -2
  4. package/dist/auth/guards/auth.guard.js.map +1 -1
  5. package/dist/file/file.controller.d.ts +4 -2
  6. package/dist/file/file.controller.d.ts.map +1 -1
  7. package/dist/file/file.controller.js +44 -38
  8. package/dist/file/file.controller.js.map +1 -1
  9. package/dist/file/file.service.d.ts +2 -2
  10. package/dist/file/file.service.d.ts.map +1 -1
  11. package/dist/file/file.service.js +25 -42
  12. package/dist/file/file.service.js.map +1 -1
  13. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  14. package/dist/file/provider/azure.provider.js +10 -15
  15. package/dist/file/provider/azure.provider.js.map +1 -1
  16. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  17. package/dist/file/provider/gcs.provider.js +8 -9
  18. package/dist/file/provider/gcs.provider.js.map +1 -1
  19. package/dist/file/provider/local.provider.d.ts.map +1 -1
  20. package/dist/file/provider/local.provider.js +11 -10
  21. package/dist/file/provider/local.provider.js.map +1 -1
  22. package/dist/file/provider/provider.factory.d.ts +1 -1
  23. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  24. package/dist/file/provider/provider.factory.js +16 -6
  25. package/dist/file/provider/provider.factory.js.map +1 -1
  26. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  27. package/dist/file/provider/s3.provider.js +4 -20
  28. package/dist/file/provider/s3.provider.js.map +1 -1
  29. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  30. package/dist/file/provider/spaces.provider.js +4 -19
  31. package/dist/file/provider/spaces.provider.js.map +1 -1
  32. package/dist/integration-profile/integration-profile.controller.d.ts +1 -1
  33. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  34. package/dist/integration-profile/integration-profile.controller.js +4 -3
  35. package/dist/integration-profile/integration-profile.controller.js.map +1 -1
  36. package/dist/integration-profile/integration-profile.service.d.ts +1 -1
  37. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  38. package/dist/integration-profile/integration-profile.service.js +7 -4
  39. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  40. package/dist/integration-profile/integration-profile.utils.d.ts +6 -0
  41. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  42. package/dist/integration-profile/integration-profile.utils.js +9 -0
  43. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  44. package/dist/mail/mail.module.d.ts.map +1 -1
  45. package/dist/mail/mail.module.js +9 -4
  46. package/dist/mail/mail.module.js.map +1 -1
  47. package/dist/mail/mail.service.d.ts +2 -1
  48. package/dist/mail/mail.service.d.ts.map +1 -1
  49. package/dist/mail/mail.service.js +11 -7
  50. package/dist/mail/mail.service.js.map +1 -1
  51. package/dist/mcp/guards/mcp-auth.guard.js +1 -1
  52. package/dist/mcp/guards/mcp-auth.guard.js.map +1 -1
  53. package/dist/mcp/mcp-api-key.controller.d.ts +2 -0
  54. package/dist/mcp/mcp-api-key.controller.d.ts.map +1 -1
  55. package/dist/mcp/mcp-api-key.controller.js +1 -1
  56. package/dist/mcp/mcp-api-key.controller.js.map +1 -1
  57. package/dist/mcp/mcp-api-key.service.d.ts +2 -1
  58. package/dist/mcp/mcp-api-key.service.d.ts.map +1 -1
  59. package/dist/mcp/mcp-api-key.service.js +6 -2
  60. package/dist/mcp/mcp-api-key.service.js.map +1 -1
  61. package/dist/mcp/mcp.module.d.ts.map +1 -1
  62. package/dist/mcp/mcp.module.js +1 -3
  63. package/dist/mcp/mcp.module.js.map +1 -1
  64. package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
  65. package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
  66. package/dist/oauth/oauth.controller.d.ts +4 -1
  67. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  68. package/dist/oauth/oauth.controller.js +31 -5
  69. package/dist/oauth/oauth.controller.js.map +1 -1
  70. package/dist/oauth/oauth.service.d.ts +12 -1
  71. package/dist/oauth/oauth.service.d.ts.map +1 -1
  72. package/dist/oauth/oauth.service.js +125 -23
  73. package/dist/oauth/oauth.service.js.map +1 -1
  74. package/dist/oauth/providers/abstract.provider.d.ts +1 -1
  75. package/dist/oauth/providers/abstract.provider.d.ts.map +1 -1
  76. package/dist/oauth/providers/facebook.provider.d.ts +5 -2
  77. package/dist/oauth/providers/facebook.provider.d.ts.map +1 -1
  78. package/dist/oauth/providers/facebook.provider.js +34 -19
  79. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  80. package/dist/oauth/providers/github.provider.d.ts +6 -2
  81. package/dist/oauth/providers/github.provider.d.ts.map +1 -1
  82. package/dist/oauth/providers/github.provider.js +43 -20
  83. package/dist/oauth/providers/github.provider.js.map +1 -1
  84. package/dist/oauth/providers/google.provider.d.ts +5 -2
  85. package/dist/oauth/providers/google.provider.d.ts.map +1 -1
  86. package/dist/oauth/providers/google.provider.js +34 -19
  87. package/dist/oauth/providers/google.provider.js.map +1 -1
  88. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -2
  89. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  90. package/dist/oauth/providers/microsoft-entra-id.provider.js +43 -40
  91. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  92. package/dist/oauth/providers/microsoft.provider.d.ts +5 -2
  93. package/dist/oauth/providers/microsoft.provider.d.ts.map +1 -1
  94. package/dist/oauth/providers/microsoft.provider.js +34 -19
  95. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  96. package/dist/profile/profile.controller.js +1 -1
  97. package/dist/profile/profile.controller.js.map +1 -1
  98. package/dist/security/security.service.d.ts +2 -0
  99. package/dist/security/security.service.d.ts.map +1 -1
  100. package/dist/security/security.service.js +21 -0
  101. package/dist/security/security.service.js.map +1 -1
  102. package/dist/setting/setting.service.d.ts +26 -1
  103. package/dist/setting/setting.service.d.ts.map +1 -1
  104. package/dist/setting/setting.service.js +240 -5
  105. package/dist/setting/setting.service.js.map +1 -1
  106. package/dist/user/user.controller.d.ts +2 -1
  107. package/dist/user/user.controller.d.ts.map +1 -1
  108. package/dist/user/user.controller.js +5 -4
  109. package/dist/user/user.controller.js.map +1 -1
  110. package/dist/user/user.service.d.ts +4 -2
  111. package/dist/user/user.service.d.ts.map +1 -1
  112. package/dist/user/user.service.js +19 -4
  113. package/dist/user/user.service.js.map +1 -1
  114. package/dist/webhook-integration/dto/webhook-action.dto.d.ts +17 -2
  115. package/dist/webhook-integration/dto/webhook-action.dto.d.ts.map +1 -1
  116. package/dist/webhook-integration/dto/webhook-action.dto.js +93 -2
  117. package/dist/webhook-integration/dto/webhook-action.dto.js.map +1 -1
  118. package/dist/webhook-integration/webhook-command.registry.d.ts +15 -0
  119. package/dist/webhook-integration/webhook-command.registry.d.ts.map +1 -0
  120. package/dist/webhook-integration/webhook-command.registry.js +36 -0
  121. package/dist/webhook-integration/webhook-command.registry.js.map +1 -0
  122. package/dist/webhook-integration/webhook-integration.controller.d.ts +19 -0
  123. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  124. package/dist/webhook-integration/webhook-integration.controller.js +38 -0
  125. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  126. package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
  127. package/dist/webhook-integration/webhook-integration.module.js +5 -0
  128. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  129. package/dist/webhook-integration/webhook-integration.service.d.ts +25 -1
  130. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  131. package/dist/webhook-integration/webhook-integration.service.js +332 -23
  132. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  133. package/hedhog/data/integration_provider.yaml +16 -0
  134. package/hedhog/data/menu.yaml +0 -16
  135. package/hedhog/data/route.yaml +32 -383
  136. package/hedhog/data/setting_group.yaml +179 -174
  137. package/hedhog/data/setting_subgroup.yaml +16 -0
  138. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +421 -0
  139. package/hedhog/frontend/app/account/layout.tsx.ejs +6 -3
  140. package/hedhog/frontend/app/account/mcp-tokens/page.tsx.ejs +2 -2
  141. package/hedhog/frontend/app/account/tokens/page.tsx.ejs +5 -0
  142. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +414 -76
  143. package/hedhog/frontend/app/configurations/layout.tsx.ejs +1 -1
  144. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +3 -1
  145. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +186 -18
  146. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +597 -21
  147. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +2 -2
  148. package/hedhog/frontend/app/setting-list/page.tsx.ejs +229 -395
  149. package/hedhog/frontend/messages/en.json +67 -3
  150. package/hedhog/frontend/messages/pt.json +67 -3
  151. package/hedhog/table/file.yaml +1 -1
  152. package/hedhog/table/mcp_api_key.yaml +4 -0
  153. package/hedhog/table/webhook_action.yaml +57 -1
  154. package/package.json +5 -4
  155. package/src/auth/guards/auth.guard.ts +32 -0
  156. package/src/file/file.controller.ts +51 -42
  157. package/src/file/file.service.ts +31 -52
  158. package/src/file/provider/azure.provider.ts +14 -28
  159. package/src/file/provider/gcs.provider.ts +9 -9
  160. package/src/file/provider/local.provider.ts +10 -13
  161. package/src/file/provider/provider.factory.ts +19 -7
  162. package/src/file/provider/s3.provider.ts +4 -20
  163. package/src/file/provider/spaces.provider.ts +4 -19
  164. package/src/integration-profile/integration-profile.controller.ts +2 -0
  165. package/src/integration-profile/integration-profile.service.ts +7 -2
  166. package/src/integration-profile/integration-profile.utils.ts +17 -0
  167. package/src/mail/mail.module.ts +14 -4
  168. package/src/mail/mail.service.ts +12 -7
  169. package/src/mcp/guards/mcp-auth.guard.ts +1 -1
  170. package/src/mcp/mcp-api-key.controller.ts +2 -2
  171. package/src/mcp/mcp-api-key.service.ts +6 -2
  172. package/src/mcp/mcp.module.ts +1 -3
  173. package/src/oauth/interfaces/OAuthProvider.ts +1 -1
  174. package/src/oauth/oauth.controller.ts +30 -2
  175. package/src/oauth/oauth.service.ts +194 -35
  176. package/src/oauth/providers/abstract.provider.ts +1 -1
  177. package/src/oauth/providers/facebook.provider.ts +36 -20
  178. package/src/oauth/providers/github.provider.ts +46 -23
  179. package/src/oauth/providers/google.provider.ts +36 -20
  180. package/src/oauth/providers/microsoft-entra-id.provider.ts +42 -46
  181. package/src/oauth/providers/microsoft.provider.ts +36 -21
  182. package/src/profile/profile.controller.ts +2 -2
  183. package/src/security/security.service.ts +34 -1
  184. package/src/setting/setting.service.ts +307 -7
  185. package/src/user/user.controller.ts +4 -1
  186. package/src/user/user.service.ts +20 -2
  187. package/src/webhook-integration/dto/webhook-action.dto.ts +85 -4
  188. package/src/webhook-integration/webhook-command.registry.ts +42 -0
  189. package/src/webhook-integration/webhook-integration.controller.ts +20 -0
  190. package/src/webhook-integration/webhook-integration.module.ts +50 -45
  191. package/src/webhook-integration/webhook-integration.service.ts +374 -11
  192. package/dist/mail-profile/dto/create.dto.d.ts +0 -23
  193. package/dist/mail-profile/dto/create.dto.d.ts.map +0 -1
  194. package/dist/mail-profile/dto/create.dto.js +0 -139
  195. package/dist/mail-profile/dto/create.dto.js.map +0 -1
  196. package/dist/mail-profile/dto/update.dto.d.ts +0 -6
  197. package/dist/mail-profile/dto/update.dto.d.ts.map +0 -1
  198. package/dist/mail-profile/dto/update.dto.js +0 -9
  199. package/dist/mail-profile/dto/update.dto.js.map +0 -1
  200. package/dist/mail-profile/mail-profile.controller.d.ts +0 -94
  201. package/dist/mail-profile/mail-profile.controller.d.ts.map +0 -1
  202. package/dist/mail-profile/mail-profile.controller.js +0 -95
  203. package/dist/mail-profile/mail-profile.controller.js.map +0 -1
  204. package/dist/mail-profile/mail-profile.module.d.ts +0 -3
  205. package/dist/mail-profile/mail-profile.module.d.ts.map +0 -1
  206. package/dist/mail-profile/mail-profile.module.js +0 -31
  207. package/dist/mail-profile/mail-profile.module.js.map +0 -1
  208. package/dist/mail-profile/mail-profile.service.d.ts +0 -109
  209. package/dist/mail-profile/mail-profile.service.d.ts.map +0 -1
  210. package/dist/mail-profile/mail-profile.service.js +0 -278
  211. package/dist/mail-profile/mail-profile.service.js.map +0 -1
  212. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +0 -20
  213. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +0 -1
  214. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +0 -96
  215. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +0 -1
  216. package/dist/whatsapp-profile/dto/create.dto.d.ts +0 -11
  217. package/dist/whatsapp-profile/dto/create.dto.d.ts.map +0 -1
  218. package/dist/whatsapp-profile/dto/create.dto.js +0 -55
  219. package/dist/whatsapp-profile/dto/create.dto.js.map +0 -1
  220. package/dist/whatsapp-profile/dto/update.dto.d.ts +0 -6
  221. package/dist/whatsapp-profile/dto/update.dto.d.ts.map +0 -1
  222. package/dist/whatsapp-profile/dto/update.dto.js +0 -9
  223. package/dist/whatsapp-profile/dto/update.dto.js.map +0 -1
  224. package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts +0 -54
  225. package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts.map +0 -1
  226. package/dist/whatsapp-profile/whatsapp-profile.controller.js +0 -85
  227. package/dist/whatsapp-profile/whatsapp-profile.controller.js.map +0 -1
  228. package/dist/whatsapp-profile/whatsapp-profile.module.d.ts +0 -3
  229. package/dist/whatsapp-profile/whatsapp-profile.module.d.ts.map +0 -1
  230. package/dist/whatsapp-profile/whatsapp-profile.module.js +0 -26
  231. package/dist/whatsapp-profile/whatsapp-profile.module.js.map +0 -1
  232. package/dist/whatsapp-profile/whatsapp-profile.service.d.ts +0 -63
  233. package/dist/whatsapp-profile/whatsapp-profile.service.d.ts.map +0 -1
  234. package/dist/whatsapp-profile/whatsapp-profile.service.js +0 -138
  235. package/dist/whatsapp-profile/whatsapp-profile.service.js.map +0 -1
  236. package/hedhog/data/mail_profile.yaml +0 -18
  237. package/hedhog/data/whatsapp_profile.yaml +0 -6
  238. package/hedhog/table/mail_profile.yaml +0 -75
  239. package/hedhog/table/whatsapp_profile.yaml +0 -26
  240. package/src/mail-profile/dto/create.dto.ts +0 -145
  241. package/src/mail-profile/dto/update.dto.ts +0 -4
  242. package/src/mail-profile/mail-profile.controller.ts +0 -56
  243. package/src/mail-profile/mail-profile.module.ts +0 -18
  244. package/src/mail-profile/mail-profile.service.ts +0 -308
  245. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +0 -69
  246. package/src/whatsapp-profile/dto/create.dto.ts +0 -36
  247. package/src/whatsapp-profile/dto/update.dto.ts +0 -4
  248. package/src/whatsapp-profile/whatsapp-profile.controller.ts +0 -51
  249. package/src/whatsapp-profile/whatsapp-profile.module.ts +0 -13
  250. package/src/whatsapp-profile/whatsapp-profile.service.ts +0 -160
@@ -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 {
@@ -211,7 +464,14 @@ export class SettingService {
211
464
  'time-format',
212
465
  'timezone',
213
466
  'menu-width',
467
+ 'maintenance-mode-enabled',
214
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',
215
475
  'disable-authentication-with-email-and-password',
216
476
  'contact-allow-company-registration',
217
477
  'operations.comment-edit-window',
@@ -311,6 +571,7 @@ export class SettingService {
311
571
 
312
572
  await this.prismaService.$transaction(transaction);
313
573
  await this.emitSettingChangedEvents(changedSettings);
574
+ await this.publishSettingsVersionChange(changedSettings.map((item) => item.slug));
314
575
  return { success: true };
315
576
  }
316
577
 
@@ -655,6 +916,8 @@ export class SettingService {
655
916
  },
656
917
  ]);
657
918
 
919
+ await this.publishSettingsVersionChange([createdSetting.slug]);
920
+
658
921
  return createdSetting;
659
922
  }
660
923
 
@@ -726,6 +989,8 @@ export class SettingService {
726
989
  },
727
990
  ]);
728
991
 
992
+ await this.publishSettingsVersionChange([updatedSetting.slug]);
993
+
729
994
  // Garantir que o value sobrescreve o valor original
730
995
  const result = { ...updatedSetting };
731
996
  result.value = this.getValueFormattedByType(updatedSetting.type, updatedSetting.value);
@@ -770,6 +1035,8 @@ export class SettingService {
770
1035
  },
771
1036
  ]);
772
1037
 
1038
+ await this.publishSettingsVersionChange([updatedSetting.slug]);
1039
+
773
1040
  const result = { ...updatedSetting };
774
1041
  result.value = this.getValueFormattedByType(updatedSetting.type, updatedSetting.value);
775
1042
  return result;
@@ -855,8 +1122,18 @@ export class SettingService {
855
1122
  switch (type) {
856
1123
  case 'boolean':
857
1124
  return value === true || value === 'true' ? 'true' : 'false';
858
- case 'number':
859
- 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
+ }
860
1137
  case 'array':
861
1138
  case 'json':
862
1139
  return typeof value === 'string' ? value : JSON.stringify(value);
@@ -869,8 +1146,18 @@ export class SettingService {
869
1146
  switch (type) {
870
1147
  case 'boolean':
871
1148
  return value === true || value === 'true' ? 'true' : 'false';
872
- case 'number':
873
- 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
+ }
874
1161
  case 'array':
875
1162
  case 'json':
876
1163
  return typeof value === 'string' ? value : JSON.stringify(value);
@@ -895,6 +1182,7 @@ export class SettingService {
895
1182
  },
896
1183
  select: {
897
1184
  id: true,
1185
+ slug: true,
898
1186
  },
899
1187
  });
900
1188
 
@@ -904,13 +1192,19 @@ export class SettingService {
904
1192
  );
905
1193
  }
906
1194
 
907
- return this.prismaService.setting.deleteMany({
1195
+ const deleted = await this.prismaService.setting.deleteMany({
908
1196
  where: {
909
1197
  id: {
910
1198
  in: ids,
911
1199
  },
912
1200
  },
913
1201
  });
1202
+
1203
+ if (deleted.count > 0) {
1204
+ await this.publishSettingsVersionChange(existingSettings.map((item) => item.slug));
1205
+ }
1206
+
1207
+ return deleted;
914
1208
  }
915
1209
 
916
1210
  async setSettingUserValue(locale:string, user_id: number, slug: string, value: string) {
@@ -970,6 +1264,10 @@ export class SettingService {
970
1264
  ): Promise<Record<string, any>> {
971
1265
 
972
1266
  const slugs = Array.isArray(slug) ? slug : [slug];
1267
+ if (userId === undefined) {
1268
+ await this.ensureCacheVersionFresh();
1269
+ }
1270
+
973
1271
  if (
974
1272
  userId === undefined &&
975
1273
  this.cachedSettings &&
@@ -1142,6 +1440,8 @@ export class SettingService {
1142
1440
  },
1143
1441
  ]);
1144
1442
 
1443
+ await this.publishSettingsVersionChange([currentSetting.slug]);
1444
+
1145
1445
  return result;
1146
1446
  }
1147
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(
@@ -0,0 +1,42 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ export interface WebhookCommand {
4
+ slug: string;
5
+ name: string;
6
+ description: string;
7
+ inputSchema: Record<string, unknown>;
8
+ handler: (
9
+ params: Record<string, unknown>,
10
+ context?: Record<string, unknown>,
11
+ ) => Promise<unknown>;
12
+ }
13
+
14
+ @Injectable()
15
+ export class WebhookCommandRegistry {
16
+ private readonly commands = new Map<string, WebhookCommand>();
17
+
18
+ register(command: WebhookCommand): void {
19
+ this.commands.set(command.slug, command);
20
+ }
21
+
22
+ find(slug: string): WebhookCommand | undefined {
23
+ return this.commands.get(slug);
24
+ }
25
+
26
+ list(): WebhookCommand[] {
27
+ return Array.from(this.commands.values());
28
+ }
29
+
30
+ async dispatch(
31
+ slug: string,
32
+ params: Record<string, unknown>,
33
+ context?: Record<string, unknown>,
34
+ ): Promise<unknown> {
35
+ const command = this.find(slug);
36
+ if (!command) {
37
+ throw new Error(`Unknown app_command slug: ${slug}`);
38
+ }
39
+
40
+ return command.handler(params, context);
41
+ }
42
+ }
@@ -31,6 +31,26 @@ export class WebhookIntegrationController {
31
31
  return this.service.list(paginationParams);
32
32
  }
33
33
 
34
+ @Get('user-tokens')
35
+ listUserTokens(@Query('userId', ParseIntPipe) userId: number) {
36
+ return this.service.listUserApiTokens(userId);
37
+ }
38
+
39
+ @Post('user-tokens')
40
+ createUserToken(@Body() data: { userId: number; name: string }) {
41
+ return this.service.createUserApiToken(data.userId, data.name);
42
+ }
43
+
44
+ @Get('internal-routes')
45
+ listInternalRoutes() {
46
+ return this.service.listInternalRoutes();
47
+ }
48
+
49
+ @Get('app-commands')
50
+ listAppCommands() {
51
+ return this.service.listAppCommands();
52
+ }
53
+
34
54
  @Get(':id')
35
55
  get(@Param('id', ParseIntPipe) id: number) {
36
56
  return this.service.get(id);