@hed-hog/core 0.0.354 → 0.0.356

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 (167) 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/core.module.d.ts.map +1 -1
  6. package/dist/core.module.js +3 -3
  7. package/dist/core.module.js.map +1 -1
  8. package/dist/file/file.service.d.ts +1 -1
  9. package/dist/file/file.service.d.ts.map +1 -1
  10. package/dist/file/file.service.js +50 -64
  11. package/dist/file/file.service.js.map +1 -1
  12. package/dist/file/provider/provider.enum.d.ts +2 -1
  13. package/dist/file/provider/provider.enum.d.ts.map +1 -1
  14. package/dist/file/provider/provider.enum.js +1 -0
  15. package/dist/file/provider/provider.enum.js.map +1 -1
  16. package/dist/file/provider/provider.factory.d.ts.map +1 -1
  17. package/dist/file/provider/provider.factory.js +3 -0
  18. package/dist/file/provider/provider.factory.js.map +1 -1
  19. package/dist/file/provider/spaces.provider.d.ts +27 -0
  20. package/dist/file/provider/spaces.provider.d.ts.map +1 -0
  21. package/dist/file/provider/spaces.provider.js +218 -0
  22. package/dist/file/provider/spaces.provider.js.map +1 -0
  23. package/dist/index.d.ts +4 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +4 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/integration-profile/dto/create.dto.d.ts +9 -0
  28. package/dist/integration-profile/dto/create.dto.d.ts.map +1 -0
  29. package/dist/integration-profile/dto/create.dto.js +54 -0
  30. package/dist/integration-profile/dto/create.dto.js.map +1 -0
  31. package/dist/integration-profile/dto/export.dto.d.ts +5 -0
  32. package/dist/integration-profile/dto/export.dto.d.ts.map +1 -0
  33. package/dist/integration-profile/dto/export.dto.js +29 -0
  34. package/dist/integration-profile/dto/export.dto.js.map +1 -0
  35. package/dist/integration-profile/dto/import-confirm.dto.d.ts +17 -0
  36. package/dist/integration-profile/dto/import-confirm.dto.d.ts.map +1 -0
  37. package/dist/integration-profile/dto/import-confirm.dto.js +77 -0
  38. package/dist/integration-profile/dto/import-confirm.dto.js.map +1 -0
  39. package/dist/integration-profile/dto/update.dto.d.ts +6 -0
  40. package/dist/integration-profile/dto/update.dto.d.ts.map +1 -0
  41. package/dist/integration-profile/dto/update.dto.js +9 -0
  42. package/dist/integration-profile/dto/update.dto.js.map +1 -0
  43. package/dist/integration-profile/integration-profile.controller.d.ts +142 -0
  44. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -0
  45. package/dist/integration-profile/integration-profile.controller.js +165 -0
  46. package/dist/integration-profile/integration-profile.controller.js.map +1 -0
  47. package/dist/integration-profile/integration-profile.module.d.ts +3 -0
  48. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -0
  49. package/dist/integration-profile/integration-profile.module.js +29 -0
  50. package/dist/integration-profile/integration-profile.module.js.map +1 -0
  51. package/dist/integration-profile/integration-profile.service.d.ts +160 -0
  52. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -0
  53. package/dist/integration-profile/integration-profile.service.js +606 -0
  54. package/dist/integration-profile/integration-profile.service.js.map +1 -0
  55. package/dist/integration-profile/integration-profile.utils.d.ts +24 -0
  56. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -0
  57. package/dist/integration-profile/integration-profile.utils.js +147 -0
  58. package/dist/integration-profile/integration-profile.utils.js.map +1 -0
  59. package/dist/mail/mail.module.d.ts.map +1 -1
  60. package/dist/mail/mail.module.js +27 -19
  61. package/dist/mail/mail.module.js.map +1 -1
  62. package/dist/mail/mail.service.d.ts +3 -2
  63. package/dist/mail/mail.service.d.ts.map +1 -1
  64. package/dist/mail/mail.service.js +25 -47
  65. package/dist/mail/mail.service.js.map +1 -1
  66. package/dist/mcp/mcp.module.js +2 -2
  67. package/dist/mcp/mcp.module.js.map +1 -1
  68. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +3 -15
  69. package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +1 -1
  70. package/dist/mcp/tools/core-mail-profile.mcp-tools.js +12 -94
  71. package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +1 -1
  72. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  73. package/dist/mcp-chat/mcp-chat.service.js +17 -16
  74. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  75. package/dist/setting/setting.service.d.ts.map +1 -1
  76. package/dist/setting/setting.service.js +0 -2
  77. package/dist/setting/setting.service.js.map +1 -1
  78. package/dist/webhook-integration/event-webhook.controller.d.ts +1 -0
  79. package/dist/webhook-integration/event-webhook.controller.d.ts.map +1 -1
  80. package/dist/webhook-integration/event-webhook.controller.js +10 -0
  81. package/dist/webhook-integration/event-webhook.controller.js.map +1 -1
  82. package/dist/webhook-integration/event-webhook.service.d.ts +2 -0
  83. package/dist/webhook-integration/event-webhook.service.d.ts.map +1 -1
  84. package/dist/webhook-integration/event-webhook.service.js +30 -0
  85. package/dist/webhook-integration/event-webhook.service.js.map +1 -1
  86. package/dist/webhook-integration/webhook-integration.controller.d.ts +1 -0
  87. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  88. package/dist/webhook-integration/webhook-integration.controller.js +10 -0
  89. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  90. package/dist/webhook-integration/webhook-integration.service.d.ts +2 -1
  91. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  92. package/dist/webhook-integration/webhook-integration.service.js +62 -44
  93. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  94. package/dist/whatsapp-profile/dto/create.dto.d.ts +11 -0
  95. package/dist/whatsapp-profile/dto/create.dto.d.ts.map +1 -0
  96. package/dist/whatsapp-profile/dto/create.dto.js +55 -0
  97. package/dist/whatsapp-profile/dto/create.dto.js.map +1 -0
  98. package/dist/whatsapp-profile/dto/update.dto.d.ts +6 -0
  99. package/dist/whatsapp-profile/dto/update.dto.d.ts.map +1 -0
  100. package/dist/whatsapp-profile/dto/update.dto.js +9 -0
  101. package/dist/whatsapp-profile/dto/update.dto.js.map +1 -0
  102. package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts +54 -0
  103. package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts.map +1 -0
  104. package/dist/whatsapp-profile/whatsapp-profile.controller.js +85 -0
  105. package/dist/whatsapp-profile/whatsapp-profile.controller.js.map +1 -0
  106. package/dist/whatsapp-profile/whatsapp-profile.module.d.ts +3 -0
  107. package/dist/whatsapp-profile/whatsapp-profile.module.d.ts.map +1 -0
  108. package/dist/whatsapp-profile/whatsapp-profile.module.js +26 -0
  109. package/dist/whatsapp-profile/whatsapp-profile.module.js.map +1 -0
  110. package/dist/whatsapp-profile/whatsapp-profile.service.d.ts +63 -0
  111. package/dist/whatsapp-profile/whatsapp-profile.service.d.ts.map +1 -0
  112. package/dist/whatsapp-profile/whatsapp-profile.service.js +138 -0
  113. package/dist/whatsapp-profile/whatsapp-profile.service.js.map +1 -0
  114. package/hedhog/data/file_provider.yaml +9 -1
  115. package/hedhog/data/integration_provider.yaml +132 -0
  116. package/hedhog/data/integration_type.yaml +29 -0
  117. package/hedhog/data/menu.yaml +15 -14
  118. package/hedhog/data/role.yaml +8 -0
  119. package/hedhog/data/route.yaml +66 -15
  120. package/hedhog/data/setting_group.yaml +30 -191
  121. package/hedhog/data/setting_subgroup.yaml +5 -13
  122. package/hedhog/data/whatsapp_profile.yaml +6 -0
  123. package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +25 -20
  124. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +579 -204
  125. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +1662 -0
  126. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +233 -108
  127. package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +3 -822
  128. package/hedhog/frontend/app/users/permissions.tsx.ejs +79 -2
  129. package/hedhog/frontend/app/users/user-sheet.tsx.ejs +7 -2
  130. package/hedhog/frontend/messages/en.json +172 -0
  131. package/hedhog/frontend/messages/pt.json +172 -0
  132. package/hedhog/table/integration_profile.yaml +34 -0
  133. package/hedhog/table/integration_provider.yaml +23 -0
  134. package/hedhog/table/integration_type.yaml +19 -0
  135. package/hedhog/table/webhook_action.yaml +85 -85
  136. package/hedhog/table/whatsapp_profile.yaml +26 -0
  137. package/package.json +2 -2
  138. package/src/ai/ai.service.ts +35 -153
  139. package/src/core.module.ts +3 -3
  140. package/src/file/file.service.ts +60 -76
  141. package/src/file/provider/provider.enum.ts +1 -0
  142. package/src/file/provider/provider.factory.ts +3 -0
  143. package/src/file/provider/spaces.provider.ts +261 -0
  144. package/src/index.ts +6 -3
  145. package/src/integration-profile/dto/create.dto.ts +44 -0
  146. package/src/integration-profile/dto/export.dto.ts +15 -0
  147. package/src/integration-profile/dto/import-confirm.dto.ts +61 -0
  148. package/src/integration-profile/dto/update.dto.ts +4 -0
  149. package/src/integration-profile/integration-profile.controller.ts +129 -0
  150. package/src/integration-profile/integration-profile.module.ts +16 -0
  151. package/src/integration-profile/integration-profile.service.ts +788 -0
  152. package/src/integration-profile/integration-profile.utils.ts +156 -0
  153. package/src/mail/mail.module.ts +27 -18
  154. package/src/mail/mail.service.ts +31 -60
  155. package/src/mcp/mcp.module.ts +2 -2
  156. package/src/mcp/tools/core-mail-profile.mcp-tools.ts +10 -69
  157. package/src/mcp-chat/mcp-chat.service.ts +23 -20
  158. package/src/setting/setting.service.ts +0 -2
  159. package/src/webhook-integration/event-webhook.controller.ts +55 -50
  160. package/src/webhook-integration/event-webhook.service.ts +422 -384
  161. package/src/webhook-integration/webhook-integration.controller.ts +5 -0
  162. package/src/webhook-integration/webhook-integration.service.ts +74 -46
  163. package/src/whatsapp-profile/dto/create.dto.ts +36 -0
  164. package/src/whatsapp-profile/dto/update.dto.ts +4 -0
  165. package/src/whatsapp-profile/whatsapp-profile.controller.ts +51 -0
  166. package/src/whatsapp-profile/whatsapp-profile.module.ts +13 -0
  167. package/src/whatsapp-profile/whatsapp-profile.service.ts +160 -0
@@ -0,0 +1,156 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ import { EnumProvider } from '../file/provider/provider.enum';
3
+
4
+ type Cfg = Record<string, unknown>;
5
+
6
+ export function buildMailConfigFromIntegration(
7
+ providerSlug: string,
8
+ config: unknown,
9
+ ): any {
10
+ const cfg = (config as Cfg) ?? {};
11
+
12
+ switch (providerSlug.toLowerCase()) {
13
+ case 'smtp':
14
+ return {
15
+ global: true,
16
+ type: 'SMTP' as const,
17
+ host: String(cfg.host ?? ''),
18
+ port: Number(cfg.port ?? 587),
19
+ secure: Boolean(cfg.secure),
20
+ username: String(cfg.username ?? ''),
21
+ password: String(cfg.password ?? ''),
22
+ };
23
+ case 'gmail':
24
+ return {
25
+ global: true,
26
+ type: 'GMAIL' as const,
27
+ clientId: String(cfg.client_id ?? ''),
28
+ clientSecret: String(cfg.client_secret ?? ''),
29
+ refreshToken: String(cfg.refresh_token ?? ''),
30
+ from: String(cfg.from_email ?? ''),
31
+ };
32
+ case 'ses':
33
+ return {
34
+ global: true,
35
+ type: 'SES' as const,
36
+ region: String(cfg.region ?? ''),
37
+ accessKeyId: String(cfg.access_key_id ?? ''),
38
+ secretAccessKey: String(cfg.secret_access_key ?? ''),
39
+ from: String(cfg.from_email ?? ''),
40
+ };
41
+ default:
42
+ throw new BadRequestException(`Unsupported mail provider: ${providerSlug}`);
43
+ }
44
+ }
45
+
46
+ export function getFromAddress(config: unknown): string {
47
+ const cfg = (config as Cfg) ?? {};
48
+ const fromEmail = String(cfg.from_email ?? '');
49
+ const fromName = String(cfg.from_name ?? '');
50
+ return fromName ? `${fromName} <${fromEmail}>` : fromEmail;
51
+ }
52
+
53
+ export function getReplyToAddress(config: unknown): string | undefined {
54
+ const cfg = (config as Cfg) ?? {};
55
+ const replyToEmail = String(cfg.reply_to_email ?? '').trim();
56
+ if (!replyToEmail) return undefined;
57
+ const replyToName = String(cfg.reply_to_name ?? '').trim();
58
+ return replyToName ? `${replyToName} <${replyToEmail}>` : replyToEmail;
59
+ }
60
+
61
+ // ─── Storage utilities ────────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Maps an integration_profile config JSON to the flat settings dict that the
65
+ * existing provider classes (S3Provider, AzureProvider, etc.) expect.
66
+ *
67
+ * integration_provider slugs → EnumProvider:
68
+ * local, s3, gcs → same
69
+ * azure-blob → abs
70
+ * s3-compatible → spaces
71
+ */
72
+ export function buildStorageConfigFromIntegration(
73
+ providerSlug: string,
74
+ config: unknown,
75
+ ): Record<string, string> {
76
+ const cfg = (config as Cfg) ?? {};
77
+
78
+ switch (providerSlug.toLowerCase()) {
79
+ case 'local':
80
+ return {
81
+ 'storage-local-path': String(cfg.base_path ?? 'storage'),
82
+ };
83
+ case 's3':
84
+ return {
85
+ 'storage-s3-key': String(cfg.access_key_id ?? ''),
86
+ 'storage-s3-secret': String(cfg.secret_access_key ?? ''),
87
+ 'storage-s3-region': String(cfg.region ?? 'us-east-1'),
88
+ 'storage-s3-bucket': String(cfg.bucket ?? ''),
89
+ };
90
+ case 'gcs':
91
+ return {
92
+ 'storage-gcs-keyfile': String(cfg.key_file_json ?? ''),
93
+ 'storage-gcs-bucket': String(cfg.bucket ?? ''),
94
+ };
95
+ case 'azure-blob':
96
+ return {
97
+ 'storage-abs-account': String(cfg.account ?? ''),
98
+ 'storage-abs-key': String(cfg.key ?? ''),
99
+ 'storage-abs-container': String(cfg.container ?? ''),
100
+ };
101
+ case 's3-compatible':
102
+ return {
103
+ 'storage-spaces-key': String(cfg.access_key_id ?? ''),
104
+ 'storage-spaces-secret': String(cfg.secret_access_key ?? ''),
105
+ 'storage-spaces-region': String(cfg.region ?? ''),
106
+ 'storage-spaces-bucket': String(cfg.bucket ?? ''),
107
+ 'storage-spaces-cdn': String(cfg.endpoint ?? ''),
108
+ };
109
+ default:
110
+ throw new BadRequestException(`Unsupported storage provider: ${providerSlug}`);
111
+ }
112
+ }
113
+
114
+ /** Maps integration_provider slug to EnumProvider. */
115
+ export function resolveStorageEnumProvider(providerSlug: string): EnumProvider {
116
+ switch (providerSlug.toLowerCase()) {
117
+ case 'local': return EnumProvider.LOCAL;
118
+ case 's3': return EnumProvider.S3;
119
+ case 'gcs': return EnumProvider.GCS;
120
+ case 'azure-blob': return EnumProvider.AZURE;
121
+ case 's3-compatible': return EnumProvider.SPACES;
122
+ default:
123
+ throw new BadRequestException(`Unsupported storage provider: ${providerSlug}`);
124
+ }
125
+ }
126
+
127
+ /** Maps integration_provider slug to the slug used in the file_provider table. */
128
+ export function resolveFileProviderSlug(providerSlug: string): string {
129
+ switch (providerSlug.toLowerCase()) {
130
+ case 'azure-blob': return 'abs';
131
+ case 's3-compatible': return 'spaces';
132
+ default: return providerSlug.toLowerCase();
133
+ }
134
+ }
135
+
136
+ // ─── AI utilities ─────────────────────────────────────────────────────────────
137
+
138
+ /** Maps an integration_profile config JSON to the API key and optional org needed by AI providers. */
139
+ export function buildAiConfigFromIntegration(
140
+ providerSlug: string,
141
+ config: unknown,
142
+ ): { apiKey: string; organization?: string } {
143
+ const cfg = (config as Cfg) ?? {};
144
+ switch (providerSlug.toLowerCase()) {
145
+ case 'openai':
146
+ return {
147
+ apiKey: String(cfg.api_key ?? ''),
148
+ ...(cfg.organization ? { organization: String(cfg.organization) } : {}),
149
+ };
150
+ case 'gemini':
151
+ case 'claude':
152
+ return { apiKey: String(cfg.api_key ?? '') };
153
+ default:
154
+ throw new BadRequestException(`Unsupported AI provider: ${providerSlug}`);
155
+ }
156
+ }
@@ -26,41 +26,50 @@ import { MailService } from './mail.service';
26
26
  return { global: true, type: 'SMTP', host: 'localhost', port: 1025, secure: false, username: '', password: '' } as MailModuleOptions;
27
27
  }
28
28
 
29
- const profile = await prisma.mail_profile.findUnique({
29
+ const profile = await (prisma as any).integration_profile.findUnique({
30
30
  where: { id: profileId },
31
+ include: {
32
+ integration_provider: { select: { slug: true } },
33
+ integration_type: { select: { slug: true } },
34
+ },
31
35
  });
32
36
 
33
37
  if (!profile) {
34
38
  return { global: true, type: 'SMTP', host: 'localhost', port: 1025, secure: false, username: '', password: '' } as MailModuleOptions;
35
39
  }
36
40
 
41
+ if (profile.integration_type?.slug !== 'email') {
42
+ return { global: true, type: 'SMTP', host: 'localhost', port: 1025, secure: false, username: '', password: '' } as MailModuleOptions;
43
+ }
44
+
45
+ const cfg = (profile.config as Record<string, unknown>) ?? {};
37
46
  const base: MailModuleOptions = { global: true } as MailModuleOptions;
38
47
 
39
- switch (profile.provider) {
40
- case 'GMAIL':
48
+ switch (profile.integration_provider.slug) {
49
+ case 'gmail':
41
50
  return Object.assign(base, {
42
51
  type: 'GMAIL',
43
- clientId: profile.gmail_client_id ?? '',
44
- clientSecret: profile.gmail_client_secret ?? '',
45
- refreshToken: profile.gmail_refresh_token ?? '',
46
- from: profile.from_email,
52
+ clientId: String(cfg.client_id ?? ''),
53
+ clientSecret: String(cfg.client_secret ?? ''),
54
+ refreshToken: String(cfg.refresh_token ?? ''),
55
+ from: String(cfg.from_email ?? ''),
47
56
  });
48
- case 'SMTP':
57
+ case 'smtp':
49
58
  return Object.assign(base, {
50
59
  type: 'SMTP',
51
- host: profile.smtp_host ?? 'localhost',
52
- port: profile.smtp_port ?? 1025,
53
- secure: profile.smtp_secure ?? false,
54
- username: profile.smtp_username ?? '',
55
- password: profile.smtp_password ?? '',
60
+ host: String(cfg.host ?? 'localhost'),
61
+ port: Number(cfg.port ?? 1025),
62
+ secure: Boolean(cfg.secure),
63
+ username: String(cfg.username ?? ''),
64
+ password: String(cfg.password ?? ''),
56
65
  });
57
- case 'SES':
66
+ case 'ses':
58
67
  return Object.assign(base, {
59
68
  type: 'SES',
60
- region: profile.ses_region ?? '',
61
- accessKeyId: profile.ses_access_key_id ?? '',
62
- secretAccessKey: profile.ses_secret_access_key ?? '',
63
- from: profile.from_email,
69
+ region: String(cfg.region ?? ''),
70
+ accessKeyId: String(cfg.access_key_id ?? ''),
71
+ secretAccessKey: String(cfg.secret_access_key ?? ''),
72
+ from: String(cfg.from_email ?? ''),
64
73
  });
65
74
  default:
66
75
  return { global: true, type: 'SMTP', host: 'localhost', port: 1025, secure: false, username: '', password: '' } as MailModuleOptions;
@@ -13,6 +13,10 @@ import {
13
13
  OnModuleInit,
14
14
  } from '@nestjs/common';
15
15
  import * as Handlebars from 'handlebars';
16
+ import {
17
+ buildMailConfigFromIntegration,
18
+ getFromAddress,
19
+ } from '../integration-profile/integration-profile.utils';
16
20
  import { IntegrationDeveloperApiService } from '../integration/services/integration-developer-api.service';
17
21
  import { SettingService } from '../setting/setting.service';
18
22
  import { CreateDTO } from './dto/create.dto';
@@ -96,89 +100,56 @@ export class MailService implements OnModuleInit {
96
100
 
97
101
  /**
98
102
  * Reload mail configuration from database and update MailMainService.
103
+ * When profileId is provided, that profile is used directly.
104
+ * Otherwise the global `mail-profile-id` setting is read.
99
105
  * Returns the active from address and the built config object.
100
106
  */
101
- async reloadConfig(): Promise<{ success: boolean; provider: string; from: string; config: any }> {
107
+ async reloadConfig(profileId?: number): Promise<{ success: boolean; provider: string; from: string; config: any }> {
102
108
  try {
103
- const settings = await this.setting.getSettingValues(['mail-profile-id']);
104
- const profileId = Number(settings['mail-profile-id']);
109
+ let resolvedProfileId = profileId;
110
+
111
+ if (!resolvedProfileId) {
112
+ const settings = await this.setting.getSettingValues(['mail-profile-id']);
113
+ resolvedProfileId = Number(settings['mail-profile-id']);
114
+ }
105
115
 
106
- if (!profileId || profileId <= 0) {
116
+ if (!resolvedProfileId || resolvedProfileId <= 0) {
107
117
  throw new BadRequestException('No mail profile configured. Set a mail profile in the Mail settings.');
108
118
  }
109
119
 
110
- const profile = await this.prismaService.mail_profile.findUnique({
111
- where: { id: profileId },
120
+ const profile = await this.prismaService.integration_profile.findUnique({
121
+ where: { id: resolvedProfileId },
122
+ include: {
123
+ integration_provider: { select: { slug: true } },
124
+ integration_type: { select: { slug: true } },
125
+ },
112
126
  });
113
127
 
114
128
  if (!profile) {
115
129
  throw new NotFoundException(`Mail profile with id ${profileId} not found.`);
116
130
  }
117
131
 
118
- const config = this.buildMailConfig(profile);
132
+ if (profile.integration_type?.slug !== 'email') {
133
+ throw new BadRequestException(
134
+ `Integration profile ${resolvedProfileId} is not an email profile.`,
135
+ );
136
+ }
137
+
138
+ const providerSlug = profile.integration_provider.slug;
139
+ const config = buildMailConfigFromIntegration(providerSlug, profile.config);
119
140
 
120
141
  this.mailMainService.setConfig(config);
121
- this.logger.log(`Mail configuration reloaded from profile "${profile.name}" (${profile.provider})`);
142
+ this.logger.log(`Mail configuration reloaded from profile "${profile.name}" (${providerSlug})`);
122
143
 
123
- const from = profile.from_name
124
- ? `${profile.from_name} <${profile.from_email}>`
125
- : profile.from_email;
144
+ const from = getFromAddress(profile.config);
126
145
 
127
- return { success: true, provider: profile.provider, from, config };
146
+ return { success: true, provider: providerSlug, from, config };
128
147
  } catch (error) {
129
148
  this.logger.error('Error reloading mail configuration:', error);
130
149
  throw error;
131
150
  }
132
151
  }
133
152
 
134
- private buildMailConfig(profile: {
135
- provider: string;
136
- from_email: string;
137
- smtp_host?: string | null;
138
- smtp_port?: number | null;
139
- smtp_secure?: boolean | null;
140
- smtp_username?: string | null;
141
- smtp_password?: string | null;
142
- gmail_client_id?: string | null;
143
- gmail_client_secret?: string | null;
144
- gmail_refresh_token?: string | null;
145
- ses_access_key_id?: string | null;
146
- ses_secret_access_key?: string | null;
147
- ses_region?: string | null;
148
- }): any {
149
- switch (profile.provider) {
150
- case 'GMAIL':
151
- return {
152
- global: true,
153
- type: 'GMAIL' as const,
154
- clientId: profile.gmail_client_id ?? '',
155
- clientSecret: profile.gmail_client_secret ?? '',
156
- refreshToken: profile.gmail_refresh_token ?? '',
157
- from: profile.from_email,
158
- };
159
- case 'SMTP':
160
- return {
161
- global: true,
162
- type: 'SMTP' as const,
163
- host: profile.smtp_host ?? '',
164
- port: profile.smtp_port ?? 587,
165
- secure: profile.smtp_secure ?? false,
166
- username: profile.smtp_username ?? '',
167
- password: profile.smtp_password ?? '',
168
- };
169
- case 'SES':
170
- return {
171
- global: true,
172
- type: 'SES' as const,
173
- region: profile.ses_region ?? '',
174
- accessKeyId: profile.ses_access_key_id ?? '',
175
- secretAccessKey: profile.ses_secret_access_key ?? '',
176
- from: profile.from_email,
177
- };
178
- default:
179
- throw new BadRequestException(`Invalid mail provider: ${profile.provider}`);
180
- }
181
- }
182
153
 
183
154
  async list(locale: string, paginationParams: PaginationDTO) {
184
155
  const result = await this.localeService.listModelWithLocale(
@@ -6,8 +6,8 @@ import { AiModule } from '../ai/ai.module';
6
6
  import { SystemModule } from '../core/system.module';
7
7
  import { DashboardModule } from '../dashboard/dashboard.module';
8
8
  import { FileModule } from '../file/file.module';
9
+ import { IntegrationProfileModule } from '../integration-profile/integration-profile.module';
9
10
  import { MailModule } from '../mail/mail.module';
10
- import { MailProfileModule } from '../mail-profile/mail-profile.module';
11
11
  import { MailSentModule } from '../mail-sent/mail-sent.module';
12
12
  import { McpChatService } from '../mcp-chat/mcp-chat.service';
13
13
  import { MenuModule } from '../menu/menu.module';
@@ -69,7 +69,7 @@ import { AccessLogModule } from '../access-log/access-log.module';
69
69
  forwardRef(() => ScreenModule),
70
70
  forwardRef(() => FileModule),
71
71
  forwardRef(() => MailModule),
72
- forwardRef(() => MailProfileModule),
72
+ forwardRef(() => IntegrationProfileModule),
73
73
  forwardRef(() => MailSentModule),
74
74
  forwardRef(() => SessionModule),
75
75
  forwardRef(() => NotificationModule),
@@ -1,15 +1,15 @@
1
1
  import { Injectable } from '@nestjs/common';
2
- import { MailProfileService } from '../../mail-profile/mail-profile.service';
2
+ import { IntegrationProfileService } from '../../integration-profile/integration-profile.service';
3
3
  import { McpTool } from '../decorators/mcp-tool.decorator';
4
4
  import { McpContext } from '../types/mcp-context.type';
5
5
 
6
6
  @Injectable()
7
7
  export class CoreMailProfileMcpTools {
8
- constructor(private readonly mailProfileService: MailProfileService) {}
8
+ constructor(private readonly integrationProfileService: IntegrationProfileService) {}
9
9
 
10
10
  @McpTool({
11
11
  name: 'core.mail-profile.list',
12
- description: 'Lists mail profiles with optional pagination. Admin only.',
12
+ description: 'Lists integration profiles with optional pagination. Admin only.',
13
13
  inputSchema: {
14
14
  type: 'object',
15
15
  properties: {
@@ -26,7 +26,7 @@ export class CoreMailProfileMcpTools {
26
26
  args: { page?: number; pageSize?: number; search?: string; sortField?: string; sortOrder?: string },
27
27
  _context: McpContext,
28
28
  ): Promise<any> {
29
- return this.mailProfileService.list({
29
+ return this.integrationProfileService.list({
30
30
  page: args.page ?? 1,
31
31
  pageSize: args.pageSize ?? 20,
32
32
  search: args.search ?? '',
@@ -38,91 +38,32 @@ export class CoreMailProfileMcpTools {
38
38
 
39
39
  @McpTool({
40
40
  name: 'core.mail-profile.get',
41
- description: 'Returns a single mail profile by ID. Admin only.',
41
+ description: 'Returns a single integration profile by ID. Admin only.',
42
42
  inputSchema: {
43
43
  type: 'object',
44
44
  properties: {
45
- id: { type: 'number', description: 'Mail profile ID' },
45
+ id: { type: 'number', description: 'Profile ID' },
46
46
  },
47
47
  required: ['id'],
48
48
  },
49
49
  readOnly: true,
50
50
  })
51
51
  async getMailProfile(args: { id: number }, _context: McpContext): Promise<any> {
52
- return this.mailProfileService.get(args.id);
53
- }
54
-
55
- @McpTool({
56
- name: 'core.mail-profile.create',
57
- description: 'Creates a new mail profile (SMTP configuration). Admin only.',
58
- inputSchema: {
59
- type: 'object',
60
- properties: {
61
- name: { type: 'string', description: 'Profile name' },
62
- host: { type: 'string', description: 'SMTP host' },
63
- port: { type: 'number', description: 'SMTP port' },
64
- username: { type: 'string', description: 'SMTP username' },
65
- password: { type: 'string', description: 'SMTP password' },
66
- },
67
- required: ['name', 'host', 'port', 'username'],
68
- },
69
- })
70
- async createMailProfile(args: { name: string; host: string; port: number; username: string; password?: string }, _context: McpContext): Promise<any> {
71
- return this.mailProfileService.create(args as any);
72
- }
73
-
74
- @McpTool({
75
- name: 'core.mail-profile.update',
76
- description: 'Updates an existing mail profile by ID. Admin only.',
77
- inputSchema: {
78
- type: 'object',
79
- properties: {
80
- id: { type: 'number', description: 'Mail profile ID' },
81
- name: { type: 'string', description: 'New profile name' },
82
- host: { type: 'string', description: 'New SMTP host' },
83
- port: { type: 'number', description: 'New SMTP port' },
84
- username: { type: 'string', description: 'New SMTP username' },
85
- password: { type: 'string', description: 'New SMTP password' },
86
- },
87
- required: ['id'],
88
- },
89
- })
90
- async updateMailProfile(args: { id: number; [key: string]: any }, _context: McpContext): Promise<any> {
91
- const { id, ...data } = args;
92
- return this.mailProfileService.update({ id, data: data as any });
52
+ return this.integrationProfileService.get(args.id);
93
53
  }
94
54
 
95
55
  @McpTool({
96
56
  name: 'core.mail-profile.delete',
97
- description: 'Deletes one or more mail profiles by their IDs. Admin only.',
57
+ description: 'Deletes one or more integration profiles by their IDs. Admin only.',
98
58
  inputSchema: {
99
59
  type: 'object',
100
60
  properties: {
101
- ids: { type: 'array', items: { type: 'number' }, description: 'Mail profile IDs to delete' },
61
+ ids: { type: 'array', items: { type: 'number' }, description: 'Profile IDs to delete' },
102
62
  },
103
63
  required: ['ids'],
104
64
  },
105
65
  })
106
66
  async deleteMailProfiles(args: { ids: number[] }, _context: McpContext): Promise<any> {
107
- return this.mailProfileService.delete({ ids: args.ids });
108
- }
109
-
110
- @McpTool({
111
- name: 'core.mail-profile.test',
112
- description: 'Tests a mail profile by sending a test message. Admin only.',
113
- inputSchema: {
114
- type: 'object',
115
- properties: {
116
- name: { type: 'string', description: 'Profile name' },
117
- host: { type: 'string', description: 'SMTP host' },
118
- port: { type: 'number', description: 'SMTP port' },
119
- username: { type: 'string', description: 'SMTP username' },
120
- password: { type: 'string', description: 'SMTP password' },
121
- },
122
- required: ['host', 'port', 'username'],
123
- },
124
- })
125
- async testMailProfile(args: Record<string, any>, _context: McpContext): Promise<any> {
126
- return this.mailProfileService.test(args as any);
67
+ return this.integrationProfileService.delete({ ids: args.ids });
127
68
  }
128
69
  }
@@ -11,6 +11,7 @@ import {
11
11
  NotFoundException,
12
12
  } from '@nestjs/common';
13
13
  import axios from 'axios';
14
+ import { buildAiConfigFromIntegration } from '../integration-profile/integration-profile.utils';
14
15
  import { McpRegistryService, McpToolEntry } from '../mcp/mcp-registry.service';
15
16
  import { McpContext } from '../mcp/types/mcp-context.type';
16
17
  import { SettingService } from '../setting/setting.service';
@@ -772,30 +773,32 @@ export class McpChatService {
772
773
  }
773
774
 
774
775
  private async getProviderKey(): Promise<{ provider: 'openai' | 'gemini'; apiKey: string }> {
775
- const settings = await this.setting.getSettingValues([
776
- 'mcp-ai-provider',
777
- 'ai-openai-api-key',
778
- 'ai-gemini-api-key',
779
- ]);
780
- const provider = ((settings['mcp-ai-provider'] as string) || 'openai') as 'openai' | 'gemini';
776
+ const settings = await this.setting.getSettingValues(['mcp-ai-profile-id']);
777
+ const profileId = Number(settings['mcp-ai-profile-id']);
781
778
 
782
- if (provider === 'gemini') {
783
- const key = settings['ai-gemini-api-key'] as string;
784
- if (!key) {
785
- throw new BadRequestException(
786
- 'Gemini API key is not configured. Please set it in Settings → AI → Gemini API Key.',
787
- );
788
- }
789
- return { provider: 'gemini', apiKey: key };
790
- }
791
-
792
- const key = settings['ai-openai-api-key'] as string;
793
- if (!key) {
779
+ if (!profileId) {
794
780
  throw new BadRequestException(
795
- 'OpenAI API key is not configured. Please set it in Settings → AI → OpenAI API Key.',
781
+ 'No MCP AI profile configured. Select one in Settings → MCP.',
796
782
  );
797
783
  }
798
- return { provider: 'openai', apiKey: key };
784
+
785
+ const profile = await this.prisma.integration_profile.findUnique({
786
+ where: { id: profileId },
787
+ include: { integration_provider: { select: { slug: true } } },
788
+ });
789
+
790
+ if (!profile) {
791
+ throw new BadRequestException(`MCP AI profile ${profileId} not found.`);
792
+ }
793
+
794
+ const providerSlug = profile.integration_provider.slug;
795
+ const { apiKey } = buildAiConfigFromIntegration(providerSlug, profile.config);
796
+
797
+ if (!apiKey) {
798
+ throw new BadRequestException(`MCP AI profile ${profileId} has no API key configured.`);
799
+ }
800
+
801
+ return { provider: providerSlug === 'gemini' ? 'gemini' : 'openai', apiKey };
799
802
  }
800
803
 
801
804
  private async buildOpenAiTools(
@@ -206,8 +206,6 @@ export class SettingService {
206
206
  'password-min-uppercase',
207
207
  'password-min-length',
208
208
  'mfa-email-code-length',
209
- 'ai-openai-api-key-enabled',
210
- 'ai-gemini-api-key-enabled',
211
209
  'mcp-enabled',
212
210
  'date-format',
213
211
  'time-format',
@@ -1,50 +1,55 @@
1
- import { DeleteDTO, Role } from '@hed-hog/api';
2
- import { Pagination } from '@hed-hog/api-pagination';
3
- import {
4
- Body,
5
- Controller,
6
- Delete,
7
- Get,
8
- Param,
9
- ParseIntPipe,
10
- Patch,
11
- Post,
12
- } from '@nestjs/common';
13
- import { EventWebhookService } from './event-webhook.service';
14
-
15
- @Role()
16
- @Controller('event-webhook')
17
- export class EventWebhookController {
18
- constructor(private readonly service: EventWebhookService) {}
19
-
20
- @Get()
21
- list(@Pagination() paginationParams) {
22
- return this.service.list(paginationParams);
23
- }
24
-
25
- @Get(':id')
26
- get(@Param('id', ParseIntPipe) id: number) {
27
- return this.service.get(id);
28
- }
29
-
30
- @Post()
31
- create(@Body() data: any) {
32
- return this.service.create(data);
33
- }
34
-
35
- @Patch(':id')
36
- update(@Param('id', ParseIntPipe) id: number, @Body() data: any) {
37
- return this.service.update(id, data);
38
- }
39
-
40
- @Delete()
41
- delete(@Body() data: DeleteDTO) {
42
- return this.service.delete(data);
43
- }
44
-
45
- @Get(':id/log')
46
- logs(@Param('id', ParseIntPipe) id: number, @Pagination() paginationParams) {
47
- return this.service.listLogs(id, paginationParams);
48
- }
49
- }
50
-
1
+ import { DeleteDTO, Role } from '@hed-hog/api';
2
+ import { Pagination } from '@hed-hog/api-pagination';
3
+ import {
4
+ Body,
5
+ Controller,
6
+ Delete,
7
+ Get,
8
+ Param,
9
+ ParseIntPipe,
10
+ Patch,
11
+ Post,
12
+ } from '@nestjs/common';
13
+ import { EventWebhookService } from './event-webhook.service';
14
+
15
+ @Role()
16
+ @Controller('event-webhook')
17
+ export class EventWebhookController {
18
+ constructor(private readonly service: EventWebhookService) {}
19
+
20
+ @Get()
21
+ list(@Pagination() paginationParams) {
22
+ return this.service.list(paginationParams);
23
+ }
24
+
25
+ @Get(':id')
26
+ get(@Param('id', ParseIntPipe) id: number) {
27
+ return this.service.get(id);
28
+ }
29
+
30
+ @Post()
31
+ create(@Body() data: any) {
32
+ return this.service.create(data);
33
+ }
34
+
35
+ @Patch(':id')
36
+ update(@Param('id', ParseIntPipe) id: number, @Body() data: any) {
37
+ return this.service.update(id, data);
38
+ }
39
+
40
+ @Delete()
41
+ delete(@Body() data: DeleteDTO) {
42
+ return this.service.delete(data);
43
+ }
44
+
45
+ @Post(':id/duplicate')
46
+ duplicate(@Param('id', ParseIntPipe) id: number) {
47
+ return this.service.duplicate(id);
48
+ }
49
+
50
+ @Get(':id/log')
51
+ logs(@Param('id', ParseIntPipe) id: number, @Pagination() paginationParams) {
52
+ return this.service.listLogs(id, paginationParams);
53
+ }
54
+ }
55
+