@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.
- package/dist/ai/ai.service.d.ts +3 -12
- package/dist/ai/ai.service.d.ts.map +1 -1
- package/dist/ai/ai.service.js +34 -125
- package/dist/ai/ai.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +3 -3
- package/dist/core.module.js.map +1 -1
- package/dist/file/file.service.d.ts +1 -1
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +50 -64
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/provider/provider.enum.d.ts +2 -1
- package/dist/file/provider/provider.enum.d.ts.map +1 -1
- package/dist/file/provider/provider.enum.js +1 -0
- package/dist/file/provider/provider.enum.js.map +1 -1
- package/dist/file/provider/provider.factory.d.ts.map +1 -1
- package/dist/file/provider/provider.factory.js +3 -0
- package/dist/file/provider/provider.factory.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +27 -0
- package/dist/file/provider/spaces.provider.d.ts.map +1 -0
- package/dist/file/provider/spaces.provider.js +218 -0
- package/dist/file/provider/spaces.provider.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/dto/create.dto.d.ts +9 -0
- package/dist/integration-profile/dto/create.dto.d.ts.map +1 -0
- package/dist/integration-profile/dto/create.dto.js +54 -0
- package/dist/integration-profile/dto/create.dto.js.map +1 -0
- package/dist/integration-profile/dto/export.dto.d.ts +5 -0
- package/dist/integration-profile/dto/export.dto.d.ts.map +1 -0
- package/dist/integration-profile/dto/export.dto.js +29 -0
- package/dist/integration-profile/dto/export.dto.js.map +1 -0
- package/dist/integration-profile/dto/import-confirm.dto.d.ts +17 -0
- package/dist/integration-profile/dto/import-confirm.dto.d.ts.map +1 -0
- package/dist/integration-profile/dto/import-confirm.dto.js +77 -0
- package/dist/integration-profile/dto/import-confirm.dto.js.map +1 -0
- package/dist/integration-profile/dto/update.dto.d.ts +6 -0
- package/dist/integration-profile/dto/update.dto.d.ts.map +1 -0
- package/dist/integration-profile/dto/update.dto.js +9 -0
- package/dist/integration-profile/dto/update.dto.js.map +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts +142 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -0
- package/dist/integration-profile/integration-profile.controller.js +165 -0
- package/dist/integration-profile/integration-profile.controller.js.map +1 -0
- package/dist/integration-profile/integration-profile.module.d.ts +3 -0
- package/dist/integration-profile/integration-profile.module.d.ts.map +1 -0
- package/dist/integration-profile/integration-profile.module.js +29 -0
- package/dist/integration-profile/integration-profile.module.js.map +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts +160 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -0
- package/dist/integration-profile/integration-profile.service.js +606 -0
- package/dist/integration-profile/integration-profile.service.js.map +1 -0
- package/dist/integration-profile/integration-profile.utils.d.ts +24 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -0
- package/dist/integration-profile/integration-profile.utils.js +147 -0
- package/dist/integration-profile/integration-profile.utils.js.map +1 -0
- package/dist/mail/mail.module.d.ts.map +1 -1
- package/dist/mail/mail.module.js +27 -19
- package/dist/mail/mail.module.js.map +1 -1
- package/dist/mail/mail.service.d.ts +3 -2
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +25 -47
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp.module.js +2 -2
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts +3 -15
- package/dist/mcp/tools/core-mail-profile.mcp-tools.d.ts.map +1 -1
- package/dist/mcp/tools/core-mail-profile.mcp-tools.js +12 -94
- package/dist/mcp/tools/core-mail-profile.mcp-tools.js.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
- package/dist/mcp-chat/mcp-chat.service.js +17 -16
- package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +0 -2
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/webhook-integration/event-webhook.controller.d.ts +1 -0
- package/dist/webhook-integration/event-webhook.controller.d.ts.map +1 -1
- package/dist/webhook-integration/event-webhook.controller.js +10 -0
- package/dist/webhook-integration/event-webhook.controller.js.map +1 -1
- package/dist/webhook-integration/event-webhook.service.d.ts +2 -0
- package/dist/webhook-integration/event-webhook.service.d.ts.map +1 -1
- package/dist/webhook-integration/event-webhook.service.js +30 -0
- package/dist/webhook-integration/event-webhook.service.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.d.ts +1 -0
- package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.js +10 -0
- package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts +2 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +62 -44
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/dist/whatsapp-profile/dto/create.dto.d.ts +11 -0
- package/dist/whatsapp-profile/dto/create.dto.d.ts.map +1 -0
- package/dist/whatsapp-profile/dto/create.dto.js +55 -0
- package/dist/whatsapp-profile/dto/create.dto.js.map +1 -0
- package/dist/whatsapp-profile/dto/update.dto.d.ts +6 -0
- package/dist/whatsapp-profile/dto/update.dto.d.ts.map +1 -0
- package/dist/whatsapp-profile/dto/update.dto.js +9 -0
- package/dist/whatsapp-profile/dto/update.dto.js.map +1 -0
- package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts +54 -0
- package/dist/whatsapp-profile/whatsapp-profile.controller.d.ts.map +1 -0
- package/dist/whatsapp-profile/whatsapp-profile.controller.js +85 -0
- package/dist/whatsapp-profile/whatsapp-profile.controller.js.map +1 -0
- package/dist/whatsapp-profile/whatsapp-profile.module.d.ts +3 -0
- package/dist/whatsapp-profile/whatsapp-profile.module.d.ts.map +1 -0
- package/dist/whatsapp-profile/whatsapp-profile.module.js +26 -0
- package/dist/whatsapp-profile/whatsapp-profile.module.js.map +1 -0
- package/dist/whatsapp-profile/whatsapp-profile.service.d.ts +63 -0
- package/dist/whatsapp-profile/whatsapp-profile.service.d.ts.map +1 -0
- package/dist/whatsapp-profile/whatsapp-profile.service.js +138 -0
- package/dist/whatsapp-profile/whatsapp-profile.service.js.map +1 -0
- package/hedhog/data/file_provider.yaml +9 -1
- package/hedhog/data/integration_provider.yaml +132 -0
- package/hedhog/data/integration_type.yaml +29 -0
- package/hedhog/data/menu.yaml +15 -14
- package/hedhog/data/role.yaml +8 -0
- package/hedhog/data/route.yaml +66 -15
- package/hedhog/data/setting_group.yaml +30 -191
- package/hedhog/data/setting_subgroup.yaml +5 -13
- package/hedhog/data/whatsapp_profile.yaml +6 -0
- package/hedhog/frontend/app/account/components/connected-accounts.tsx.ejs +25 -20
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +579 -204
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +1662 -0
- package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +233 -108
- package/hedhog/frontend/app/mail/profiles/page.tsx.ejs +3 -822
- package/hedhog/frontend/app/users/permissions.tsx.ejs +79 -2
- package/hedhog/frontend/app/users/user-sheet.tsx.ejs +7 -2
- package/hedhog/frontend/messages/en.json +172 -0
- package/hedhog/frontend/messages/pt.json +172 -0
- package/hedhog/table/integration_profile.yaml +34 -0
- package/hedhog/table/integration_provider.yaml +23 -0
- package/hedhog/table/integration_type.yaml +19 -0
- package/hedhog/table/webhook_action.yaml +85 -85
- package/hedhog/table/whatsapp_profile.yaml +26 -0
- package/package.json +2 -2
- package/src/ai/ai.service.ts +35 -153
- package/src/core.module.ts +3 -3
- package/src/file/file.service.ts +60 -76
- package/src/file/provider/provider.enum.ts +1 -0
- package/src/file/provider/provider.factory.ts +3 -0
- package/src/file/provider/spaces.provider.ts +261 -0
- package/src/index.ts +6 -3
- package/src/integration-profile/dto/create.dto.ts +44 -0
- package/src/integration-profile/dto/export.dto.ts +15 -0
- package/src/integration-profile/dto/import-confirm.dto.ts +61 -0
- package/src/integration-profile/dto/update.dto.ts +4 -0
- package/src/integration-profile/integration-profile.controller.ts +129 -0
- package/src/integration-profile/integration-profile.module.ts +16 -0
- package/src/integration-profile/integration-profile.service.ts +788 -0
- package/src/integration-profile/integration-profile.utils.ts +156 -0
- package/src/mail/mail.module.ts +27 -18
- package/src/mail/mail.service.ts +31 -60
- package/src/mcp/mcp.module.ts +2 -2
- package/src/mcp/tools/core-mail-profile.mcp-tools.ts +10 -69
- package/src/mcp-chat/mcp-chat.service.ts +23 -20
- package/src/setting/setting.service.ts +0 -2
- package/src/webhook-integration/event-webhook.controller.ts +55 -50
- package/src/webhook-integration/event-webhook.service.ts +422 -384
- package/src/webhook-integration/webhook-integration.controller.ts +5 -0
- package/src/webhook-integration/webhook-integration.service.ts +74 -46
- package/src/whatsapp-profile/dto/create.dto.ts +36 -0
- package/src/whatsapp-profile/dto/update.dto.ts +4 -0
- package/src/whatsapp-profile/whatsapp-profile.controller.ts +51 -0
- package/src/whatsapp-profile/whatsapp-profile.module.ts +13 -0
- package/src/whatsapp-profile/whatsapp-profile.service.ts +160 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import { DeleteDTO } from '@hed-hog/api';
|
|
2
|
+
import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
|
|
3
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
4
|
+
import {
|
|
5
|
+
BadRequestException,
|
|
6
|
+
Inject,
|
|
7
|
+
Injectable,
|
|
8
|
+
NotFoundException,
|
|
9
|
+
forwardRef,
|
|
10
|
+
} from '@nestjs/common';
|
|
11
|
+
import * as pako from 'pako';
|
|
12
|
+
import { buildMailConfigFromIntegration } from './integration-profile.utils';
|
|
13
|
+
import { CreateDTO } from './dto/create.dto';
|
|
14
|
+
import {
|
|
15
|
+
ConflictResolutionDTO,
|
|
16
|
+
ImportProfileItemDTO,
|
|
17
|
+
} from './dto/import-confirm.dto';
|
|
18
|
+
import { UpdateDTO } from './dto/update.dto';
|
|
19
|
+
|
|
20
|
+
type IntegrationProfileExportItem = {
|
|
21
|
+
slug: string;
|
|
22
|
+
name: string;
|
|
23
|
+
type_slug: string;
|
|
24
|
+
provider_slug: string;
|
|
25
|
+
config: Record<string, unknown> | null;
|
|
26
|
+
is_active: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type IntegrationProfileImportEnvelope = {
|
|
30
|
+
type: 'integration-profile';
|
|
31
|
+
version: 1;
|
|
32
|
+
generated_at: string;
|
|
33
|
+
include_secrets: boolean;
|
|
34
|
+
data: IntegrationProfileExportItem[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type ValidatedImportItem = {
|
|
38
|
+
index: number;
|
|
39
|
+
slug: string;
|
|
40
|
+
name: string;
|
|
41
|
+
type_slug: string;
|
|
42
|
+
provider_slug: string;
|
|
43
|
+
config: Record<string, unknown> | null;
|
|
44
|
+
is_active: boolean;
|
|
45
|
+
has_conflict: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type InvalidImportItem = {
|
|
49
|
+
index: number;
|
|
50
|
+
slug?: string;
|
|
51
|
+
errors: string[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
@Injectable()
|
|
55
|
+
export class IntegrationProfileService {
|
|
56
|
+
constructor(
|
|
57
|
+
@Inject(forwardRef(() => PrismaService))
|
|
58
|
+
private readonly prismaService: PrismaService,
|
|
59
|
+
@Inject(forwardRef(() => PaginationService))
|
|
60
|
+
private readonly paginationService: PaginationService,
|
|
61
|
+
) {}
|
|
62
|
+
|
|
63
|
+
async listTypes() {
|
|
64
|
+
return this.prismaService.integration_type.findMany({
|
|
65
|
+
orderBy: { id: 'asc' },
|
|
66
|
+
select: {
|
|
67
|
+
id: true,
|
|
68
|
+
slug: true,
|
|
69
|
+
icon: true,
|
|
70
|
+
integration_type_locale: {
|
|
71
|
+
select: { name: true, locale: { select: { code: true } } },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async listProviders(typeId?: number, typeSlug?: string) {
|
|
78
|
+
const normalizedTypeSlug = String(typeSlug ?? '').trim();
|
|
79
|
+
|
|
80
|
+
return this.prismaService.integration_provider.findMany({
|
|
81
|
+
where: {
|
|
82
|
+
...(typeId ? { type_id: typeId } : {}),
|
|
83
|
+
...(normalizedTypeSlug
|
|
84
|
+
? { integration_type: { slug: normalizedTypeSlug } }
|
|
85
|
+
: {}),
|
|
86
|
+
},
|
|
87
|
+
orderBy: { id: 'asc' },
|
|
88
|
+
select: {
|
|
89
|
+
id: true,
|
|
90
|
+
slug: true,
|
|
91
|
+
type_id: true,
|
|
92
|
+
integration_provider_locale: {
|
|
93
|
+
select: { name: true, locale: { select: { code: true } } },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async list(
|
|
100
|
+
paginationParams: PaginationDTO,
|
|
101
|
+
typeId?: number,
|
|
102
|
+
typeSlug?: string,
|
|
103
|
+
) {
|
|
104
|
+
const fields = ['slug', 'name'];
|
|
105
|
+
const OR = this.prismaService.createInsensitiveSearch(fields, paginationParams);
|
|
106
|
+
const normalizedTypeSlug = String(typeSlug ?? '').trim();
|
|
107
|
+
|
|
108
|
+
const result = await this.paginationService.paginate(
|
|
109
|
+
this.prismaService.integration_profile,
|
|
110
|
+
paginationParams,
|
|
111
|
+
{
|
|
112
|
+
where: {
|
|
113
|
+
...(OR.length > 0 ? { OR } : {}),
|
|
114
|
+
...(typeId ? { type_id: typeId } : {}),
|
|
115
|
+
...(normalizedTypeSlug
|
|
116
|
+
? { integration_type: { slug: normalizedTypeSlug } }
|
|
117
|
+
: {}),
|
|
118
|
+
},
|
|
119
|
+
select: {
|
|
120
|
+
id: true,
|
|
121
|
+
slug: true,
|
|
122
|
+
name: true,
|
|
123
|
+
config: true,
|
|
124
|
+
type_id: true,
|
|
125
|
+
provider_id: true,
|
|
126
|
+
is_active: true,
|
|
127
|
+
created_at: true,
|
|
128
|
+
updated_at: true,
|
|
129
|
+
integration_type: {
|
|
130
|
+
select: {
|
|
131
|
+
slug: true,
|
|
132
|
+
icon: true,
|
|
133
|
+
integration_type_locale: {
|
|
134
|
+
select: { name: true, locale: { select: { code: true } } },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
integration_provider: {
|
|
139
|
+
select: {
|
|
140
|
+
slug: true,
|
|
141
|
+
integration_provider_locale: {
|
|
142
|
+
select: { name: true, locale: { select: { code: true } } },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
orderBy: [{ updated_at: 'desc' }, { id: 'desc' }],
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const items = Array.isArray(result.data) ? result.data : [];
|
|
152
|
+
if (items.length > 0) {
|
|
153
|
+
const ids = items.map((p: any) => p.id as number);
|
|
154
|
+
const counts = await this.prismaService.$queryRaw<
|
|
155
|
+
{ id: number; uses_count: bigint }[]
|
|
156
|
+
>`
|
|
157
|
+
SELECT
|
|
158
|
+
ip.id,
|
|
159
|
+
(
|
|
160
|
+
COALESCE((SELECT COUNT(*)::int FROM campaign WHERE mail_profile_id = ip.id), 0) +
|
|
161
|
+
COALESCE((SELECT COUNT(*)::int FROM webhook_action WHERE mail_profile_id = ip.id), 0)
|
|
162
|
+
) AS uses_count
|
|
163
|
+
FROM integration_profile ip
|
|
164
|
+
WHERE ip.id = ANY(${ids}::int[])
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const countMap = new Map(
|
|
168
|
+
counts.map((r) => [r.id, Number(r.uses_count)]),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
result.data = items.map((p: any) => ({
|
|
172
|
+
...p,
|
|
173
|
+
uses_count: countMap.get(p.id) ?? 0,
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async get(id: number) {
|
|
181
|
+
const profile = await this.prismaService.integration_profile.findUnique({
|
|
182
|
+
where: { id },
|
|
183
|
+
include: {
|
|
184
|
+
integration_type: {
|
|
185
|
+
select: {
|
|
186
|
+
slug: true,
|
|
187
|
+
icon: true,
|
|
188
|
+
integration_type_locale: {
|
|
189
|
+
select: { name: true, locale: { select: { code: true } } },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
integration_provider: {
|
|
194
|
+
select: {
|
|
195
|
+
slug: true,
|
|
196
|
+
integration_provider_locale: {
|
|
197
|
+
select: { name: true, locale: { select: { code: true } } },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!profile) {
|
|
205
|
+
throw new NotFoundException(
|
|
206
|
+
`Integration profile with id ${id} not found.`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return profile;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async create(data: CreateDTO) {
|
|
214
|
+
await this.ensureSlugIsAvailable(data.slug);
|
|
215
|
+
await this.validateTypeAndProvider(data.type_id, data.provider_id);
|
|
216
|
+
|
|
217
|
+
return this.prismaService.integration_profile.create({
|
|
218
|
+
data: {
|
|
219
|
+
slug: data.slug.trim(),
|
|
220
|
+
name: data.name.trim(),
|
|
221
|
+
type_id: data.type_id,
|
|
222
|
+
provider_id: data.provider_id,
|
|
223
|
+
config: (data.config as object) ?? null,
|
|
224
|
+
is_active: data.is_active ?? true,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async update({ id, data }: { id: number; data: UpdateDTO }) {
|
|
230
|
+
const existing = await this.prismaService.integration_profile.findUnique({
|
|
231
|
+
where: { id },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (!existing) {
|
|
235
|
+
throw new NotFoundException(
|
|
236
|
+
`Integration profile with id ${id} not found.`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (data.slug && data.slug !== existing.slug) {
|
|
241
|
+
await this.ensureSlugIsAvailable(data.slug);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const type_id = data.type_id ?? existing.type_id;
|
|
245
|
+
const provider_id = data.provider_id ?? existing.provider_id;
|
|
246
|
+
await this.validateTypeAndProvider(type_id, provider_id);
|
|
247
|
+
|
|
248
|
+
return this.prismaService.integration_profile.update({
|
|
249
|
+
where: { id },
|
|
250
|
+
data: {
|
|
251
|
+
slug: data.slug?.trim() ?? existing.slug,
|
|
252
|
+
name: data.name?.trim() ?? existing.name,
|
|
253
|
+
type_id,
|
|
254
|
+
provider_id,
|
|
255
|
+
config:
|
|
256
|
+
data.config !== undefined
|
|
257
|
+
? ((data.config as object) ?? null)
|
|
258
|
+
: existing.config,
|
|
259
|
+
is_active: data.is_active ?? existing.is_active,
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async delete({ ids }: DeleteDTO): Promise<{ count: number }> {
|
|
265
|
+
if (!ids?.length) {
|
|
266
|
+
throw new BadRequestException(
|
|
267
|
+
'You must select at least one item to delete.',
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return this.prismaService.integration_profile.deleteMany({
|
|
272
|
+
where: { id: { in: ids } },
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async test(data: CreateDTO) {
|
|
277
|
+
await this.validateTypeAndProvider(data.type_id, data.provider_id);
|
|
278
|
+
|
|
279
|
+
const provider = await this.prismaService.integration_provider.findUnique({
|
|
280
|
+
where: { id: data.provider_id },
|
|
281
|
+
select: {
|
|
282
|
+
slug: true,
|
|
283
|
+
integration_type: {
|
|
284
|
+
select: {
|
|
285
|
+
slug: true,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (!provider) {
|
|
292
|
+
throw new BadRequestException(
|
|
293
|
+
`Provider with id ${data.provider_id} not found.`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (provider.integration_type.slug !== 'email') {
|
|
298
|
+
throw new BadRequestException(
|
|
299
|
+
'Integration profile test is only available for email type.',
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
buildMailConfigFromIntegration(provider.slug, data.config);
|
|
304
|
+
|
|
305
|
+
const destination = String(
|
|
306
|
+
(data.config as Record<string, unknown> | null | undefined)?.from_email ??
|
|
307
|
+
'',
|
|
308
|
+
).trim();
|
|
309
|
+
|
|
310
|
+
if (!destination) {
|
|
311
|
+
throw new BadRequestException('Sender email is required.');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
success: true,
|
|
316
|
+
destination,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async exportProfiles({
|
|
321
|
+
ids,
|
|
322
|
+
includeSecrets = false,
|
|
323
|
+
}: {
|
|
324
|
+
ids?: number[];
|
|
325
|
+
includeSecrets?: boolean;
|
|
326
|
+
}): Promise<Buffer> {
|
|
327
|
+
const rows = await this.prismaService.integration_profile.findMany({
|
|
328
|
+
where: ids?.length ? { id: { in: ids } } : undefined,
|
|
329
|
+
select: {
|
|
330
|
+
slug: true,
|
|
331
|
+
name: true,
|
|
332
|
+
config: true,
|
|
333
|
+
is_active: true,
|
|
334
|
+
integration_type: {
|
|
335
|
+
select: { slug: true },
|
|
336
|
+
},
|
|
337
|
+
integration_provider: {
|
|
338
|
+
select: { slug: true },
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
orderBy: [{ slug: 'asc' }],
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const payload: IntegrationProfileExportItem[] = rows.map((row) => ({
|
|
345
|
+
slug: row.slug,
|
|
346
|
+
name: row.name,
|
|
347
|
+
type_slug: row.integration_type.slug,
|
|
348
|
+
provider_slug: row.integration_provider.slug,
|
|
349
|
+
config: this.sanitizeConfigForExport(
|
|
350
|
+
row.integration_provider.slug,
|
|
351
|
+
row.config as Record<string, unknown> | null,
|
|
352
|
+
includeSecrets,
|
|
353
|
+
),
|
|
354
|
+
is_active: row.is_active,
|
|
355
|
+
}));
|
|
356
|
+
|
|
357
|
+
const envelope: IntegrationProfileImportEnvelope = {
|
|
358
|
+
type: 'integration-profile',
|
|
359
|
+
version: 1,
|
|
360
|
+
generated_at: new Date().toISOString(),
|
|
361
|
+
include_secrets: includeSecrets,
|
|
362
|
+
data: payload,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const compressed = pako.gzip(JSON.stringify(envelope));
|
|
366
|
+
return Buffer.from(compressed);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async validateImportFile(file: MulterFile) {
|
|
370
|
+
const envelope = this.parseImportEnvelope(file);
|
|
371
|
+
|
|
372
|
+
const typeMap = new Map(
|
|
373
|
+
(
|
|
374
|
+
await this.prismaService.integration_type.findMany({
|
|
375
|
+
select: { id: true, slug: true },
|
|
376
|
+
})
|
|
377
|
+
).map((row) => [row.slug.toLowerCase(), row]),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const providerMap = new Map(
|
|
381
|
+
(
|
|
382
|
+
await this.prismaService.integration_provider.findMany({
|
|
383
|
+
select: { id: true, slug: true, type_id: true },
|
|
384
|
+
})
|
|
385
|
+
).map((row) => [row.slug.toLowerCase(), row]),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const firstSlugIndex = new Map<string, number>();
|
|
389
|
+
const validItems: Omit<ValidatedImportItem, 'has_conflict'>[] = [];
|
|
390
|
+
const invalidItems: InvalidImportItem[] = [];
|
|
391
|
+
|
|
392
|
+
envelope.data.forEach((item, index) => {
|
|
393
|
+
const errors: string[] = [];
|
|
394
|
+
const slug = String(item?.slug ?? '').trim();
|
|
395
|
+
const name = String(item?.name ?? '').trim();
|
|
396
|
+
const typeSlug = String(item?.type_slug ?? '')
|
|
397
|
+
.trim()
|
|
398
|
+
.toLowerCase();
|
|
399
|
+
const providerSlug = String(item?.provider_slug ?? '')
|
|
400
|
+
.trim()
|
|
401
|
+
.toLowerCase();
|
|
402
|
+
|
|
403
|
+
if (!slug) errors.push('Missing slug.');
|
|
404
|
+
if (!name) errors.push(`Missing name for slug "${slug || index}".`);
|
|
405
|
+
if (!typeSlug) errors.push(`Missing type_slug for slug "${slug || index}".`);
|
|
406
|
+
if (!providerSlug) {
|
|
407
|
+
errors.push(`Missing provider_slug for slug "${slug || index}".`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (slug) {
|
|
411
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
|
|
412
|
+
errors.push(`Invalid slug format "${slug}".`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (firstSlugIndex.has(slug)) {
|
|
416
|
+
const originalIndex = firstSlugIndex.get(slug);
|
|
417
|
+
errors.push(
|
|
418
|
+
`Duplicate slug "${slug}" in file (already used at index ${originalIndex}).`,
|
|
419
|
+
);
|
|
420
|
+
} else {
|
|
421
|
+
firstSlugIndex.set(slug, index);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const type = typeMap.get(typeSlug);
|
|
426
|
+
if (!type && typeSlug) {
|
|
427
|
+
errors.push(`Unknown type_slug "${typeSlug}".`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const provider = providerMap.get(providerSlug);
|
|
431
|
+
if (!provider && providerSlug) {
|
|
432
|
+
errors.push(`Unknown provider_slug "${providerSlug}".`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (type && provider && provider.type_id !== type.id) {
|
|
436
|
+
errors.push(
|
|
437
|
+
`Provider "${providerSlug}" does not belong to type "${typeSlug}".`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (
|
|
442
|
+
item?.config !== null &&
|
|
443
|
+
item?.config !== undefined &&
|
|
444
|
+
(typeof item.config !== 'object' || Array.isArray(item.config))
|
|
445
|
+
) {
|
|
446
|
+
errors.push(`Invalid config for slug "${slug || index}".`);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (errors.length > 0) {
|
|
450
|
+
invalidItems.push({ index, slug: slug || undefined, errors });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
validItems.push({
|
|
455
|
+
index,
|
|
456
|
+
slug,
|
|
457
|
+
name,
|
|
458
|
+
type_slug: typeSlug,
|
|
459
|
+
provider_slug: providerSlug,
|
|
460
|
+
config: (item.config as Record<string, unknown> | null) ?? null,
|
|
461
|
+
is_active: item.is_active ?? true,
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const existingSlugs = new Set(
|
|
466
|
+
(
|
|
467
|
+
await this.prismaService.integration_profile.findMany({
|
|
468
|
+
where: {
|
|
469
|
+
slug: {
|
|
470
|
+
in: validItems.map((item) => item.slug),
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
select: { slug: true },
|
|
474
|
+
})
|
|
475
|
+
).map((row) => row.slug),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
const profiles: ValidatedImportItem[] = validItems.map((item) => ({
|
|
479
|
+
...item,
|
|
480
|
+
has_conflict: existingSlugs.has(item.slug),
|
|
481
|
+
}));
|
|
482
|
+
|
|
483
|
+
const conflictSlugs = profiles
|
|
484
|
+
.filter((item) => item.has_conflict)
|
|
485
|
+
.map((item) => item.slug);
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
format: {
|
|
489
|
+
type: envelope.type,
|
|
490
|
+
version: envelope.version,
|
|
491
|
+
generated_at: envelope.generated_at,
|
|
492
|
+
include_secrets: envelope.include_secrets,
|
|
493
|
+
},
|
|
494
|
+
total_profiles: envelope.data.length,
|
|
495
|
+
valid_profiles: profiles.length,
|
|
496
|
+
invalid_profiles: invalidItems.length,
|
|
497
|
+
conflict_profiles: conflictSlugs.length,
|
|
498
|
+
importable_profiles: profiles.length - conflictSlugs.length,
|
|
499
|
+
conflict_slugs: conflictSlugs,
|
|
500
|
+
profiles,
|
|
501
|
+
invalid_items: invalidItems,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async confirmImport({
|
|
506
|
+
profiles,
|
|
507
|
+
conflictResolutions,
|
|
508
|
+
}: {
|
|
509
|
+
profiles: ImportProfileItemDTO[];
|
|
510
|
+
conflictResolutions?: ConflictResolutionDTO[];
|
|
511
|
+
}) {
|
|
512
|
+
if (!profiles?.length) {
|
|
513
|
+
throw new BadRequestException('No profiles provided for import.');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const resolutionMap = new Map<string, ConflictResolutionDTO['action']>(
|
|
517
|
+
(conflictResolutions ?? []).map((resolution) => [
|
|
518
|
+
resolution.slug,
|
|
519
|
+
resolution.action,
|
|
520
|
+
]),
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const typeMap = new Map(
|
|
524
|
+
(
|
|
525
|
+
await this.prismaService.integration_type.findMany({
|
|
526
|
+
select: { id: true, slug: true },
|
|
527
|
+
})
|
|
528
|
+
).map((row) => [row.slug.toLowerCase(), row]),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const providerMap = new Map(
|
|
532
|
+
(
|
|
533
|
+
await this.prismaService.integration_provider.findMany({
|
|
534
|
+
select: { id: true, slug: true, type_id: true },
|
|
535
|
+
})
|
|
536
|
+
).map((row) => [row.slug.toLowerCase(), row]),
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
const duplicatedSlugs = new Set<string>();
|
|
540
|
+
const seenSlugs = new Set<string>();
|
|
541
|
+
profiles.forEach((item) => {
|
|
542
|
+
const slug = item.slug.trim();
|
|
543
|
+
if (seenSlugs.has(slug)) {
|
|
544
|
+
duplicatedSlugs.add(slug);
|
|
545
|
+
}
|
|
546
|
+
seenSlugs.add(slug);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
if (duplicatedSlugs.size > 0) {
|
|
550
|
+
throw new BadRequestException(
|
|
551
|
+
`File has duplicated slugs: ${Array.from(duplicatedSlugs).join(', ')}`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const existingBySlug = new Map(
|
|
556
|
+
(
|
|
557
|
+
await this.prismaService.integration_profile.findMany({
|
|
558
|
+
where: { slug: { in: profiles.map((profile) => profile.slug.trim()) } },
|
|
559
|
+
select: { id: true, slug: true },
|
|
560
|
+
})
|
|
561
|
+
).map((row) => [row.slug, row]),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
let created = 0;
|
|
565
|
+
let replaced = 0;
|
|
566
|
+
let ignored = 0;
|
|
567
|
+
let renamed = 0;
|
|
568
|
+
|
|
569
|
+
for (const item of profiles) {
|
|
570
|
+
const slug = item.slug.trim();
|
|
571
|
+
const name = item.name.trim();
|
|
572
|
+
const typeSlug = item.type_slug.trim().toLowerCase();
|
|
573
|
+
const providerSlug = item.provider_slug.trim().toLowerCase();
|
|
574
|
+
|
|
575
|
+
if (!slug || !name || !typeSlug || !providerSlug) {
|
|
576
|
+
throw new BadRequestException(
|
|
577
|
+
`Invalid profile payload for slug "${slug || '<empty>'}".`,
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const type = typeMap.get(typeSlug);
|
|
582
|
+
if (!type) {
|
|
583
|
+
throw new BadRequestException(`Unknown type_slug "${typeSlug}".`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const provider = providerMap.get(providerSlug);
|
|
587
|
+
if (!provider) {
|
|
588
|
+
throw new BadRequestException(`Unknown provider_slug "${providerSlug}".`);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (provider.type_id !== type.id) {
|
|
592
|
+
throw new BadRequestException(
|
|
593
|
+
`Provider "${providerSlug}" does not belong to type "${typeSlug}".`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const existing = existingBySlug.get(slug);
|
|
598
|
+
const data = {
|
|
599
|
+
slug,
|
|
600
|
+
name,
|
|
601
|
+
type_id: type.id,
|
|
602
|
+
provider_id: provider.id,
|
|
603
|
+
config: (item.config as object | null) ?? null,
|
|
604
|
+
is_active: item.is_active ?? true,
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
if (!existing) {
|
|
608
|
+
await this.prismaService.integration_profile.create({ data });
|
|
609
|
+
created += 1;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const action = resolutionMap.get(slug) ?? 'ignore';
|
|
614
|
+
if (action === 'ignore') {
|
|
615
|
+
ignored += 1;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (action === 'replace') {
|
|
620
|
+
await this.prismaService.integration_profile.update({
|
|
621
|
+
where: { id: existing.id },
|
|
622
|
+
data: {
|
|
623
|
+
name: data.name,
|
|
624
|
+
type_id: data.type_id,
|
|
625
|
+
provider_id: data.provider_id,
|
|
626
|
+
config: data.config,
|
|
627
|
+
is_active: data.is_active,
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
replaced += 1;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const newSlug = await this.generateUniqueSlug(slug);
|
|
635
|
+
await this.prismaService.integration_profile.create({
|
|
636
|
+
data: {
|
|
637
|
+
...data,
|
|
638
|
+
slug: newSlug,
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
renamed += 1;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
success: true,
|
|
646
|
+
total_received: profiles.length,
|
|
647
|
+
created,
|
|
648
|
+
replaced,
|
|
649
|
+
renamed,
|
|
650
|
+
ignored,
|
|
651
|
+
imported: created + replaced + renamed,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
private async ensureSlugIsAvailable(slug: string) {
|
|
656
|
+
const existing = await this.prismaService.integration_profile.findUnique({
|
|
657
|
+
where: { slug },
|
|
658
|
+
select: { id: true },
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
if (existing) {
|
|
662
|
+
throw new BadRequestException(
|
|
663
|
+
`Integration profile with slug "${slug}" already exists.`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
private async validateTypeAndProvider(type_id: number, provider_id: number) {
|
|
669
|
+
const provider = await this.prismaService.integration_provider.findUnique({
|
|
670
|
+
where: { id: provider_id },
|
|
671
|
+
select: { id: true, type_id: true },
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
if (!provider) {
|
|
675
|
+
throw new BadRequestException(
|
|
676
|
+
`Provider with id ${provider_id} not found.`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (provider.type_id !== type_id) {
|
|
681
|
+
throw new BadRequestException(
|
|
682
|
+
`Provider ${provider_id} does not belong to type ${type_id}.`,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private parseImportEnvelope(file: MulterFile): IntegrationProfileImportEnvelope {
|
|
688
|
+
let parsed: unknown;
|
|
689
|
+
try {
|
|
690
|
+
const decompressed = pako.ungzip(file.buffer, { to: 'string' });
|
|
691
|
+
parsed = JSON.parse(decompressed);
|
|
692
|
+
} catch {
|
|
693
|
+
throw new BadRequestException(
|
|
694
|
+
'File is not a valid .hedhog archive or is corrupted.',
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
699
|
+
throw new BadRequestException('Invalid .hedhog payload format.');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const payload = parsed as Partial<IntegrationProfileImportEnvelope>;
|
|
703
|
+
if (payload.type !== 'integration-profile') {
|
|
704
|
+
throw new BadRequestException(
|
|
705
|
+
`Invalid .hedhog type "${String(payload.type ?? '')}".`,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (payload.version !== 1) {
|
|
710
|
+
throw new BadRequestException(
|
|
711
|
+
`Unsupported .hedhog version "${String(payload.version ?? '')}".`,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (!Array.isArray(payload.data)) {
|
|
716
|
+
throw new BadRequestException('Invalid .hedhog data: expected an array.');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
type: 'integration-profile',
|
|
721
|
+
version: 1,
|
|
722
|
+
generated_at:
|
|
723
|
+
typeof payload.generated_at === 'string'
|
|
724
|
+
? payload.generated_at
|
|
725
|
+
: new Date().toISOString(),
|
|
726
|
+
include_secrets: payload.include_secrets === true,
|
|
727
|
+
data: payload.data as IntegrationProfileExportItem[],
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
private sanitizeConfigForExport(
|
|
732
|
+
providerSlug: string,
|
|
733
|
+
config: Record<string, unknown> | null,
|
|
734
|
+
includeSecrets: boolean,
|
|
735
|
+
): Record<string, unknown> | null {
|
|
736
|
+
if (!config || includeSecrets) return config;
|
|
737
|
+
|
|
738
|
+
const hiddenKeys = this.getSensitiveConfigKeys(providerSlug);
|
|
739
|
+
if (hiddenKeys.length === 0) return config;
|
|
740
|
+
|
|
741
|
+
const sanitized = { ...config };
|
|
742
|
+
hiddenKeys.forEach((key) => {
|
|
743
|
+
delete sanitized[key];
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
return sanitized;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private getSensitiveConfigKeys(providerSlug: string): string[] {
|
|
750
|
+
const map: Record<string, string[]> = {
|
|
751
|
+
smtp: ['password'],
|
|
752
|
+
gmail: ['client_secret', 'refresh_token'],
|
|
753
|
+
ses: ['secret_access_key'],
|
|
754
|
+
'evolution-api': ['token'],
|
|
755
|
+
'whatsapp-official': ['access_token'],
|
|
756
|
+
s3: ['secret_access_key'],
|
|
757
|
+
gcs: ['key_file_json'],
|
|
758
|
+
'azure-blob': ['key'],
|
|
759
|
+
's3-compatible': ['secret_access_key'],
|
|
760
|
+
openai: ['api_key'],
|
|
761
|
+
gemini: ['api_key'],
|
|
762
|
+
claude: ['api_key'],
|
|
763
|
+
'google-oauth': ['client_secret'],
|
|
764
|
+
'github-oauth': ['client_secret'],
|
|
765
|
+
'microsoft-oauth': ['client_secret'],
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
return map[providerSlug.toLowerCase()] ?? [];
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private async generateUniqueSlug(baseSlug: string): Promise<string> {
|
|
772
|
+
let candidate = `${baseSlug}-copy`;
|
|
773
|
+
let suffix = 1;
|
|
774
|
+
|
|
775
|
+
// Gera slug único determinístico para imports com conflito e opção de renomear.
|
|
776
|
+
while (
|
|
777
|
+
await this.prismaService.integration_profile.findUnique({
|
|
778
|
+
where: { slug: candidate },
|
|
779
|
+
select: { id: true },
|
|
780
|
+
})
|
|
781
|
+
) {
|
|
782
|
+
candidate = `${baseSlug}-copy-${suffix}`;
|
|
783
|
+
suffix += 1;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return candidate;
|
|
787
|
+
}
|
|
788
|
+
}
|