rez_core 2.2.154 → 2.2.156
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/app.module.js +2 -0
- package/dist/app.module.js.map +1 -1
- package/dist/constant/global.constant.d.ts +1 -1
- package/dist/constant/global.constant.js +1 -1
- package/dist/constant/global.constant.js.map +1 -1
- package/dist/module/auth/strategies/google.strategy.js +1 -1
- package/dist/module/auth/strategies/google.strategy.js.map +1 -1
- package/dist/module/communication/communication.module.d.ts +2 -0
- package/dist/module/communication/communication.module.js +69 -0
- package/dist/module/communication/communication.module.js.map +1 -0
- package/dist/module/communication/controller/communication.controller.d.ts +54 -0
- package/dist/module/communication/controller/communication.controller.js +148 -0
- package/dist/module/communication/controller/communication.controller.js.map +1 -0
- package/dist/module/communication/dto/create-config.dto.d.ts +91 -0
- package/dist/module/communication/dto/create-config.dto.js +243 -0
- package/dist/module/communication/dto/create-config.dto.js.map +1 -0
- package/dist/module/communication/entity/communication-config.entity.d.ts +44 -0
- package/dist/module/communication/entity/communication-config.entity.js +45 -0
- package/dist/module/communication/entity/communication-config.entity.js.map +1 -0
- package/dist/module/communication/entity/communication-hub.entity.d.ts +20 -0
- package/dist/module/communication/entity/communication-hub.entity.js +105 -0
- package/dist/module/communication/entity/communication-hub.entity.js.map +1 -0
- package/dist/module/communication/examples/usage.example.d.ts +11 -0
- package/dist/module/communication/examples/usage.example.js +89 -0
- package/dist/module/communication/examples/usage.example.js.map +1 -0
- package/dist/module/communication/factories/base.factory.d.ts +9 -0
- package/dist/module/communication/factories/base.factory.js +3 -0
- package/dist/module/communication/factories/base.factory.js.map +1 -0
- package/dist/module/communication/factories/communication.factory.d.ts +33 -0
- package/dist/module/communication/factories/communication.factory.js +104 -0
- package/dist/module/communication/factories/communication.factory.js.map +1 -0
- package/dist/module/communication/factories/email.factory.d.ts +19 -0
- package/dist/module/communication/factories/email.factory.js +61 -0
- package/dist/module/communication/factories/email.factory.js.map +1 -0
- package/dist/module/communication/factories/sms.factory.d.ts +15 -0
- package/dist/module/communication/factories/sms.factory.js +49 -0
- package/dist/module/communication/factories/sms.factory.js.map +1 -0
- package/dist/module/communication/factories/telephone.factory.d.ts +13 -0
- package/dist/module/communication/factories/telephone.factory.js +43 -0
- package/dist/module/communication/factories/telephone.factory.js.map +1 -0
- package/dist/module/communication/factories/whatsapp.factory.d.ts +13 -0
- package/dist/module/communication/factories/whatsapp.factory.js +43 -0
- package/dist/module/communication/factories/whatsapp.factory.js.map +1 -0
- package/dist/module/communication/service/communication.service.d.ts +111 -0
- package/dist/module/communication/service/communication.service.js +726 -0
- package/dist/module/communication/service/communication.service.js.map +1 -0
- package/dist/module/communication/service/oauth.service.d.ts +18 -0
- package/dist/module/communication/service/oauth.service.js +185 -0
- package/dist/module/communication/service/oauth.service.js.map +1 -0
- package/dist/module/communication/strategies/communication.strategy.d.ts +17 -0
- package/dist/module/communication/strategies/communication.strategy.js +3 -0
- package/dist/module/communication/strategies/communication.strategy.js.map +1 -0
- package/dist/module/communication/strategies/email/gmail-api.strategy.d.ts +7 -0
- package/dist/module/communication/strategies/email/gmail-api.strategy.js +135 -0
- package/dist/module/communication/strategies/email/gmail-api.strategy.js.map +1 -0
- package/dist/module/communication/strategies/email/gmail-smtp.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/email/gmail-smtp.strategy.js +49 -0
- package/dist/module/communication/strategies/email/gmail-smtp.strategy.js.map +1 -0
- package/dist/module/communication/strategies/email/outlook-api.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/email/outlook-api.strategy.js +44 -0
- package/dist/module/communication/strategies/email/outlook-api.strategy.js.map +1 -0
- package/dist/module/communication/strategies/gmail-smtp.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/gmail-smtp.strategy.js +61 -0
- package/dist/module/communication/strategies/gmail-smtp.strategy.js.map +1 -0
- package/dist/module/communication/strategies/gmail.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/gmail.strategy.js +71 -0
- package/dist/module/communication/strategies/gmail.strategy.js.map +1 -0
- package/dist/module/communication/strategies/knowlarity.strategy.d.ts +6 -0
- package/dist/module/communication/strategies/knowlarity.strategy.js +115 -0
- package/dist/module/communication/strategies/knowlarity.strategy.js.map +1 -0
- package/dist/module/communication/strategies/outlook-smtp.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/outlook-smtp.strategy.js +66 -0
- package/dist/module/communication/strategies/outlook-smtp.strategy.js.map +1 -0
- package/dist/module/communication/strategies/outlook.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/outlook.strategy.js +64 -0
- package/dist/module/communication/strategies/outlook.strategy.js.map +1 -0
- package/dist/module/communication/strategies/sms/knowlarity.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/sms/knowlarity.strategy.js +44 -0
- package/dist/module/communication/strategies/sms/knowlarity.strategy.js.map +1 -0
- package/dist/module/communication/strategies/sms/twilio.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/sms/twilio.strategy.js +44 -0
- package/dist/module/communication/strategies/sms/twilio.strategy.js.map +1 -0
- package/dist/module/communication/strategies/sms.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/sms.strategy.js +50 -0
- package/dist/module/communication/strategies/sms.strategy.js.map +1 -0
- package/dist/module/communication/strategies/telephone/knowlarity-voice.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/telephone/knowlarity-voice.strategy.js +44 -0
- package/dist/module/communication/strategies/telephone/knowlarity-voice.strategy.js.map +1 -0
- package/dist/module/communication/strategies/whatsapp/whatsapp-cloud.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/whatsapp/whatsapp-cloud.strategy.js +47 -0
- package/dist/module/communication/strategies/whatsapp/whatsapp-cloud.strategy.js.map +1 -0
- package/dist/module/communication/strategies/whatsapp.strategy.d.ts +5 -0
- package/dist/module/communication/strategies/whatsapp.strategy.js +58 -0
- package/dist/module/communication/strategies/whatsapp.strategy.js.map +1 -0
- package/dist/module/meta/entity.module.js +2 -1
- package/dist/module/meta/entity.module.js.map +1 -1
- package/dist/module/user/controller/login.controller.d.ts +4 -2
- package/dist/module/user/controller/login.controller.js +26 -4
- package/dist/module/user/controller/login.controller.js.map +1 -1
- package/dist/module/workflow/service/populate-workflow.service.js +2 -2
- package/dist/module/workflow/service/populate-workflow.service.js.map +1 -1
- package/dist/module/workflow/service/stage.service.js +1 -1
- package/dist/module/workflow/service/stage.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -1
- package/src/app.module.ts +2 -0
- package/src/constant/global.constant.ts +1 -1
- package/src/module/auth/strategies/google.strategy.ts +1 -1
- package/src/module/communication/communication.module.ts +77 -0
- package/src/module/communication/controller/communication.controller.ts +122 -0
- package/src/module/communication/dto/create-config.dto.ts +234 -0
- package/src/module/communication/entity/communication-config.entity.ts +80 -0
- package/src/module/communication/entity/communication-hub.entity.ts +77 -0
- package/src/module/communication/examples/usage.example.ts +169 -0
- package/src/module/communication/factories/base.factory.ts +7 -0
- package/src/module/communication/factories/communication.factory.ts +103 -0
- package/src/module/communication/factories/email.factory.ts +51 -0
- package/src/module/communication/factories/sms.factory.ts +41 -0
- package/src/module/communication/factories/telephone.factory.ts +34 -0
- package/src/module/communication/factories/whatsapp.factory.ts +34 -0
- package/src/module/communication/service/communication.service.ts +1118 -0
- package/src/module/communication/service/oauth.service.ts +203 -0
- package/src/module/communication/strategies/communication.strategy.ts +23 -0
- package/src/module/communication/strategies/email/gmail-api.strategy.ts +161 -0
- package/src/module/communication/strategies/email/gmail-smtp.strategy.ts +51 -0
- package/src/module/communication/strategies/email/outlook-api.strategy.ts +44 -0
- package/src/module/communication/strategies/gmail-smtp.strategy.ts +64 -0
- package/src/module/communication/strategies/gmail.strategy.ts +68 -0
- package/src/module/communication/strategies/knowlarity.strategy.ts +124 -0
- package/src/module/communication/strategies/outlook-smtp.strategy.ts +69 -0
- package/src/module/communication/strategies/outlook.strategy.ts +57 -0
- package/src/module/communication/strategies/sms/knowlarity.strategy.ts +44 -0
- package/src/module/communication/strategies/sms/twilio.strategy.ts +44 -0
- package/src/module/communication/strategies/sms.strategy.ts +44 -0
- package/src/module/communication/strategies/telephone/knowlarity-voice.strategy.ts +44 -0
- package/src/module/communication/strategies/whatsapp/whatsapp-cloud.strategy.ts +49 -0
- package/src/module/communication/strategies/whatsapp.strategy.ts +53 -0
- package/src/module/meta/entity.module.ts +2 -1
- package/src/module/user/controller/login.controller.ts +34 -3
- package/src/module/workflow/service/populate-workflow.service.ts +3 -3
- package/src/module/workflow/service/stage.service.ts +1 -1
- package/src/resources/dev.properties.yaml +1 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { InjectRepository } from '@nestjs/typeorm';
|
|
3
|
+
import { Repository } from 'typeorm';
|
|
4
|
+
import { ConfigService } from '@nestjs/config';
|
|
5
|
+
import { google } from 'googleapis';
|
|
6
|
+
import {
|
|
7
|
+
CommunicationHub,
|
|
8
|
+
CommunicationConfigType,
|
|
9
|
+
} from '../entity/communication-hub.entity';
|
|
10
|
+
import { CommunicationConfig } from '../entity/communication-config.entity';
|
|
11
|
+
import { CommunicationFactory } from '../factories/communication.factory';
|
|
12
|
+
import { CommunicationResult } from '../strategies/communication.strategy';
|
|
13
|
+
import { GmailApiStrategy } from '../strategies/email/gmail-api.strategy';
|
|
14
|
+
|
|
15
|
+
export interface SendMessageDto {
|
|
16
|
+
levelId: number;
|
|
17
|
+
levelType: string;
|
|
18
|
+
to: string;
|
|
19
|
+
message: string;
|
|
20
|
+
mode?: CommunicationConfigType;
|
|
21
|
+
priority?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface GenericMessageDto {
|
|
25
|
+
levelId: number;
|
|
26
|
+
levelType: string;
|
|
27
|
+
to: string | string[];
|
|
28
|
+
message: string;
|
|
29
|
+
subject?: string;
|
|
30
|
+
type?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE';
|
|
31
|
+
priority?: 'high' | 'medium' | 'low';
|
|
32
|
+
cc?: string | string[];
|
|
33
|
+
bcc?: string | string[];
|
|
34
|
+
html?: string;
|
|
35
|
+
attachments?: any[];
|
|
36
|
+
mediaUrl?: string;
|
|
37
|
+
templateId?: string;
|
|
38
|
+
variables?: Record<string, any>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface BulkMessageDto {
|
|
42
|
+
levelId: number;
|
|
43
|
+
levelType: string;
|
|
44
|
+
recipients: string[];
|
|
45
|
+
message: string;
|
|
46
|
+
subject?: string;
|
|
47
|
+
type?: 'EMAIL' | 'SMS' | 'WA' | 'TELEPHONE';
|
|
48
|
+
priority?: 'high' | 'medium' | 'low';
|
|
49
|
+
html?: string;
|
|
50
|
+
templateId?: string;
|
|
51
|
+
variables?: Record<string, any>;
|
|
52
|
+
batchSize?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface CommunicationHubWithConfig extends CommunicationHub {
|
|
56
|
+
config: CommunicationConfig;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface GmailOAuthState {
|
|
60
|
+
levelId: number;
|
|
61
|
+
levelType: string;
|
|
62
|
+
email?: string;
|
|
63
|
+
timestamp: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface GmailSSOResult {
|
|
67
|
+
hubId: number;
|
|
68
|
+
configId: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Injectable()
|
|
72
|
+
export class CommunicationService {
|
|
73
|
+
private readonly logger = new Logger(CommunicationService.name);
|
|
74
|
+
private readonly gmailOAuthStates = new Map<string, GmailOAuthState>();
|
|
75
|
+
|
|
76
|
+
constructor(
|
|
77
|
+
@InjectRepository(CommunicationHub)
|
|
78
|
+
private readonly hubRepository: Repository<CommunicationHub>,
|
|
79
|
+
|
|
80
|
+
@InjectRepository(CommunicationConfig)
|
|
81
|
+
private readonly configRepository: Repository<CommunicationConfig>,
|
|
82
|
+
|
|
83
|
+
private readonly communicationFactory: CommunicationFactory,
|
|
84
|
+
private readonly gmailApiStrategy: GmailApiStrategy,
|
|
85
|
+
private readonly configService: ConfigService,
|
|
86
|
+
) {}
|
|
87
|
+
|
|
88
|
+
async sendMessage({
|
|
89
|
+
levelId,
|
|
90
|
+
levelType,
|
|
91
|
+
to,
|
|
92
|
+
message,
|
|
93
|
+
mode,
|
|
94
|
+
priority = 1,
|
|
95
|
+
}: SendMessageDto): Promise<CommunicationResult> {
|
|
96
|
+
try {
|
|
97
|
+
// Get active communication hubs for the level
|
|
98
|
+
const hubs = await this.getActiveHubs(levelId, levelType, mode);
|
|
99
|
+
|
|
100
|
+
if (!hubs.length) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`No active communication configuration found for ${levelType} ${levelId}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Sort by priority if provided
|
|
107
|
+
const sortedHubs = this.sortHubsByPriority(hubs, priority);
|
|
108
|
+
|
|
109
|
+
// Try each hub until one succeeds
|
|
110
|
+
for (const hub of sortedHubs) {
|
|
111
|
+
try {
|
|
112
|
+
const result = await this.sendViaHub(hub, to, message);
|
|
113
|
+
if (result.success) {
|
|
114
|
+
this.logger.log(
|
|
115
|
+
`Message sent successfully via ${hub.provider}/${hub.service}`,
|
|
116
|
+
);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.logger.warn(
|
|
121
|
+
`Failed to send via ${hub.provider}/${hub.service}: ${result.error}`,
|
|
122
|
+
);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.logger.error(
|
|
125
|
+
`Error sending via ${hub.provider}/${hub.service}:`,
|
|
126
|
+
error.message,
|
|
127
|
+
);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
throw new Error('All communication providers failed');
|
|
133
|
+
} catch (error) {
|
|
134
|
+
this.logger.error('Communication service error:', error.message);
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async getActiveHubs(
|
|
140
|
+
levelId: number,
|
|
141
|
+
levelType: string,
|
|
142
|
+
mode?: CommunicationConfigType,
|
|
143
|
+
): Promise<CommunicationHubWithConfig[]> {
|
|
144
|
+
let query = this.hubRepository
|
|
145
|
+
.createQueryBuilder('hub')
|
|
146
|
+
.leftJoin('communication_config', 'config', 'config.id = hub.config_id')
|
|
147
|
+
.select([
|
|
148
|
+
'hub.id',
|
|
149
|
+
'hub.status',
|
|
150
|
+
'hub.config_id',
|
|
151
|
+
'hub.communication_config_type',
|
|
152
|
+
'hub.service',
|
|
153
|
+
'hub.provider',
|
|
154
|
+
'hub.priority',
|
|
155
|
+
'hub.is_default',
|
|
156
|
+
'hub.created_at',
|
|
157
|
+
'hub.updated_at',
|
|
158
|
+
'config.id as config_id',
|
|
159
|
+
'config.config_json as config_json',
|
|
160
|
+
'config.created_at as config_created_at',
|
|
161
|
+
'config.updated_at as config_updated_at'
|
|
162
|
+
])
|
|
163
|
+
.where('hub.level_id = :levelId', { levelId })
|
|
164
|
+
.andWhere('hub.level_type = :levelType', { levelType })
|
|
165
|
+
.andWhere('hub.status = :status', { status: 1 });
|
|
166
|
+
|
|
167
|
+
if (mode) {
|
|
168
|
+
query = query.andWhere('hub.communication_config_type = :mode', { mode });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const results = await query.getRawMany();
|
|
172
|
+
|
|
173
|
+
// Transform the raw results into the expected format
|
|
174
|
+
const hubsWithConfig: CommunicationHubWithConfig[] = results.map(row => ({
|
|
175
|
+
id: row.hub_id,
|
|
176
|
+
level_id: row.hub_level_id,
|
|
177
|
+
level_type: row.hub_level_type,
|
|
178
|
+
status: row.hub_status,
|
|
179
|
+
config_id: row.hub_config_id,
|
|
180
|
+
communication_config_type: row.hub_communication_config_type,
|
|
181
|
+
service: row.hub_service,
|
|
182
|
+
provider: row.hub_provider,
|
|
183
|
+
priority: row.hub_priority || 1,
|
|
184
|
+
is_default: row.hub_is_default || false,
|
|
185
|
+
created_at: row.hub_created_at,
|
|
186
|
+
updated_at: row.hub_updated_at,
|
|
187
|
+
config: {
|
|
188
|
+
id: row.config_id,
|
|
189
|
+
config_json: row.config_json,
|
|
190
|
+
created_at: row.config_created_at,
|
|
191
|
+
updated_at: row.config_updated_at,
|
|
192
|
+
},
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
return hubsWithConfig;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private sortHubsByPriority(
|
|
199
|
+
hubs: CommunicationHubWithConfig[],
|
|
200
|
+
_priority: number,
|
|
201
|
+
): CommunicationHubWithConfig[] {
|
|
202
|
+
// For now, just return as-is. In the future, implement priority logic
|
|
203
|
+
return hubs;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private async sendViaHub(
|
|
207
|
+
hub: CommunicationHubWithConfig,
|
|
208
|
+
to: string,
|
|
209
|
+
message: string,
|
|
210
|
+
): Promise<CommunicationResult> {
|
|
211
|
+
const strategy = this.communicationFactory.create(
|
|
212
|
+
hub.communication_config_type,
|
|
213
|
+
hub.service,
|
|
214
|
+
hub.provider,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return strategy.sendMessage(to, message, hub.config.config_json);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async createCommunicationConfig(
|
|
221
|
+
levelId: number,
|
|
222
|
+
levelType: string,
|
|
223
|
+
configType: CommunicationConfigType,
|
|
224
|
+
service: string,
|
|
225
|
+
provider: string,
|
|
226
|
+
config: any,
|
|
227
|
+
priority?: number,
|
|
228
|
+
is_default?: boolean,
|
|
229
|
+
): Promise<CommunicationHub | { authUrl: string; state: string; message: string }> {
|
|
230
|
+
// Validate service/provider combination using supported combinations
|
|
231
|
+
const supportedCombinations = await this.getSupportedCombinations();
|
|
232
|
+
const isValidCombination = supportedCombinations.some(
|
|
233
|
+
combo => combo.mode === configType &&
|
|
234
|
+
combo.service.toLowerCase() === service.toLowerCase() &&
|
|
235
|
+
combo.provider.toLowerCase() === provider.toLowerCase()
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (!isValidCombination) {
|
|
239
|
+
throw new Error(`Unsupported combination: ${configType}/${service}/${provider}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if this requires OAuth flow
|
|
243
|
+
const requiresOAuth = this.requiresOAuthFlow(configType, service, provider, config);
|
|
244
|
+
|
|
245
|
+
if (requiresOAuth) {
|
|
246
|
+
// Generate OAuth URL and return it instead of creating config immediately
|
|
247
|
+
return await this.generateOAuthUrl(levelId, levelType, configType, service, provider, config, priority, is_default);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Direct config creation (non-OAuth flow)
|
|
251
|
+
return await this.createDirectConfig(levelId, levelType, configType, service, provider, config, priority, is_default);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async getSupportedCombinations(): Promise<
|
|
255
|
+
{
|
|
256
|
+
mode: string;
|
|
257
|
+
service: string;
|
|
258
|
+
provider: string;
|
|
259
|
+
}[]
|
|
260
|
+
> {
|
|
261
|
+
return this.communicationFactory.getAllSupportedCombinations();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async getLevelConfigs(
|
|
265
|
+
levelId: number,
|
|
266
|
+
levelType: string,
|
|
267
|
+
): Promise<CommunicationHub[]> {
|
|
268
|
+
return this.hubRepository.find({
|
|
269
|
+
where: { level_id: levelId, level_type: levelType },
|
|
270
|
+
order: { created_at: 'DESC' },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async updateConfigStatus(hubId: number, status: number): Promise<void> {
|
|
275
|
+
await this.hubRepository.update(hubId, { status });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async sendGenericMessage(messageDto: GenericMessageDto): Promise<CommunicationResult> {
|
|
279
|
+
try {
|
|
280
|
+
const {
|
|
281
|
+
levelId,
|
|
282
|
+
levelType,
|
|
283
|
+
to,
|
|
284
|
+
message,
|
|
285
|
+
type,
|
|
286
|
+
priority = 'medium',
|
|
287
|
+
subject,
|
|
288
|
+
html,
|
|
289
|
+
cc,
|
|
290
|
+
bcc,
|
|
291
|
+
attachments,
|
|
292
|
+
mediaUrl,
|
|
293
|
+
templateId,
|
|
294
|
+
variables
|
|
295
|
+
} = messageDto;
|
|
296
|
+
|
|
297
|
+
// Auto-detect communication type if not specified
|
|
298
|
+
const communicationType = this.detectCommunicationType(to, type);
|
|
299
|
+
|
|
300
|
+
// Get active hubs for the detected type
|
|
301
|
+
const hubs = await this.getActiveHubs(levelId, levelType, communicationType);
|
|
302
|
+
|
|
303
|
+
if (!hubs.length) {
|
|
304
|
+
throw new Error(`No active ${communicationType} configuration found for ${levelType} ${levelId}`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Sort hubs by priority and default preference
|
|
308
|
+
const sortedHubs = this.sortHubsByPriorityAndDefault(hubs, priority);
|
|
309
|
+
|
|
310
|
+
// Prepare enhanced config with additional parameters
|
|
311
|
+
const enhancedConfig = {
|
|
312
|
+
subject,
|
|
313
|
+
html,
|
|
314
|
+
cc,
|
|
315
|
+
bcc,
|
|
316
|
+
attachments,
|
|
317
|
+
mediaUrl,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Handle multiple recipients by sending to each individually
|
|
321
|
+
if (Array.isArray(to)) {
|
|
322
|
+
const results: CommunicationResult[] = [];
|
|
323
|
+
let hasSuccess = false;
|
|
324
|
+
|
|
325
|
+
for (const recipient of to) {
|
|
326
|
+
for (const hub of sortedHubs) {
|
|
327
|
+
try {
|
|
328
|
+
const strategy = this.communicationFactory.create(
|
|
329
|
+
hub.communication_config_type,
|
|
330
|
+
hub.service,
|
|
331
|
+
hub.provider,
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// Process template if provided
|
|
335
|
+
let processedMessage = message;
|
|
336
|
+
if (templateId && variables) {
|
|
337
|
+
processedMessage = this.processTemplate(message, variables);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Merge config with enhanced parameters
|
|
341
|
+
const finalConfig = { ...hub.config.config_json, ...enhancedConfig };
|
|
342
|
+
|
|
343
|
+
const result = await strategy.sendMessage(recipient, processedMessage, finalConfig);
|
|
344
|
+
|
|
345
|
+
if (result.success) {
|
|
346
|
+
this.logger.log(
|
|
347
|
+
`Generic message sent successfully via ${hub.provider}/${hub.service} to ${recipient}`,
|
|
348
|
+
);
|
|
349
|
+
results.push(result);
|
|
350
|
+
hasSuccess = true;
|
|
351
|
+
break; // Move to next recipient
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.logger.warn(
|
|
355
|
+
`Failed to send via ${hub.provider}/${hub.service} to ${recipient}: ${result.error}`,
|
|
356
|
+
);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
this.logger.error(
|
|
359
|
+
`Error sending via ${hub.provider}/${hub.service} to ${recipient}:`,
|
|
360
|
+
error.message,
|
|
361
|
+
);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (hasSuccess) {
|
|
368
|
+
return {
|
|
369
|
+
success: true,
|
|
370
|
+
messageId: results.map(r => r.messageId).join(','),
|
|
371
|
+
provider: 'multiple',
|
|
372
|
+
service: 'multiple',
|
|
373
|
+
timestamp: new Date(),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
// Handle single recipient
|
|
378
|
+
for (const hub of sortedHubs) {
|
|
379
|
+
try {
|
|
380
|
+
const strategy = this.communicationFactory.create(
|
|
381
|
+
hub.communication_config_type,
|
|
382
|
+
hub.service,
|
|
383
|
+
hub.provider,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Process template if provided
|
|
387
|
+
let processedMessage = message;
|
|
388
|
+
if (templateId && variables) {
|
|
389
|
+
processedMessage = this.processTemplate(message, variables);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Merge config with enhanced parameters
|
|
393
|
+
const finalConfig = { ...hub.config.config_json, ...enhancedConfig };
|
|
394
|
+
|
|
395
|
+
const result = await strategy.sendMessage(to, processedMessage, finalConfig);
|
|
396
|
+
|
|
397
|
+
if (result.success) {
|
|
398
|
+
this.logger.log(
|
|
399
|
+
`Generic message sent successfully via ${hub.provider}/${hub.service} to ${to}`,
|
|
400
|
+
);
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
this.logger.warn(
|
|
405
|
+
`Failed to send via ${hub.provider}/${hub.service}: ${result.error}`,
|
|
406
|
+
);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
this.logger.error(
|
|
409
|
+
`Error sending via ${hub.provider}/${hub.service}:`,
|
|
410
|
+
error.message,
|
|
411
|
+
);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
throw new Error('All communication providers failed');
|
|
418
|
+
} catch (error) {
|
|
419
|
+
this.logger.error('Generic communication service error:', error.message);
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async sendBulkMessage(bulkDto: BulkMessageDto): Promise<{ results: CommunicationResult[]; summary: any }> {
|
|
425
|
+
try {
|
|
426
|
+
const {
|
|
427
|
+
levelId,
|
|
428
|
+
levelType,
|
|
429
|
+
recipients,
|
|
430
|
+
message,
|
|
431
|
+
type,
|
|
432
|
+
priority = 'low',
|
|
433
|
+
subject,
|
|
434
|
+
html,
|
|
435
|
+
templateId,
|
|
436
|
+
variables,
|
|
437
|
+
batchSize = 10
|
|
438
|
+
} = bulkDto;
|
|
439
|
+
|
|
440
|
+
const results: CommunicationResult[] = [];
|
|
441
|
+
const batches = this.chunkArray(recipients, batchSize);
|
|
442
|
+
|
|
443
|
+
for (let i = 0; i < batches.length; i++) {
|
|
444
|
+
const batch = batches[i];
|
|
445
|
+
this.logger.log(`Processing batch ${i + 1}/${batches.length} with ${batch.length} recipients`);
|
|
446
|
+
|
|
447
|
+
const batchPromises = batch.map(async (recipient) => {
|
|
448
|
+
try {
|
|
449
|
+
const messageDto: GenericMessageDto = {
|
|
450
|
+
levelId,
|
|
451
|
+
levelType,
|
|
452
|
+
to: recipient,
|
|
453
|
+
message,
|
|
454
|
+
type,
|
|
455
|
+
priority,
|
|
456
|
+
subject,
|
|
457
|
+
html,
|
|
458
|
+
templateId,
|
|
459
|
+
variables,
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
return await this.sendGenericMessage(messageDto);
|
|
463
|
+
} catch (error) {
|
|
464
|
+
return {
|
|
465
|
+
success: false,
|
|
466
|
+
provider: 'unknown',
|
|
467
|
+
service: 'unknown',
|
|
468
|
+
error: error.message,
|
|
469
|
+
timestamp: new Date(),
|
|
470
|
+
} as CommunicationResult;
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const batchResults = await Promise.allSettled(batchPromises);
|
|
475
|
+
const processedResults = batchResults.map((result) => {
|
|
476
|
+
if (result.status === 'fulfilled') {
|
|
477
|
+
return result.value;
|
|
478
|
+
} else {
|
|
479
|
+
return {
|
|
480
|
+
success: false,
|
|
481
|
+
provider: 'unknown',
|
|
482
|
+
service: 'unknown',
|
|
483
|
+
error: result.reason?.message || 'Unknown error',
|
|
484
|
+
timestamp: new Date(),
|
|
485
|
+
} as CommunicationResult;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
results.push(...processedResults);
|
|
490
|
+
|
|
491
|
+
// Rate limiting delay between batches
|
|
492
|
+
if (i < batches.length - 1) {
|
|
493
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const summary = {
|
|
498
|
+
total: results.length,
|
|
499
|
+
successful: results.filter(r => r.success).length,
|
|
500
|
+
failed: results.filter(r => !r.success).length,
|
|
501
|
+
successRate: ((results.filter(r => r.success).length / results.length) * 100).toFixed(2) + '%',
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
this.logger.log(`Bulk message completed: ${summary.successful}/${summary.total} successful`);
|
|
505
|
+
|
|
506
|
+
return { results, summary };
|
|
507
|
+
} catch (error) {
|
|
508
|
+
this.logger.error('Bulk communication service error:', error.message);
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async scheduleMessage(scheduledDto: any): Promise<{ scheduled: boolean; scheduleId?: string }> {
|
|
514
|
+
// For now, return a placeholder. In production, integrate with a job queue like Bull or Agenda
|
|
515
|
+
this.logger.log(`Message scheduled for ${scheduledDto.scheduleFor}`);
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
scheduled: true,
|
|
519
|
+
scheduleId: `sched_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async sendTemplateMessage(templateDto: any): Promise<CommunicationResult> {
|
|
524
|
+
// Get template content (integrate with your template system)
|
|
525
|
+
const template = await this.getTemplate(templateDto.templateId);
|
|
526
|
+
|
|
527
|
+
const processedMessage = this.processTemplate(template.content, templateDto.variables);
|
|
528
|
+
const processedSubject = template.subject ? this.processTemplate(template.subject, templateDto.variables) : undefined;
|
|
529
|
+
|
|
530
|
+
const messageDto: GenericMessageDto = {
|
|
531
|
+
levelId: templateDto.levelId,
|
|
532
|
+
levelType: templateDto.levelType,
|
|
533
|
+
to: templateDto.to,
|
|
534
|
+
message: processedMessage,
|
|
535
|
+
subject: processedSubject,
|
|
536
|
+
type: templateDto.type,
|
|
537
|
+
templateId: templateDto.templateId,
|
|
538
|
+
variables: templateDto.variables,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
return this.sendGenericMessage(messageDto);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private detectCommunicationType(to: string | string[], type?: string): CommunicationConfigType {
|
|
545
|
+
if (type) {
|
|
546
|
+
return type as CommunicationConfigType;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const recipient = Array.isArray(to) ? to[0] : to;
|
|
550
|
+
|
|
551
|
+
// Email detection
|
|
552
|
+
if (recipient.includes('@')) {
|
|
553
|
+
return CommunicationConfigType.EMAIL;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Phone number detection (basic)
|
|
557
|
+
if (/^\+?[\d\s\-\(\)]+$/.test(recipient)) {
|
|
558
|
+
// Could be SMS or WhatsApp, default to SMS
|
|
559
|
+
return CommunicationConfigType.SMS;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Default to EMAIL if unsure
|
|
563
|
+
return CommunicationConfigType.EMAIL;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private sortHubsByPriorityAndDefault(
|
|
567
|
+
hubs: CommunicationHubWithConfig[],
|
|
568
|
+
_priority: string,
|
|
569
|
+
): CommunicationHubWithConfig[] {
|
|
570
|
+
return hubs.sort((a, b) => {
|
|
571
|
+
// First sort by default (true comes first)
|
|
572
|
+
if (a.is_default !== b.is_default) {
|
|
573
|
+
return b.is_default ? 1 : -1;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Then sort by priority (lower number = higher priority)
|
|
577
|
+
if (a.priority !== b.priority) {
|
|
578
|
+
return a.priority - b.priority;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Finally sort by created_at descending (newest first)
|
|
582
|
+
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private processTemplate(template: string, variables: Record<string, any>): string {
|
|
587
|
+
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
588
|
+
const value = variables[key];
|
|
589
|
+
return value !== undefined ? String(value) : match;
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private async getTemplate(templateId: string) {
|
|
594
|
+
// Placeholder for template system integration
|
|
595
|
+
// In production, this would connect to your template database/service
|
|
596
|
+
return {
|
|
597
|
+
id: templateId,
|
|
598
|
+
subject: 'Default Subject {{variable}}',
|
|
599
|
+
content: 'Hello {{name}}, this is a template message with {{variable}}.',
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private chunkArray<T>(array: T[], chunkSize: number): T[][] {
|
|
604
|
+
const chunks: T[][] = [];
|
|
605
|
+
for (let i = 0; i < array.length; i += chunkSize) {
|
|
606
|
+
chunks.push(array.slice(i, i + chunkSize));
|
|
607
|
+
}
|
|
608
|
+
return chunks;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async initGmailOAuth(
|
|
612
|
+
levelId: number,
|
|
613
|
+
levelType: string,
|
|
614
|
+
email?: string,
|
|
615
|
+
): Promise<{ authUrl: string; state: string }> {
|
|
616
|
+
try {
|
|
617
|
+
// Get system OAuth credentials from config
|
|
618
|
+
const clientId = this.configService.get<string>('CLIENT_ID');
|
|
619
|
+
const clientSecret = this.configService.get<string>('CLIENT_SECRET');
|
|
620
|
+
|
|
621
|
+
if (!clientId || !clientSecret) {
|
|
622
|
+
throw new Error('Gmail OAuth credentials not configured');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const state = this.generateSecureState();
|
|
626
|
+
// Use the existing registered callback URL
|
|
627
|
+
const callbackUrl = this.configService.get<string>('CALLBACK_URL') || 'http://localhost:5001/auth/google/callback';
|
|
628
|
+
|
|
629
|
+
this.gmailOAuthStates.set(state, {
|
|
630
|
+
levelId,
|
|
631
|
+
levelType,
|
|
632
|
+
email,
|
|
633
|
+
timestamp: Date.now(),
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Auto-cleanup after 10 minutes
|
|
637
|
+
setTimeout(() => {
|
|
638
|
+
this.gmailOAuthStates.delete(state);
|
|
639
|
+
}, 10 * 60 * 1000);
|
|
640
|
+
|
|
641
|
+
const oauth2Client = new google.auth.OAuth2(
|
|
642
|
+
clientId,
|
|
643
|
+
clientSecret,
|
|
644
|
+
callbackUrl,
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
const scopes = [
|
|
648
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
649
|
+
'https://www.googleapis.com/auth/gmail.readonly',
|
|
650
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
651
|
+
];
|
|
652
|
+
|
|
653
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
654
|
+
access_type: 'offline',
|
|
655
|
+
scope: scopes,
|
|
656
|
+
state: `gmail_config:${state}`, // Prefix to identify Gmail config requests
|
|
657
|
+
prompt: 'consent',
|
|
658
|
+
login_hint: email,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
return { authUrl, state };
|
|
662
|
+
} catch (error) {
|
|
663
|
+
this.logger.error('Error initializing Gmail OAuth:', error.message);
|
|
664
|
+
throw new Error('Failed to initialize Gmail OAuth');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async handleGmailOAuthCallback(
|
|
669
|
+
code: string,
|
|
670
|
+
state: string,
|
|
671
|
+
): Promise<GmailSSOResult> {
|
|
672
|
+
try {
|
|
673
|
+
const oauthState = this.gmailOAuthStates.get(state);
|
|
674
|
+
if (!oauthState) {
|
|
675
|
+
throw new Error('Invalid or expired OAuth state');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
this.gmailOAuthStates.delete(state);
|
|
679
|
+
|
|
680
|
+
if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
|
|
681
|
+
throw new Error('OAuth state expired');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Get system OAuth credentials
|
|
685
|
+
const clientId = this.configService.get<string>('CLIENT_ID');
|
|
686
|
+
const clientSecret = this.configService.get<string>('CLIENT_SECRET');
|
|
687
|
+
const callbackUrl = this.configService.get<string>('CALLBACK_URL') || 'http://localhost:5001/auth/google/callback';
|
|
688
|
+
|
|
689
|
+
const oauth2Client = new google.auth.OAuth2(
|
|
690
|
+
clientId,
|
|
691
|
+
clientSecret,
|
|
692
|
+
callbackUrl,
|
|
693
|
+
);
|
|
694
|
+
|
|
695
|
+
const { tokens } = await oauth2Client.getToken(code);
|
|
696
|
+
|
|
697
|
+
if (!tokens.access_token) {
|
|
698
|
+
throw new Error('Failed to obtain access token');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Get user email from Google
|
|
702
|
+
oauth2Client.setCredentials(tokens);
|
|
703
|
+
const oauth2 = google.oauth2({ version: 'v2', auth: oauth2Client });
|
|
704
|
+
const userInfo = await oauth2.userinfo.get();
|
|
705
|
+
const email = userInfo.data.email;
|
|
706
|
+
|
|
707
|
+
if (!email) {
|
|
708
|
+
throw new Error('Failed to get user email');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Verify email matches if hint was provided
|
|
712
|
+
if (oauthState.email && oauthState.email !== email) {
|
|
713
|
+
throw new Error('Email mismatch with OAuth hint');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const gmailConfig = {
|
|
717
|
+
clientId,
|
|
718
|
+
clientSecret,
|
|
719
|
+
email: email,
|
|
720
|
+
accessToken: tokens.access_token,
|
|
721
|
+
refreshToken: tokens.refresh_token,
|
|
722
|
+
scope: tokens.scope,
|
|
723
|
+
tokenType: tokens.token_type,
|
|
724
|
+
expiryDate: tokens.expiry_date,
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
// Create communication config
|
|
728
|
+
const communicationConfig = this.configRepository.create({
|
|
729
|
+
config_json: gmailConfig as any,
|
|
730
|
+
});
|
|
731
|
+
const savedConfig = await this.configRepository.save(communicationConfig);
|
|
732
|
+
|
|
733
|
+
// Create communication hub
|
|
734
|
+
const hub = this.hubRepository.create({
|
|
735
|
+
level_id: oauthState.levelId,
|
|
736
|
+
level_type: oauthState.levelType,
|
|
737
|
+
config_id: savedConfig.id,
|
|
738
|
+
communication_config_type: CommunicationConfigType.EMAIL,
|
|
739
|
+
service: 'API',
|
|
740
|
+
provider: 'gmail',
|
|
741
|
+
status: 1,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
const savedHub = await this.hubRepository.save(hub);
|
|
745
|
+
|
|
746
|
+
this.logger.log(
|
|
747
|
+
`Gmail OAuth configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
hubId: savedHub.id,
|
|
752
|
+
configId: savedConfig.id,
|
|
753
|
+
};
|
|
754
|
+
} catch (error) {
|
|
755
|
+
this.logger.error('Error handling Gmail OAuth callback:', error.message);
|
|
756
|
+
throw new Error(`Failed to complete Gmail OAuth: ${error.message}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async handleGmailTokensCallback(
|
|
761
|
+
email: string,
|
|
762
|
+
accessToken: string,
|
|
763
|
+
refreshToken: string,
|
|
764
|
+
state: string,
|
|
765
|
+
): Promise<GmailSSOResult> {
|
|
766
|
+
try {
|
|
767
|
+
const oauthState = this.gmailOAuthStates.get(state);
|
|
768
|
+
if (!oauthState) {
|
|
769
|
+
throw new Error('Invalid or expired OAuth state');
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.gmailOAuthStates.delete(state);
|
|
773
|
+
|
|
774
|
+
if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
|
|
775
|
+
throw new Error('OAuth state expired');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Verify email matches if hint was provided
|
|
779
|
+
if (oauthState.email && oauthState.email !== email) {
|
|
780
|
+
throw new Error('Email mismatch with OAuth hint');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Pure SSO configuration - no client credentials stored per user
|
|
784
|
+
const gmailConfig = {
|
|
785
|
+
email: email,
|
|
786
|
+
accessToken: accessToken,
|
|
787
|
+
refreshToken: refreshToken,
|
|
788
|
+
authMethod: 'GOOGLE_SSO',
|
|
789
|
+
scopes: [
|
|
790
|
+
'https://www.googleapis.com/auth/gmail.send',
|
|
791
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
792
|
+
],
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
// Create communication config
|
|
796
|
+
const communicationConfig = this.configRepository.create({
|
|
797
|
+
config_json: gmailConfig as any,
|
|
798
|
+
});
|
|
799
|
+
const savedConfig = await this.configRepository.save(communicationConfig);
|
|
800
|
+
|
|
801
|
+
// Create communication hub
|
|
802
|
+
const hub = this.hubRepository.create({
|
|
803
|
+
level_id: oauthState.levelId,
|
|
804
|
+
level_type: oauthState.levelType,
|
|
805
|
+
config_id: savedConfig.id,
|
|
806
|
+
communication_config_type: CommunicationConfigType.EMAIL,
|
|
807
|
+
service: 'API',
|
|
808
|
+
provider: 'gmail',
|
|
809
|
+
status: 1,
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
const savedHub = await this.hubRepository.save(hub);
|
|
813
|
+
|
|
814
|
+
this.logger.log(
|
|
815
|
+
`Gmail tokens configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
hubId: savedHub.id,
|
|
820
|
+
configId: savedConfig.id,
|
|
821
|
+
};
|
|
822
|
+
} catch (error) {
|
|
823
|
+
this.logger.error('Error handling Gmail tokens callback:', error.message);
|
|
824
|
+
throw new Error(`Failed to complete Gmail tokens callback: ${error.message}`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async testGmailConfig(hubId: number): Promise<{ success: boolean; error?: string }> {
|
|
829
|
+
try {
|
|
830
|
+
const hub = await this.hubRepository.findOne({
|
|
831
|
+
where: { id: hubId },
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
if (!hub) {
|
|
835
|
+
throw new Error('Communication hub not found');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const config = await this.configRepository.findOne({
|
|
839
|
+
where: { id: hub.config_id },
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
if (!config) {
|
|
843
|
+
throw new Error('Configuration not found');
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const isValid = await this.gmailApiStrategy.validateConnection(config.config_json);
|
|
847
|
+
|
|
848
|
+
if (!isValid) {
|
|
849
|
+
return { success: false, error: 'Gmail configuration is invalid or expired' };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return { success: true };
|
|
853
|
+
} catch (error) {
|
|
854
|
+
this.logger.error('Error testing Gmail config:', error.message);
|
|
855
|
+
return { success: false, error: error.message };
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private generateSecureState(): string {
|
|
860
|
+
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private requiresOAuthFlow(
|
|
864
|
+
configType: CommunicationConfigType,
|
|
865
|
+
service: string,
|
|
866
|
+
provider: string,
|
|
867
|
+
config: any
|
|
868
|
+
): boolean {
|
|
869
|
+
// Check if config indicates OAuth is needed (missing tokens or explicit OAuth request)
|
|
870
|
+
const key = `${configType.toLowerCase()}_${service.toLowerCase()}_${provider.toLowerCase()}`;
|
|
871
|
+
|
|
872
|
+
switch (key) {
|
|
873
|
+
case 'email_api_gmail':
|
|
874
|
+
// Requires OAuth if no access token provided or OAuth explicitly requested
|
|
875
|
+
return !config.accessToken || config.useOAuth === true;
|
|
876
|
+
|
|
877
|
+
case 'email_api_outlook':
|
|
878
|
+
// Requires OAuth if no access token provided or OAuth explicitly requested
|
|
879
|
+
return !config.accessToken || config.useOAuth === true;
|
|
880
|
+
|
|
881
|
+
default:
|
|
882
|
+
// Most other providers don't require OAuth
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
private async generateOAuthUrl(
|
|
888
|
+
levelId: number,
|
|
889
|
+
levelType: string,
|
|
890
|
+
configType: CommunicationConfigType,
|
|
891
|
+
service: string,
|
|
892
|
+
provider: string,
|
|
893
|
+
config: any,
|
|
894
|
+
priority?: number,
|
|
895
|
+
is_default?: boolean
|
|
896
|
+
): Promise<{ authUrl: string; state: string; message: string }> {
|
|
897
|
+
const key = `${configType.toLowerCase()}_${service.toLowerCase()}_${provider.toLowerCase()}`;
|
|
898
|
+
|
|
899
|
+
switch (key) {
|
|
900
|
+
case 'email_api_gmail':
|
|
901
|
+
const gmailResult = await this.initGmailOAuth(levelId, levelType, config.email);
|
|
902
|
+
return {
|
|
903
|
+
authUrl: gmailResult.authUrl,
|
|
904
|
+
state: gmailResult.state,
|
|
905
|
+
message: 'Please complete Gmail OAuth authorization. Configuration will be created automatically after authorization.'
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
case 'email_api_outlook':
|
|
909
|
+
const outlookResult = await this.initOutlookOAuth(levelId, levelType, config.email);
|
|
910
|
+
return {
|
|
911
|
+
authUrl: outlookResult.authUrl,
|
|
912
|
+
state: outlookResult.state,
|
|
913
|
+
message: 'Please complete Outlook OAuth authorization. Configuration will be created automatically after authorization.'
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
default:
|
|
917
|
+
throw new Error(`OAuth not supported for ${configType}/${service}/${provider}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
private async createDirectConfig(
|
|
922
|
+
levelId: number,
|
|
923
|
+
levelType: string,
|
|
924
|
+
configType: CommunicationConfigType,
|
|
925
|
+
service: string,
|
|
926
|
+
provider: string,
|
|
927
|
+
config: any,
|
|
928
|
+
priority?: number,
|
|
929
|
+
is_default?: boolean
|
|
930
|
+
): Promise<CommunicationHub> {
|
|
931
|
+
// If setting as default, remove default from other configurations of same type
|
|
932
|
+
if (is_default) {
|
|
933
|
+
await this.hubRepository.update(
|
|
934
|
+
{
|
|
935
|
+
level_id: levelId,
|
|
936
|
+
level_type: levelType,
|
|
937
|
+
communication_config_type: configType,
|
|
938
|
+
},
|
|
939
|
+
{ is_default: false }
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// First, create the config
|
|
944
|
+
const communicationConfig = this.configRepository.create({
|
|
945
|
+
config_json: config as any,
|
|
946
|
+
});
|
|
947
|
+
const savedConfig = await this.configRepository.save(communicationConfig);
|
|
948
|
+
|
|
949
|
+
// Then create the hub
|
|
950
|
+
const hub = this.hubRepository.create({
|
|
951
|
+
level_id: levelId,
|
|
952
|
+
level_type: levelType,
|
|
953
|
+
config_id: savedConfig.id,
|
|
954
|
+
communication_config_type: configType,
|
|
955
|
+
service: service as any,
|
|
956
|
+
provider: provider as any,
|
|
957
|
+
priority: priority || 1,
|
|
958
|
+
is_default: is_default || false,
|
|
959
|
+
status: 1,
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
const savedHub = await this.hubRepository.save(hub);
|
|
963
|
+
this.logger.log(`Communication config created: ${configType}/${service}/${provider} for ${levelType} ${levelId}`);
|
|
964
|
+
|
|
965
|
+
return Array.isArray(savedHub) ? savedHub[0] : savedHub;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async initOutlookOAuth(
|
|
969
|
+
levelId: number,
|
|
970
|
+
levelType: string,
|
|
971
|
+
email?: string,
|
|
972
|
+
): Promise<{ authUrl: string; state: string }> {
|
|
973
|
+
try {
|
|
974
|
+
// Get system OAuth credentials from config
|
|
975
|
+
const clientId = this.configService.get<string>('OUTLOOK_CLIENT_ID');
|
|
976
|
+
const tenantId = this.configService.get<string>('OUTLOOK_TENANT_ID');
|
|
977
|
+
|
|
978
|
+
if (!clientId || !tenantId) {
|
|
979
|
+
throw new Error('Outlook OAuth credentials not configured');
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const state = this.generateSecureState();
|
|
983
|
+
const callbackUrl = this.configService.get<string>('OUTLOOK_CALLBACK_URL') || 'http://localhost:5001/auth/outlook/callback';
|
|
984
|
+
|
|
985
|
+
this.gmailOAuthStates.set(state, {
|
|
986
|
+
levelId,
|
|
987
|
+
levelType,
|
|
988
|
+
email,
|
|
989
|
+
timestamp: Date.now(),
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// Auto-cleanup after 10 minutes
|
|
993
|
+
setTimeout(() => {
|
|
994
|
+
this.gmailOAuthStates.delete(state);
|
|
995
|
+
}, 10 * 60 * 1000);
|
|
996
|
+
|
|
997
|
+
const scopes = [
|
|
998
|
+
'https://graph.microsoft.com/Mail.Send',
|
|
999
|
+
'https://graph.microsoft.com/User.Read',
|
|
1000
|
+
];
|
|
1001
|
+
|
|
1002
|
+
const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?` +
|
|
1003
|
+
`client_id=${clientId}&` +
|
|
1004
|
+
`response_type=code&` +
|
|
1005
|
+
`redirect_uri=${encodeURIComponent(callbackUrl)}&` +
|
|
1006
|
+
`scope=${encodeURIComponent(scopes.join(' '))}&` +
|
|
1007
|
+
`state=outlook_config:${state}&` +
|
|
1008
|
+
`response_mode=query&` +
|
|
1009
|
+
`prompt=consent` +
|
|
1010
|
+
(email ? `&login_hint=${encodeURIComponent(email)}` : '');
|
|
1011
|
+
|
|
1012
|
+
return { authUrl, state };
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
this.logger.error('Error initializing Outlook OAuth:', error.message);
|
|
1015
|
+
throw new Error('Failed to initialize Outlook OAuth');
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
async handleOutlookOAuthCallback(
|
|
1020
|
+
code: string,
|
|
1021
|
+
state: string,
|
|
1022
|
+
): Promise<GmailSSOResult> {
|
|
1023
|
+
try {
|
|
1024
|
+
const oauthState = this.gmailOAuthStates.get(state);
|
|
1025
|
+
if (!oauthState) {
|
|
1026
|
+
throw new Error('Invalid or expired OAuth state');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
this.gmailOAuthStates.delete(state);
|
|
1030
|
+
|
|
1031
|
+
if (Date.now() - oauthState.timestamp > 10 * 60 * 1000) {
|
|
1032
|
+
throw new Error('OAuth state expired');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const clientId = this.configService.get<string>('OUTLOOK_CLIENT_ID');
|
|
1036
|
+
const clientSecret = this.configService.get<string>('OUTLOOK_CLIENT_SECRET');
|
|
1037
|
+
const tenantId = this.configService.get<string>('OUTLOOK_TENANT_ID');
|
|
1038
|
+
const callbackUrl = this.configService.get<string>('OUTLOOK_CALLBACK_URL') || 'http://localhost:5001/auth/outlook/callback';
|
|
1039
|
+
|
|
1040
|
+
// Exchange code for tokens
|
|
1041
|
+
const tokenResponse = await fetch(`https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`, {
|
|
1042
|
+
method: 'POST',
|
|
1043
|
+
headers: {
|
|
1044
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
1045
|
+
},
|
|
1046
|
+
body: new URLSearchParams({
|
|
1047
|
+
client_id: clientId!,
|
|
1048
|
+
client_secret: clientSecret!,
|
|
1049
|
+
code: code,
|
|
1050
|
+
redirect_uri: callbackUrl,
|
|
1051
|
+
grant_type: 'authorization_code',
|
|
1052
|
+
}),
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
const tokens = await tokenResponse.json();
|
|
1056
|
+
|
|
1057
|
+
if (!tokens.access_token) {
|
|
1058
|
+
throw new Error('Failed to obtain access token');
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Get user info from Microsoft Graph
|
|
1062
|
+
const userResponse = await fetch('https://graph.microsoft.com/v1.0/me', {
|
|
1063
|
+
headers: {
|
|
1064
|
+
'Authorization': `Bearer ${tokens.access_token}`,
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
const userInfo = await userResponse.json();
|
|
1069
|
+
const email = userInfo.mail || userInfo.userPrincipalName;
|
|
1070
|
+
|
|
1071
|
+
if (!email) {
|
|
1072
|
+
throw new Error('Failed to get user email');
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const outlookConfig = {
|
|
1076
|
+
clientId,
|
|
1077
|
+
tenantId,
|
|
1078
|
+
email: email,
|
|
1079
|
+
accessToken: tokens.access_token,
|
|
1080
|
+
refreshToken: tokens.refresh_token,
|
|
1081
|
+
scope: tokens.scope,
|
|
1082
|
+
tokenType: tokens.token_type,
|
|
1083
|
+
expiresIn: tokens.expires_in,
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
// Create communication config
|
|
1087
|
+
const communicationConfig = this.configRepository.create({
|
|
1088
|
+
config_json: outlookConfig as any,
|
|
1089
|
+
});
|
|
1090
|
+
const savedConfig = await this.configRepository.save(communicationConfig);
|
|
1091
|
+
|
|
1092
|
+
// Create communication hub
|
|
1093
|
+
const hub = this.hubRepository.create({
|
|
1094
|
+
level_id: oauthState.levelId,
|
|
1095
|
+
level_type: oauthState.levelType,
|
|
1096
|
+
config_id: savedConfig.id,
|
|
1097
|
+
communication_config_type: CommunicationConfigType.EMAIL,
|
|
1098
|
+
service: 'API',
|
|
1099
|
+
provider: 'outlook',
|
|
1100
|
+
status: 1,
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
const savedHub = await this.hubRepository.save(hub);
|
|
1104
|
+
|
|
1105
|
+
this.logger.log(
|
|
1106
|
+
`Outlook OAuth configuration created successfully for ${oauthState.levelType} ${oauthState.levelId} and email ${email}`,
|
|
1107
|
+
);
|
|
1108
|
+
|
|
1109
|
+
return {
|
|
1110
|
+
hubId: savedHub.id,
|
|
1111
|
+
configId: savedConfig.id,
|
|
1112
|
+
};
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
this.logger.error('Error handling Outlook OAuth callback:', error.message);
|
|
1115
|
+
throw new Error(`Failed to complete Outlook OAuth: ${error.message}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|