Skip to content

Commit 9699eed

Browse files
fix(api): add support for WhatsApp Business tokens exceeding 255 characters
- Add string truncation validation to prevent database column size errors - Implement full token cache system for WhatsApp Business instances - Auto-save full token to cache on authentication requests - Update instance in memory with full token when detected - Add token full length support in monitor service - Adjust token comparison in auth guard and websocket controller - Fix duplicate token check for full length tokens This fixes the issue where WhatsApp Business API tokens (>255 chars) were truncated in the database, causing authentication failures and API errors. The solution uses a hybrid cache system to store full tokens while keeping the database compatible with the 255 char limit.
1 parent cd800f2 commit 9699eed

File tree

7 files changed

+206
-41
lines changed

7 files changed

+206
-41
lines changed

src/api/controllers/instance.controller.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,26 @@ export class InstanceController {
7272
status: instanceData.status,
7373
});
7474

75-
instance.setInstance({
76-
instanceName: instanceData.instanceName,
77-
instanceId,
78-
integration: instanceData.integration,
79-
token: hash,
80-
number: instanceData.number,
81-
businessId: instanceData.businessId,
82-
});
75+
// Para WhatsApp Business, setInstance é async e precisa ser aguardado
76+
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
77+
await (instance as any).setInstance({
78+
instanceName: instanceData.instanceName,
79+
instanceId,
80+
integration: instanceData.integration,
81+
token: instanceData.token || hash, // Usa o token original completo
82+
number: instanceData.number,
83+
businessId: instanceData.businessId,
84+
});
85+
} else {
86+
instance.setInstance({
87+
instanceName: instanceData.instanceName,
88+
instanceId,
89+
integration: instanceData.integration,
90+
token: hash,
91+
number: instanceData.number,
92+
businessId: instanceData.businessId,
93+
});
94+
}
8395

8496
this.waMonitor.waInstances[instance.instanceName] = instance;
8597
this.waMonitor.delInstanceTime(instance.instanceName);

src/api/guards/auth.guard.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { InstanceDto } from '@api/dto/instance.dto';
2-
import { prismaRepository } from '@api/server.module';
2+
import { cache, prismaRepository, waMonitor } from '@api/server.module';
3+
import { Integration } from '@api/types/wa.types';
34
import { Auth, configService, Database } from '@config/env.config';
45
import { Logger } from '@config/logger.config';
56
import { ForbiddenException, UnauthorizedException } from '@exceptions';
@@ -30,15 +31,69 @@ async function apikey(req: Request, _: Response, next: NextFunction) {
3031
const instance = await prismaRepository.instance.findUnique({
3132
where: { name: param.instanceName },
3233
});
33-
if (instance.token === key) {
34+
const keyToCompare = key.length > 255 ? key.substring(0, 255) : key;
35+
if (instance.token === keyToCompare) {
36+
// Se o token fornecido é maior que 255 e a instância é WhatsApp Business, salva no cache
37+
if (key.length > 255 && instance.integration === Integration.WHATSAPP_BUSINESS) {
38+
const cacheKey = `instance:${param.instanceName}:fullToken`;
39+
await cache.set(cacheKey, key, 0);
40+
logger.log(`Stored full token in cache for ${param.instanceName} from request`);
41+
42+
// Atualiza a instância em memória se existir
43+
if (waMonitor.waInstances[param.instanceName]) {
44+
const waInstance = waMonitor.waInstances[param.instanceName];
45+
if (waInstance && typeof (waInstance as any).setInstance === 'function') {
46+
try {
47+
await (waInstance as any).setInstance({
48+
instanceName: param.instanceName,
49+
instanceId: instance.id,
50+
integration: instance.integration,
51+
token: key,
52+
number: instance.number,
53+
businessId: instance.businessId,
54+
});
55+
logger.log(`Updated full token in memory for ${param.instanceName}`);
56+
} catch (error) {
57+
logger.error(`Error updating token in memory: ${error}`);
58+
}
59+
}
60+
}
61+
}
3462
return next();
3563
}
3664
} else {
3765
if (req.originalUrl.includes('/instance/fetchInstances') && db.SAVE_DATA.INSTANCE) {
66+
const keyToCompare = key.length > 255 ? key.substring(0, 255) : key;
3867
const instanceByKey = await prismaRepository.instance.findFirst({
39-
where: { token: key },
68+
where: { token: keyToCompare },
4069
});
4170
if (instanceByKey) {
71+
// Se o token fornecido é maior que 255 e a instância é WhatsApp Business, salva no cache
72+
if (key.length > 255 && instanceByKey.integration === Integration.WHATSAPP_BUSINESS) {
73+
const cacheKey = `instance:${instanceByKey.name}:fullToken`;
74+
await cache.set(cacheKey, key, 0);
75+
logger.log(`Stored full token in cache for ${instanceByKey.name} from request`);
76+
77+
// Atualiza a instância em memória se existir
78+
if (waMonitor.waInstances[instanceByKey.name]) {
79+
const waInstance = waMonitor.waInstances[instanceByKey.name];
80+
if (waInstance && typeof (waInstance as any).setInstance === 'function') {
81+
try {
82+
await (waInstance as any).setInstance({
83+
instanceName: instanceByKey.name,
84+
instanceId: instanceByKey.id,
85+
integration: instanceByKey.integration,
86+
token: key,
87+
number: instanceByKey.number,
88+
businessId: instanceByKey.businessId,
89+
});
90+
logger.log(`Updated full token in memory for ${instanceByKey.name}`);
91+
} catch (error) {
92+
logger.error(`Error updating token in memory: ${error}`);
93+
}
94+
}
95+
}
96+
}
4297
return next();
4398
}
4499
}

src/api/integrations/channel/meta/whatsapp.business.service.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,41 @@ export class BusinessStartupService extends ChannelStartupService {
4545
super(configService, eventEmitter, prismaRepository, chatwootCache);
4646
}
4747

48+
private fullToken: string | null = null;
49+
4850
public stateConnection: wa.StateConnection = { state: 'open' };
4951

5052
public phoneNumber: string;
5153
public mobile: boolean;
5254

55+
// Override token getter para retornar token completo se disponível
56+
public get token(): string {
57+
return this.fullToken || this.instance.token || '';
58+
}
59+
60+
// Override setInstance para armazenar/carregar token completo
61+
public async setInstance(instance: any) {
62+
super.setInstance(instance);
63+
64+
// Se o token fornecido é maior que 255, é o token completo - armazena imediatamente
65+
if (instance.token && instance.token.length > 255) {
66+
this.fullToken = instance.token;
67+
const cacheKey = `instance:${instance.instanceName}:fullToken`;
68+
await this.cache.set(cacheKey, instance.token, 0);
69+
this.logger.log(`Stored full token in cache for ${instance.instanceName}`);
70+
} else {
71+
// Tenta carregar token completo do cache
72+
const cacheKey = `instance:${instance.instanceName}:fullToken`;
73+
const fullToken = await this.cache.get(cacheKey);
74+
if (fullToken) {
75+
this.fullToken = fullToken;
76+
this.logger.log(`Loaded full token from cache for ${instance.instanceName}`);
77+
} else {
78+
this.logger.warn(`Full token not found in cache for ${instance.instanceName}, using truncated token`);
79+
}
80+
}
81+
}
82+
5383
public get connectionStatus() {
5484
return this.stateConnection;
5585
}

src/api/integrations/event/websocket/websocket.controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ export class WebsocketController extends EventController implements EventControl
5252
return callback('apiKey is required', false);
5353
}
5454

55-
const instance = await this.prismaRepository.instance.findFirst({ where: { token: apiKey } });
55+
const keyToCompare = apiKey.length > 255 ? apiKey.substring(0, 255) : apiKey;
56+
const instance = await this.prismaRepository.instance.findFirst({ where: { token: keyToCompare } });
5657

5758
if (!instance) {
5859
const globalToken = configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;

src/api/services/auth.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ export class AuthService {
99
return true;
1010
}
1111

12+
// Compara apenas os primeiros 255 caracteres para verificar duplicatas
13+
const tokenToCompare = token.length > 255 ? token.substring(0, 255) : token;
1214
const instances = await this.prismaRepository.instance.findMany({
13-
where: { token },
15+
where: { token: tokenToCompare },
1416
});
1517

1618
if (instances.length > 0) {

src/api/services/channel.service.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,41 +157,49 @@ export class ChannelStartupService {
157157
}
158158

159159
public async setSettings(data: SettingsDto) {
160+
const truncate = (str: string | null | undefined, maxLength: number): string | null => {
161+
if (!str) return null;
162+
return str.length > maxLength ? str.substring(0, maxLength) : str;
163+
};
164+
165+
const msgCall = truncate(data.msgCall, 100);
166+
const wavoipToken = truncate(data.wavoipToken, 100);
167+
160168
await this.prismaRepository.setting.upsert({
161169
where: {
162170
instanceId: this.instanceId,
163171
},
164172
update: {
165173
rejectCall: data.rejectCall,
166-
msgCall: data.msgCall,
174+
msgCall: msgCall,
167175
groupsIgnore: data.groupsIgnore,
168176
alwaysOnline: data.alwaysOnline,
169177
readMessages: data.readMessages,
170178
readStatus: data.readStatus,
171179
syncFullHistory: data.syncFullHistory,
172-
wavoipToken: data.wavoipToken,
180+
wavoipToken: wavoipToken,
173181
},
174182
create: {
175183
rejectCall: data.rejectCall,
176-
msgCall: data.msgCall,
184+
msgCall: msgCall,
177185
groupsIgnore: data.groupsIgnore,
178186
alwaysOnline: data.alwaysOnline,
179187
readMessages: data.readMessages,
180188
readStatus: data.readStatus,
181189
syncFullHistory: data.syncFullHistory,
182-
wavoipToken: data.wavoipToken,
190+
wavoipToken: wavoipToken,
183191
instanceId: this.instanceId,
184192
},
185193
});
186194

187195
this.localSettings.rejectCall = data?.rejectCall;
188-
this.localSettings.msgCall = data?.msgCall;
196+
this.localSettings.msgCall = msgCall;
189197
this.localSettings.groupsIgnore = data?.groupsIgnore;
190198
this.localSettings.alwaysOnline = data?.alwaysOnline;
191199
this.localSettings.readMessages = data?.readMessages;
192200
this.localSettings.readStatus = data?.readStatus;
193201
this.localSettings.syncFullHistory = data?.syncFullHistory;
194-
this.localSettings.wavoipToken = data?.wavoipToken;
202+
this.localSettings.wavoipToken = wavoipToken;
195203

196204
if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
197205
this.client.ws.close();

src/api/services/monitor.service.ts

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -239,26 +239,37 @@ export class WAMonitoringService {
239239
}
240240

241241
public async saveInstance(data: any) {
242+
const truncate = (str: string | null | undefined, maxLength: number): string | null => {
243+
if (!str) return null;
244+
return str.length > maxLength ? str.substring(0, maxLength) : str;
245+
};
246+
242247
try {
243-
const clientName = await this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
248+
const instanceName = truncate(data.instanceName, 255);
249+
if (!instanceName || instanceName.trim().length === 0) {
250+
throw new Error('instanceName is required and cannot be empty');
251+
}
252+
253+
const clientName = this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
244254
await this.prismaRepository.instance.create({
245255
data: {
246256
id: data.instanceId,
247-
name: data.instanceName,
248-
ownerJid: data.ownerJid,
249-
profileName: data.profileName,
250-
profilePicUrl: data.profilePicUrl,
257+
name: instanceName,
258+
ownerJid: truncate(data.ownerJid, 100),
259+
profileName: truncate(data.profileName, 100),
260+
profilePicUrl: truncate(data.profilePicUrl, 500),
251261
connectionStatus:
252262
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : (data.status ?? 'open'),
253-
number: data.number,
254-
integration: data.integration || Integration.WHATSAPP_BAILEYS,
255-
token: data.hash,
256-
clientName: clientName,
257-
businessId: data.businessId,
263+
number: truncate(data.number, 100),
264+
integration: truncate(data.integration || Integration.WHATSAPP_BAILEYS, 100),
265+
token: truncate(data.hash, 255),
266+
clientName: truncate(clientName, 100),
267+
businessId: truncate(data.businessId, 100),
258268
},
259269
});
260270
} catch (error) {
261271
this.logger.error(error);
272+
throw error;
262273
}
263274
}
264275

@@ -283,15 +294,31 @@ export class WAMonitoringService {
283294

284295
if (!instance) return;
285296

286-
instance.setInstance({
287-
instanceId: instanceData.instanceId,
288-
instanceName: instanceData.instanceName,
289-
integration: instanceData.integration,
290-
token: instanceData.token,
291-
number: instanceData.number,
292-
businessId: instanceData.businessId,
293-
ownerJid: instanceData.ownerJid,
294-
});
297+
// Para WhatsApp Business, setInstance é async e precisa ser aguardado
298+
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
299+
const setInstanceResult = (instance as any).setInstance({
300+
instanceId: instanceData.instanceId,
301+
instanceName: instanceData.instanceName,
302+
integration: instanceData.integration,
303+
token: instanceData.token,
304+
number: instanceData.number,
305+
businessId: instanceData.businessId,
306+
ownerJid: instanceData.ownerJid,
307+
});
308+
if (setInstanceResult instanceof Promise) {
309+
await setInstanceResult;
310+
}
311+
} else {
312+
instance.setInstance({
313+
instanceId: instanceData.instanceId,
314+
instanceName: instanceData.instanceName,
315+
integration: instanceData.integration,
316+
token: instanceData.token,
317+
number: instanceData.number,
318+
businessId: instanceData.businessId,
319+
ownerJid: instanceData.ownerJid,
320+
});
321+
}
295322

296323
if (instanceData.connectionStatus === 'open' || instanceData.connectionStatus === 'connecting') {
297324
this.logger.info(
@@ -321,11 +348,21 @@ export class WAMonitoringService {
321348
return;
322349
}
323350

351+
// Para WhatsApp Business, tenta carregar token completo do cache
352+
let token = instanceData.token;
353+
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
354+
const cacheKey = `instance:${instanceData.name}:fullToken`;
355+
const fullToken = await this.cache.get(cacheKey);
356+
if (fullToken) {
357+
token = fullToken;
358+
}
359+
}
360+
324361
const instance = {
325362
instanceId: k.split(':')[1],
326363
instanceName: k.split(':')[2],
327364
integration: instanceData.integration,
328-
token: instanceData.token,
365+
token: token,
329366
number: instanceData.number,
330367
businessId: instanceData.businessId,
331368
connectionStatus: instanceData.connectionStatus as any, // Pass connection status
@@ -350,11 +387,21 @@ export class WAMonitoringService {
350387

351388
await Promise.all(
352389
instances.map(async (instance) => {
390+
// Para WhatsApp Business, tenta carregar token completo do cache
391+
let token = instance.token;
392+
if (instance.integration === Integration.WHATSAPP_BUSINESS) {
393+
const cacheKey = `instance:${instance.name}:fullToken`;
394+
const fullToken = await this.cache.get(cacheKey);
395+
if (fullToken) {
396+
token = fullToken;
397+
}
398+
}
399+
353400
this.setInstance({
354401
instanceId: instance.id,
355402
instanceName: instance.name,
356403
integration: instance.integration,
357-
token: instance.token,
404+
token: token,
358405
number: instance.number,
359406
businessId: instance.businessId,
360407
ownerJid: instance.ownerJid,
@@ -377,11 +424,21 @@ export class WAMonitoringService {
377424
where: { id: instanceId },
378425
});
379426

427+
// Para WhatsApp Business, tenta carregar token completo do cache
428+
let token = instance.token;
429+
if (instance.integration === Integration.WHATSAPP_BUSINESS) {
430+
const cacheKey = `instance:${instance.name}:fullToken`;
431+
const fullToken = await this.cache.get(cacheKey);
432+
if (fullToken) {
433+
token = fullToken;
434+
}
435+
}
436+
380437
this.setInstance({
381438
instanceId: instance.id,
382439
instanceName: instance.name,
383440
integration: instance.integration,
384-
token: instance.token,
441+
token: token,
385442
businessId: instance.businessId,
386443
connectionStatus: instance.connectionStatus as any, // Pass connection status
387444
});

0 commit comments

Comments
 (0)