Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions src/api/controllers/instance.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,26 @@ export class InstanceController {
status: instanceData.status,
});

instance.setInstance({
instanceName: instanceData.instanceName,
instanceId,
integration: instanceData.integration,
token: hash,
number: instanceData.number,
businessId: instanceData.businessId,
});
// Para WhatsApp Business, setInstance é async e precisa ser aguardado
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
await (instance as any).setInstance({
instanceName: instanceData.instanceName,
instanceId,
integration: instanceData.integration,
token: instanceData.token || hash, // Usa o token original completo
number: instanceData.number,
businessId: instanceData.businessId,
});
} else {
instance.setInstance({
instanceName: instanceData.instanceName,
instanceId,
integration: instanceData.integration,
token: hash,
number: instanceData.number,
businessId: instanceData.businessId,
});
}

this.waMonitor.waInstances[instance.instanceName] = instance;
this.waMonitor.delInstanceTime(instance.instanceName);
Expand Down
61 changes: 58 additions & 3 deletions src/api/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { InstanceDto } from '@api/dto/instance.dto';
import { prismaRepository } from '@api/server.module';
import { cache, prismaRepository, waMonitor } from '@api/server.module';
import { Integration } from '@api/types/wa.types';
import { Auth, configService, Database } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { ForbiddenException, UnauthorizedException } from '@exceptions';
Expand Down Expand Up @@ -30,15 +31,69 @@ async function apikey(req: Request, _: Response, next: NextFunction) {
const instance = await prismaRepository.instance.findUnique({
where: { name: param.instanceName },
});
if (instance.token === key) {
const keyToCompare = key.length > 255 ? key.substring(0, 255) : key;
if (instance.token === keyToCompare) {
// Se o token fornecido é maior que 255 e a instância é WhatsApp Business, salva no cache
if (key.length > 255 && instance.integration === Integration.WHATSAPP_BUSINESS) {
const cacheKey = `instance:${param.instanceName}:fullToken`;
await cache.set(cacheKey, key, 0);
logger.log(`Stored full token in cache for ${param.instanceName} from request`);

// Atualiza a instância em memória se existir
if (waMonitor.waInstances[param.instanceName]) {
const waInstance = waMonitor.waInstances[param.instanceName];
if (waInstance && typeof (waInstance as any).setInstance === 'function') {
try {
await (waInstance as any).setInstance({
instanceName: param.instanceName,
instanceId: instance.id,
integration: instance.integration,
token: key,
number: instance.number,
businessId: instance.businessId,
});
logger.log(`Updated full token in memory for ${param.instanceName}`);
} catch (error) {
logger.error(`Error updating token in memory: ${error}`);
}
}
}
}
return next();
}
} else {
if (req.originalUrl.includes('/instance/fetchInstances') && db.SAVE_DATA.INSTANCE) {
const keyToCompare = key.length > 255 ? key.substring(0, 255) : key;
const instanceByKey = await prismaRepository.instance.findFirst({
where: { token: key },
where: { token: keyToCompare },
});
if (instanceByKey) {
// Se o token fornecido é maior que 255 e a instância é WhatsApp Business, salva no cache
if (key.length > 255 && instanceByKey.integration === Integration.WHATSAPP_BUSINESS) {
const cacheKey = `instance:${instanceByKey.name}:fullToken`;
await cache.set(cacheKey, key, 0);
logger.log(`Stored full token in cache for ${instanceByKey.name} from request`);

// Atualiza a instância em memória se existir
if (waMonitor.waInstances[instanceByKey.name]) {
const waInstance = waMonitor.waInstances[instanceByKey.name];
if (waInstance && typeof (waInstance as any).setInstance === 'function') {
try {
await (waInstance as any).setInstance({
instanceName: instanceByKey.name,
instanceId: instanceByKey.id,
integration: instanceByKey.integration,
token: key,
number: instanceByKey.number,
businessId: instanceByKey.businessId,
});
logger.log(`Updated full token in memory for ${instanceByKey.name}`);
} catch (error) {
logger.error(`Error updating token in memory: ${error}`);
}
}
}
}
return next();
}
}
Expand Down
30 changes: 30 additions & 0 deletions src/api/integrations/channel/meta/whatsapp.business.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,41 @@ export class BusinessStartupService extends ChannelStartupService {
super(configService, eventEmitter, prismaRepository, chatwootCache);
}

private fullToken: string | null = null;

public stateConnection: wa.StateConnection = { state: 'open' };

public phoneNumber: string;
public mobile: boolean;

// Override token getter para retornar token completo se disponível
public get token(): string {
return this.fullToken || this.instance.token || '';
}

// Override setInstance para armazenar/carregar token completo
public async setInstance(instance: any) {
super.setInstance(instance);

// Se o token fornecido é maior que 255, é o token completo - armazena imediatamente
if (instance.token && instance.token.length > 255) {
this.fullToken = instance.token;
const cacheKey = `instance:${instance.instanceName}:fullToken`;
await this.cache.set(cacheKey, instance.token, 0);
this.logger.log(`Stored full token in cache for ${instance.instanceName}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider making the async contract of setInstance explicit and consistent with the base class.

Here setInstance is now async and some callers are awaiting it, but the base ChannelStartupService.setInstance is still effectively synchronous. This split contract can be confusing and lead to timing issues around when fullToken is available.

I’d suggest either:

  1. Making the async contract explicit on both base and override (e.g. Promise<void>) so it’s clear this is part of the instance lifecycle, or
  2. Clearly documenting that only this subclass performs async cache IO and ensuring all relevant call sites consistently await it when they depend on cache state (e.g. token getter, connection startup).

Suggested implementation:

  // Override setInstance para armazenar/carregar token completo
  public async setInstance(instance: any): Promise<void> {
    // Garante que qualquer lógica assíncrona definida na classe base seja respeitada
    await super.setInstance(instance);

    // Se o token fornecido é maior que 255, é o token completo - armazena imediatamente

Para tornar o contrato assíncrono explícito e consistente com a classe base, também será necessário:

  1. Atualizar a assinatura de setInstance na classe base (provavelmente ChannelStartupService ou similar), por exemplo em src/api/integrations/channel/...:

    • Mudar de public setInstance(instance: any) { ... } para public async setInstance(instance: any): Promise<void> { ... }.
    • Se o corpo atual é totalmente síncrono, ele pode permanecer igual; apenas a assinatura passa a ser async/Promise<void>.
  2. Verificar todos os locais onde setInstance(...) é chamado:

    • Para todos os call sites que dependem de estado inicializado por setInstance (como acesso a token ou início de conexão), garantir que a chamada seja await this.setInstance(...) e que o contexto seja assíncrono.
    • Se existirem pontos onde setInstance é chamado mas o resultado não é relevante (fire-and-forget), decidir explicitamente se:
      • deve ser aguardado com await, ou
      • deve ser chamado sem await, mas com comentários deixando claro que a inicialização é assíncrona e não é necessária imediatamente.
  3. Atualizar qualquer interface/abstração que declare setInstance (por exemplo, uma interface de serviço de canal) para usar a assinatura setInstance(instance: any): Promise<void>; para manter o contrato de tipo consistente.

} else {
// Tenta carregar token completo do cache
const cacheKey = `instance:${instance.instanceName}:fullToken`;
const fullToken = await this.cache.get(cacheKey);
if (fullToken) {
this.fullToken = fullToken;
this.logger.log(`Loaded full token from cache for ${instance.instanceName}`);
} else {
this.logger.warn(`Full token not found in cache for ${instance.instanceName}, using truncated token`);
}
}
}

public get connectionStatus() {
return this.stateConnection;
}
Expand Down
3 changes: 2 additions & 1 deletion src/api/integrations/event/websocket/websocket.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ export class WebsocketController extends EventController implements EventControl
return callback('apiKey is required', false);
}

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

if (!instance) {
const globalToken = configService.get<Auth>('AUTHENTICATION').API_KEY.KEY;
Expand Down
4 changes: 3 additions & 1 deletion src/api/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export class AuthService {
return true;
}

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

if (instances.length > 0) {
Expand Down
20 changes: 14 additions & 6 deletions src/api/services/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,41 +157,49 @@ export class ChannelStartupService {
}

public async setSettings(data: SettingsDto) {
const truncate = (str: string | null | undefined, maxLength: number): string | null => {
if (!str) return null;
return str.length > maxLength ? str.substring(0, maxLength) : str;
};

const msgCall = truncate(data.msgCall, 100);
const wavoipToken = truncate(data.wavoipToken, 100);

await this.prismaRepository.setting.upsert({
where: {
instanceId: this.instanceId,
},
update: {
rejectCall: data.rejectCall,
msgCall: data.msgCall,
msgCall: msgCall,
groupsIgnore: data.groupsIgnore,
alwaysOnline: data.alwaysOnline,
readMessages: data.readMessages,
readStatus: data.readStatus,
syncFullHistory: data.syncFullHistory,
wavoipToken: data.wavoipToken,
wavoipToken: wavoipToken,
},
create: {
rejectCall: data.rejectCall,
msgCall: data.msgCall,
msgCall: msgCall,
groupsIgnore: data.groupsIgnore,
alwaysOnline: data.alwaysOnline,
readMessages: data.readMessages,
readStatus: data.readStatus,
syncFullHistory: data.syncFullHistory,
wavoipToken: data.wavoipToken,
wavoipToken: wavoipToken,
instanceId: this.instanceId,
},
});

this.localSettings.rejectCall = data?.rejectCall;
this.localSettings.msgCall = data?.msgCall;
this.localSettings.msgCall = msgCall;
this.localSettings.groupsIgnore = data?.groupsIgnore;
this.localSettings.alwaysOnline = data?.alwaysOnline;
this.localSettings.readMessages = data?.readMessages;
this.localSettings.readStatus = data?.readStatus;
this.localSettings.syncFullHistory = data?.syncFullHistory;
this.localSettings.wavoipToken = data?.wavoipToken;
this.localSettings.wavoipToken = wavoipToken;

if (this.localSettings.wavoipToken && this.localSettings.wavoipToken.length > 0) {
this.client.ws.close();
Expand Down
101 changes: 79 additions & 22 deletions src/api/services/monitor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,26 +239,37 @@ export class WAMonitoringService {
}

public async saveInstance(data: any) {
const truncate = (str: string | null | undefined, maxLength: number): string | null => {
if (!str) return null;
return str.length > maxLength ? str.substring(0, maxLength) : str;
};

try {
const clientName = await this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
const instanceName = truncate(data.instanceName, 255);
if (!instanceName || instanceName.trim().length === 0) {
throw new Error('instanceName is required and cannot be empty');
}

const clientName = this.configService.get<Database>('DATABASE').CONNECTION.CLIENT_NAME;
await this.prismaRepository.instance.create({
data: {
id: data.instanceId,
name: data.instanceName,
ownerJid: data.ownerJid,
profileName: data.profileName,
profilePicUrl: data.profilePicUrl,
name: instanceName,
ownerJid: truncate(data.ownerJid, 100),
profileName: truncate(data.profileName, 100),
profilePicUrl: truncate(data.profilePicUrl, 500),
connectionStatus:
data.integration && data.integration === Integration.WHATSAPP_BAILEYS ? 'close' : (data.status ?? 'open'),
number: data.number,
integration: data.integration || Integration.WHATSAPP_BAILEYS,
token: data.hash,
clientName: clientName,
businessId: data.businessId,
number: truncate(data.number, 100),
integration: truncate(data.integration || Integration.WHATSAPP_BAILEYS, 100),
token: truncate(data.hash, 255),
clientName: truncate(clientName, 100),
businessId: truncate(data.businessId, 100),
},
});
} catch (error) {
this.logger.error(error);
throw error;
}
}

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

if (!instance) return;

instance.setInstance({
instanceId: instanceData.instanceId,
instanceName: instanceData.instanceName,
integration: instanceData.integration,
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
ownerJid: instanceData.ownerJid,
});
// Para WhatsApp Business, setInstance é async e precisa ser aguardado
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
const setInstanceResult = (instance as any).setInstance({
instanceId: instanceData.instanceId,
instanceName: instanceData.instanceName,
integration: instanceData.integration,
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
ownerJid: instanceData.ownerJid,
Comment on lines +321 to +330
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The Promise detection around setInstance can be simplified and made more robust.

You can drop the instanceof Promise check and always await the result of setInstance:

const maybePromise = (instance as any).setInstance({ ... });
await maybePromise;

This also handles non-native thenables and matches the controller’s await (instance as any).setInstance(...) pattern.

});
if (setInstanceResult instanceof Promise) {
await setInstanceResult;
}
} else {
instance.setInstance({
instanceId: instanceData.instanceId,
instanceName: instanceData.instanceName,
integration: instanceData.integration,
token: instanceData.token,
number: instanceData.number,
businessId: instanceData.businessId,
ownerJid: instanceData.ownerJid,
});
}

if (instanceData.connectionStatus === 'open' || instanceData.connectionStatus === 'connecting') {
this.logger.info(
Expand Down Expand Up @@ -321,11 +348,21 @@ export class WAMonitoringService {
return;
}

// Para WhatsApp Business, tenta carregar token completo do cache
let token = instanceData.token;
if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
const cacheKey = `instance:${instanceData.name}:fullToken`;
Comment on lines +375 to +378
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Cache key uses instanceData.name while the rest of this block relies on instanceName, which may cause key mismatches.

The cache key here uses instanceData.name, while nearby code and other usages build instance:${instanceName}:fullToken (with instanceName derived from k.split(':')[2] or instance.instanceName). If name and instanceName ever differ, tokens written under one key won’t be readable with the other. Please standardize on a single field (likely instanceName) for all cache key construction for this token.

const fullToken = await this.cache.get(cacheKey);
if (fullToken) {
token = fullToken;
}
}

const instance = {
instanceId: k.split(':')[1],
instanceName: k.split(':')[2],
integration: instanceData.integration,
token: instanceData.token,
token: token,
number: instanceData.number,
businessId: instanceData.businessId,
connectionStatus: instanceData.connectionStatus as any, // Pass connection status
Expand All @@ -350,11 +387,21 @@ export class WAMonitoringService {

await Promise.all(
instances.map(async (instance) => {
// Para WhatsApp Business, tenta carregar token completo do cache
let token = instance.token;
if (instance.integration === Integration.WHATSAPP_BUSINESS) {
const cacheKey = `instance:${instance.name}:fullToken`;
const fullToken = await this.cache.get(cacheKey);
if (fullToken) {
token = fullToken;
}
}

this.setInstance({
instanceId: instance.id,
instanceName: instance.name,
integration: instance.integration,
token: instance.token,
token: token,
number: instance.number,
businessId: instance.businessId,
ownerJid: instance.ownerJid,
Expand All @@ -377,11 +424,21 @@ export class WAMonitoringService {
where: { id: instanceId },
});

// Para WhatsApp Business, tenta carregar token completo do cache
let token = instance.token;
if (instance.integration === Integration.WHATSAPP_BUSINESS) {
const cacheKey = `instance:${instance.name}:fullToken`;
const fullToken = await this.cache.get(cacheKey);
if (fullToken) {
token = fullToken;
}
}

this.setInstance({
instanceId: instance.id,
instanceName: instance.name,
integration: instance.integration,
token: instance.token,
token: token,
businessId: instance.businessId,
connectionStatus: instance.connectionStatus as any, // Pass connection status
});
Expand Down
Loading