FEAT: Adicionado configuração da Meta API Fora do .env
This commit is contained in:
parent
5e9f328932
commit
197458e2a7
@ -155,3 +155,40 @@ export class UpdateWhatsappTemplateDto {
|
|||||||
@MaxLength(40)
|
@MaxLength(40)
|
||||||
category?: string;
|
category?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SaveWhatsappConfigDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
access_token?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
phone_number_id?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(20)
|
||||||
|
api_version?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
webhook_token?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidateWhatsappConfigDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
access_token: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
phone_number_id: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(20)
|
||||||
|
api_version?: string;
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DatabaseService } from '../../../infra/database/database.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WhatsappConfigRepository {
|
||||||
|
constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
async find() {
|
||||||
|
const result = await this.database.query(
|
||||||
|
`SELECT * FROM whatsapp_config LIMIT 1`,
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(fields: {
|
||||||
|
access_token?: string | null;
|
||||||
|
phone_number_id?: string | null;
|
||||||
|
api_version?: string | null;
|
||||||
|
webhook_token?: string | null;
|
||||||
|
updated_by?: number | null;
|
||||||
|
}) {
|
||||||
|
const result = await this.database.query(
|
||||||
|
`UPDATE whatsapp_config SET
|
||||||
|
access_token = CASE WHEN $1::TEXT IS NOT NULL THEN $1 ELSE access_token END,
|
||||||
|
phone_number_id = CASE WHEN $2::TEXT IS NOT NULL THEN $2 ELSE phone_number_id END,
|
||||||
|
api_version = CASE WHEN $3::TEXT IS NOT NULL THEN $3 ELSE api_version END,
|
||||||
|
webhook_token = CASE WHEN $4::TEXT IS NOT NULL THEN $4 ELSE webhook_token END,
|
||||||
|
updated_at = NOW(),
|
||||||
|
updated_by = $5
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
fields.access_token ?? null,
|
||||||
|
fields.phone_number_id ?? null,
|
||||||
|
fields.api_version ?? null,
|
||||||
|
fields.webhook_token ?? null,
|
||||||
|
fields.updated_by ?? null,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return result.rows[0] || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/modules/whatsapp/whatsapp-admin.controller.ts
Normal file
37
src/modules/whatsapp/whatsapp-admin.controller.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Body, Controller, Get, Post, Put, Request } from '@nestjs/common';
|
||||||
|
import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
|
import { WhatsappConfigService } from './whatsapp-config.service';
|
||||||
|
import { SaveWhatsappConfigDto, ValidateWhatsappConfigDto } from './dto/whatsapp.dto';
|
||||||
|
|
||||||
|
@ApiTags('WhatsApp Admin')
|
||||||
|
@Roles('Admin')
|
||||||
|
@Controller('admin/whatsapp-config')
|
||||||
|
export class WhatsappAdminController {
|
||||||
|
constructor(private readonly configService: WhatsappConfigService) {}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Retorna configuração atual do WhatsApp (sem expor o token completo)' })
|
||||||
|
@Get()
|
||||||
|
async getConfig() {
|
||||||
|
return this.configService.getPublicConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Salva configuração do WhatsApp no banco de dados' })
|
||||||
|
@ApiBody({ type: SaveWhatsappConfigDto })
|
||||||
|
@Put()
|
||||||
|
async saveConfig(@Body() body: SaveWhatsappConfigDto, @Request() req: any) {
|
||||||
|
const userId = req.user?.sub ? Number(req.user.sub) : undefined;
|
||||||
|
return this.configService.saveConfig(body, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Valida token e phone number ID junto à Meta API' })
|
||||||
|
@ApiBody({ type: ValidateWhatsappConfigDto })
|
||||||
|
@Post('validate')
|
||||||
|
async validateConfig(@Body() body: ValidateWhatsappConfigDto) {
|
||||||
|
return this.configService.validateCredentials(
|
||||||
|
body.access_token,
|
||||||
|
body.phone_number_id,
|
||||||
|
body.api_version,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/modules/whatsapp/whatsapp-config.service.ts
Normal file
85
src/modules/whatsapp/whatsapp-config.service.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { WhatsappConfigRepository } from './repositories/whatsapp-config.repository';
|
||||||
|
|
||||||
|
interface WhatsappConfig {
|
||||||
|
access_token: string | null;
|
||||||
|
phone_number_id: string | null;
|
||||||
|
api_version: string;
|
||||||
|
webhook_token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WhatsappConfigService {
|
||||||
|
private _cache: { config: WhatsappConfig; expiresAt: number } | null = null;
|
||||||
|
private readonly CACHE_TTL_MS = 60_000;
|
||||||
|
|
||||||
|
constructor(private readonly repo: WhatsappConfigRepository) {}
|
||||||
|
|
||||||
|
async getConfig(): Promise<WhatsappConfig> {
|
||||||
|
if (this._cache && Date.now() < this._cache.expiresAt) {
|
||||||
|
return this._cache.config;
|
||||||
|
}
|
||||||
|
const row = await this.repo.find();
|
||||||
|
const config: WhatsappConfig = {
|
||||||
|
access_token: row?.access_token || process.env.META_ACCESS_TOKEN || null,
|
||||||
|
phone_number_id: row?.phone_number_id || process.env.META_PHONE_NUMBER_ID || null,
|
||||||
|
api_version: row?.api_version || process.env.META_API_VERSION || 'v25.0',
|
||||||
|
webhook_token: row?.webhook_token || process.env.META_WEBHOOK_TOKEN || null,
|
||||||
|
};
|
||||||
|
this._cache = { config, expiresAt: Date.now() + this.CACHE_TTL_MS };
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPublicConfig() {
|
||||||
|
const row = await this.repo.find();
|
||||||
|
const effectiveToken = row?.access_token || process.env.META_ACCESS_TOKEN;
|
||||||
|
const effectivePhoneId = row?.phone_number_id || process.env.META_PHONE_NUMBER_ID;
|
||||||
|
return {
|
||||||
|
phone_number_id: effectivePhoneId || null,
|
||||||
|
api_version: row?.api_version || process.env.META_API_VERSION || 'v25.0',
|
||||||
|
webhook_token: row?.webhook_token || process.env.META_WEBHOOK_TOKEN || null,
|
||||||
|
is_configured: !!(effectiveToken && effectivePhoneId),
|
||||||
|
has_token: !!effectiveToken,
|
||||||
|
token_source: row?.access_token ? 'database' : effectiveToken ? 'env' : null,
|
||||||
|
updated_at: row?.updated_at || null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveConfig(
|
||||||
|
dto: { access_token?: string; phone_number_id?: string; api_version?: string; webhook_token?: string },
|
||||||
|
userId?: number,
|
||||||
|
) {
|
||||||
|
this._cache = null;
|
||||||
|
await this.repo.update({
|
||||||
|
access_token: dto.access_token || null,
|
||||||
|
phone_number_id: dto.phone_number_id || null,
|
||||||
|
api_version: dto.api_version || null,
|
||||||
|
webhook_token: dto.webhook_token || null,
|
||||||
|
updated_by: userId,
|
||||||
|
});
|
||||||
|
return this.getPublicConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateCredentials(token: string, phoneNumberId: string, apiVersion = 'v25.0') {
|
||||||
|
const url = `https://graph.facebook.com/${apiVersion}/${phoneNumberId}?access_token=${token}&fields=display_phone_number,verified_name,quality_rating`;
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = (await response.json()) as any;
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: data?.error?.message || 'Credenciais inválidas',
|
||||||
|
code: data?.error?.code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
phone_number: data.display_phone_number,
|
||||||
|
verified_name: data.verified_name,
|
||||||
|
quality_rating: data.quality_rating,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { valid: false, error: 'Erro de conexão com a Meta API' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
UpdateWhatsappTemplateDto,
|
UpdateWhatsappTemplateDto,
|
||||||
} from './dto/whatsapp.dto';
|
} from './dto/whatsapp.dto';
|
||||||
import { WhatsappTemplateService } from './whatsapp-template.service';
|
import { WhatsappTemplateService } from './whatsapp-template.service';
|
||||||
|
import { WhatsappConfigService } from './whatsapp-config.service';
|
||||||
import { Public } from '../auth/decorators/public.decorator';
|
import { Public } from '../auth/decorators/public.decorator';
|
||||||
|
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ export class WhatsappController {
|
|||||||
private readonly whatsappService: WhatsappService,
|
private readonly whatsappService: WhatsappService,
|
||||||
private readonly assignmentService: AttendanceAssignmentService,
|
private readonly assignmentService: AttendanceAssignmentService,
|
||||||
private readonly whatsappTemplateService: WhatsappTemplateService,
|
private readonly whatsappTemplateService: WhatsappTemplateService,
|
||||||
|
private readonly configService: WhatsappConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@ -275,17 +277,19 @@ export class WhatsappController {
|
|||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@ApiOperation({ summary: 'Verificação do webhook Meta' })
|
@ApiOperation({ summary: 'Verificação do webhook Meta' })
|
||||||
@Get('webhook')
|
@Get('webhook')
|
||||||
verifyWebhook(
|
async verifyWebhook(
|
||||||
@Query('hub.mode') mode: string,
|
@Query('hub.mode') mode: string,
|
||||||
@Query('hub.verify_token') token: string,
|
@Query('hub.verify_token') token: string,
|
||||||
@Query('hub.challenge') challenge: string,
|
@Query('hub.challenge') challenge: string,
|
||||||
) {
|
) {
|
||||||
if (mode === 'subscribe' && token === process.env.META_WEBHOOK_TOKEN) {
|
const config = await this.configService.getConfig();
|
||||||
return challenge;
|
const expected = config.webhook_token;
|
||||||
|
if (mode === 'subscribe' && expected && token === expected) {
|
||||||
|
return challenge;
|
||||||
|
}
|
||||||
|
throw new ForbiddenException('Token inválido');
|
||||||
}
|
}
|
||||||
throw new ForbiddenException('Token inválido');
|
|
||||||
}
|
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@ApiOperation({ summary: 'Recebe eventos do webhook Meta' })
|
@ApiOperation({ summary: 'Recebe eventos do webhook Meta' })
|
||||||
|
|||||||
@ -2,17 +2,28 @@ import { Module } from '@nestjs/common';
|
|||||||
import { WhatsappService } from './whatsapp.service';
|
import { WhatsappService } from './whatsapp.service';
|
||||||
import { WhatsappGateway } from './whatsapp.gateway';
|
import { WhatsappGateway } from './whatsapp.gateway';
|
||||||
import { WhatsappController } from './whatsapp.controller';
|
import { WhatsappController } from './whatsapp.controller';
|
||||||
|
import { WhatsappAdminController } from './whatsapp-admin.controller';
|
||||||
import { AttendanceModule } from '../attendance/attendance.module';
|
import { AttendanceModule } from '../attendance/attendance.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { ContactsModule } from '../contacts/contacts.module';
|
import { ContactsModule } from '../contacts/contacts.module';
|
||||||
import { WhatsappTemplateRepository } from './repositories/whatsapp-template.repository';
|
import { WhatsappTemplateRepository } from './repositories/whatsapp-template.repository';
|
||||||
import { WhatsappTemplateService } from './whatsapp-template.service';
|
import { WhatsappTemplateService } from './whatsapp-template.service';
|
||||||
import { MensagensRepository } from './repositories/mensagens.repository';
|
import { MensagensRepository } from './repositories/mensagens.repository';
|
||||||
|
import { WhatsappConfigRepository } from './repositories/whatsapp-config.repository';
|
||||||
|
import { WhatsappConfigService } from './whatsapp-config.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AttendanceModule, AuthModule, ContactsModule],
|
imports: [AttendanceModule, AuthModule, ContactsModule],
|
||||||
providers: [WhatsappService, WhatsappGateway, WhatsappTemplateService, WhatsappTemplateRepository, MensagensRepository],
|
providers: [
|
||||||
controllers: [WhatsappController],
|
WhatsappService,
|
||||||
|
WhatsappGateway,
|
||||||
|
WhatsappTemplateService,
|
||||||
|
WhatsappTemplateRepository,
|
||||||
|
MensagensRepository,
|
||||||
|
WhatsappConfigRepository,
|
||||||
|
WhatsappConfigService,
|
||||||
|
],
|
||||||
|
controllers: [WhatsappController, WhatsappAdminController],
|
||||||
exports: [WhatsappService],
|
exports: [WhatsappService],
|
||||||
})
|
})
|
||||||
export class WhatsappModule {}
|
export class WhatsappModule {}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { AttendanceAssignmentService } from '../attendance/attendance-assignment
|
|||||||
import { ContactsService } from '../contacts/contacts.service';
|
import { ContactsService } from '../contacts/contacts.service';
|
||||||
import { WhatsappTemplateService } from './whatsapp-template.service';
|
import { WhatsappTemplateService } from './whatsapp-template.service';
|
||||||
import { MensagensRepository } from './repositories/mensagens.repository';
|
import { MensagensRepository } from './repositories/mensagens.repository';
|
||||||
|
import { WhatsappConfigService } from './whatsapp-config.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WhatsappService {
|
export class WhatsappService {
|
||||||
@ -17,6 +18,7 @@ export class WhatsappService {
|
|||||||
private readonly contactsService: ContactsService,
|
private readonly contactsService: ContactsService,
|
||||||
private readonly whatsappTemplateService: WhatsappTemplateService,
|
private readonly whatsappTemplateService: WhatsappTemplateService,
|
||||||
private readonly mensagensRepository: MensagensRepository,
|
private readonly mensagensRepository: MensagensRepository,
|
||||||
|
private readonly configService: WhatsappConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async enqueueIncomingRoute(remoteJid: string, msg: any, messageBody: string, messageId: string) {
|
private async enqueueIncomingRoute(remoteJid: string, msg: any, messageBody: string, messageId: string) {
|
||||||
@ -112,12 +114,11 @@ export class WhatsappService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(to: string, message: string, _media?: { data: string; mimetype: string; filename?: string }, senderName?: string) {
|
async sendMessage(to: string, message: string, _media?: { data: string; mimetype: string; filename?: string }, senderName?: string) {
|
||||||
const token = process.env.META_ACCESS_TOKEN;
|
const { access_token: token, phone_number_id: phoneNumberId, api_version: apiVersion } =
|
||||||
const phoneNumberId = process.env.META_PHONE_NUMBER_ID;
|
await this.configService.getConfig();
|
||||||
const apiVersion = process.env.META_API_VERSION || 'v25.0';
|
|
||||||
|
|
||||||
if (!token || !phoneNumberId) {
|
if (!token || !phoneNumberId) {
|
||||||
throw new Error('META_ACCESS_TOKEN e META_PHONE_NUMBER_ID são obrigatórios.');
|
throw new Error('Token e Phone Number ID do WhatsApp não configurados. Acesse Admin → Canais e Integração → WhatsApp → Configurar.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = senderName ? `*Atendente: ${senderName}*\n\n${message}` : message;
|
const body = senderName ? `*Atendente: ${senderName}*\n\n${message}` : message;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user