From 4e28601237a9e21a64ad6b458799321d1c2d85a5 Mon Sep 17 00:00:00 2001 From: Sidney Gomes Date: Wed, 22 Jul 2026 22:10:37 -0300 Subject: [PATCH] feat(whatsapp): integrate evolution api and admin settings --- package.json | 1 + prisma/schema.prisma | 6 ++ server.ts | 121 ++++++++++++++++++++++- src/components/AdminDashboard.tsx | 57 +++++++++++ src/components/WhatsappConnectCard.tsx | 131 +++++++++++++++++++++++++ src/types.ts | 5 + 6 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 src/components/WhatsappConnectCard.tsx diff --git a/package.json b/package.json index 30ff9f9..a9eb7eb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "multer": "^1.4.5-lts.1", "@tailwindcss/vite": "^4.1.14", "@vitejs/plugin-react": "^5.0.4", + "axios": "^1.7.9", "bcryptjs": "^3.0.3", "dotenv": "^17.2.3", "express": "^4.21.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d14aa3a..e877d02 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -238,6 +238,12 @@ model SystemSettings { asaasWebhookUrl String @default("") @map("asaas_webhook_url") asaasWebhookSecret String @default("") @map("asaas_webhook_secret") helpText String @db.Text @default("# Central de Ajuda") @map("help_text") + + evolutionApiUrl String @default("https://evolution.microtecinformaticacurso.com.br") @map("evolution_api_url") + evolutionApiKey String @default("") @map("evolution_api_key") + evolutionInstance String @default("microtecflix") @map("evolution_instance") + whatsappConnected Boolean @default(false) @map("whatsapp_connected") + whatsappPhoneNumber String? @map("whatsapp_phone_number") @@map("system_settings") } diff --git a/server.ts b/server.ts index 8893db3..c6c7226 100644 --- a/server.ts +++ b/server.ts @@ -3,6 +3,7 @@ import path from 'path'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import multer from 'multer'; +import axios from 'axios'; import { S3Client, PutObjectCommand, CreateBucketCommand, HeadBucketCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3'; import { createServer as createViteServer } from 'vite'; import { prisma } from './src/db/prisma'; @@ -986,7 +987,12 @@ app.get('/api/admin/settings', authenticateToken, requireAdmin, async (req: Auth asaasApiKey: settings?.asaasApiKey || '', asaasWebhookUrl: settings?.asaasWebhookUrl || 'https://admin-estudo.microtecinformaticacurso.com.br/api/webhooks/asaas', asaasWebhookSecret: settings?.asaasWebhookSecret || 'devflix_webhook_secret_key', - helpText: settings?.helpText || '' + helpText: settings?.helpText || '', + evolutionApiUrl: settings?.evolutionApiUrl || 'https://evolution.microtecinformaticacurso.com.br', + evolutionApiKey: settings?.evolutionApiKey || '', + evolutionInstance: settings?.evolutionInstance || 'microtecflix', + whatsappConnected: settings?.whatsappConnected || false, + whatsappPhoneNumber: settings?.whatsappPhoneNumber || '' }); }); @@ -1019,6 +1025,119 @@ app.post('/api/admin/test-asaas', authenticateToken, requireAdmin, async (req: A } }); +// --- EVOLUTION API WHATSAPP --- +app.get('/api/admin/whatsapp/qrcode', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { + try { + const settings = await prisma.systemSettings.findFirst(); + if (!settings || !settings.evolutionApiUrl || !settings.evolutionApiKey) { + return res.status(400).json({ error: 'Credenciais da Evolution API não configuradas.' }); + } + + const apiUrl = settings.evolutionApiUrl.replace(/\/$/, ''); + const instance = settings.evolutionInstance || 'microtecflix'; + const apiKey = settings.evolutionApiKey; + + const headers = { apikey: apiKey }; + + // Check connection state + try { + const stateRes = await axios.get(`${apiUrl}/instance/connectionState/${instance}`, { headers }); + + if (stateRes.data && stateRes.data.instance && stateRes.data.instance.state === 'open') { + if (!settings.whatsappConnected) { + await prisma.systemSettings.update({ + where: { id: settings.id }, + data: { whatsappConnected: true } + }); + } + return res.json({ connected: true, status: 'open' }); + } + } catch (err: any) { + if (err.response?.status === 404) { + // Instance does not exist, create it + await axios.post(`${apiUrl}/instance/create`, { + instanceName: instance, + qrcode: true + }, { headers }); + } else { + throw err; + } + } + + // Get QR Code + const qrRes = await axios.get(`${apiUrl}/instance/connect/${instance}`, { headers }); + + // Some versions of evolution return { base64: "...", pairingCode: "..." } + res.json({ + connected: false, + qrCode: qrRes.data.base64 || qrRes.data.qrcode || null, + pairingCode: qrRes.data.pairingCode || null + }); + + } catch (err: any) { + console.error('Erro na Evolution API (QR Code):', err.message); + res.status(500).json({ error: 'Falha ao comunicar com Evolution API.' }); + } +}); + +app.post('/api/admin/whatsapp/logout', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { + try { + const settings = await prisma.systemSettings.findFirst(); + if (!settings || !settings.evolutionApiUrl || !settings.evolutionApiKey) { + return res.status(400).json({ error: 'Credenciais não configuradas.' }); + } + + const apiUrl = settings.evolutionApiUrl.replace(/\/$/, ''); + const instance = settings.evolutionInstance || 'microtecflix'; + const apiKey = settings.evolutionApiKey; + + try { + await axios.delete(`${apiUrl}/instance/logout/${instance}`, { headers: { apikey: apiKey } }); + } catch (err: any) { + // ignore 404 or others during logout + } + + await prisma.systemSettings.update({ + where: { id: settings.id }, + data: { whatsappConnected: false } + }); + + res.json({ success: true }); + } catch (err: any) { + console.error('Erro na Evolution API (Logout):', err.message); + res.status(500).json({ error: 'Falha ao desconectar Evolution API.' }); + } +}); + +export async function sendWhatsappNotification(phone: string, text: string) { + try { + const settings = await prisma.systemSettings.findFirst(); + if (!settings?.whatsappConnected || !settings.evolutionApiUrl || !settings.evolutionApiKey) { + return false; // Not configured or not connected + } + + // Clean phone number (leave only digits) + const cleanPhone = phone.replace(/\D/g, ''); + if (!cleanPhone) return false; + + const apiUrl = settings.evolutionApiUrl.replace(/\/$/, ''); + const instance = settings.evolutionInstance || 'microtecflix'; + + await axios.post(`${apiUrl}/message/sendText/${instance}`, { + number: cleanPhone, + options: { delay: 1200 }, + textMessage: { text } + }, { + headers: { apikey: settings.evolutionApiKey } + }); + + return true; + } catch (err) { + console.error('Falha ao enviar WhatsApp:', err); + return false; + } +} + // Admin manually unlock or lock a course for a student app.post('/api/admin/students/:id/unlock-course', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { const { courseId, unlocked } = req.body; diff --git a/src/components/AdminDashboard.tsx b/src/components/AdminDashboard.tsx index 9cba89e..42c64db 100644 --- a/src/components/AdminDashboard.tsx +++ b/src/components/AdminDashboard.tsx @@ -6,6 +6,7 @@ import { import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Legend, Cell, PieChart, Pie } from 'recharts'; import { SubscriptionStatus } from '../types'; import { AdminPricingManager } from './AdminPricingManager'; +import { WhatsappConnectCard } from './WhatsappConnectCard'; interface WebhookLog { id: string; @@ -999,6 +1000,62 @@ export default function AdminDashboard({ token }: AdminDashboardProps) { + + {/* Integração Evolution API */} +
+

Integração WhatsApp (Evolution API)

+ +
+
+ + setSettings({...settings, evolutionApiUrl: e.target.value})} + className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-primary transition-colors" + placeholder="https://evolution.seu-dominio.com.br" + /> +
+ +
+ + setSettings({...settings, evolutionApiKey: e.target.value})} + className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-primary transition-colors" + placeholder="Sua chave secreta global" + /> +
+ +
+ + setSettings({...settings, evolutionInstance: e.target.value})} + className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-primary transition-colors" + placeholder="microtecflix" + /> +
+ +
+ + setSettings({...settings, whatsappPhoneNumber: e.target.value})} + className="w-full bg-black/40 border border-white/10 rounded-lg p-3 text-sm text-white focus:outline-none focus:border-primary transition-colors" + placeholder="5511999999999" + /> +
+
+
+ + + {/* Whatsapp Integration Component */} +
+
diff --git a/src/components/WhatsappConnectCard.tsx b/src/components/WhatsappConnectCard.tsx new file mode 100644 index 0000000..4c2495c --- /dev/null +++ b/src/components/WhatsappConnectCard.tsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { Smartphone, QrCode, LogOut, CheckCircle2, Loader2, AlertCircle } from 'lucide-react'; + +interface WhatsappConnectCardProps { + token: string | null; +} + +export const WhatsappConnectCard: React.FC = ({ token }) => { + const [loading, setLoading] = useState(false); + const [qrCode, setQrCode] = useState(null); + const [connected, setConnected] = useState(false); + const [error, setError] = useState(null); + + const fetchStatus = async () => { + if (!token) return; + setLoading(true); + setError(null); + try { + const res = await fetch('/api/admin/whatsapp/qrcode', { + headers: { 'Authorization': `Bearer ${token}` } + }); + const data = await res.json(); + + if (!res.ok) throw new Error(data.error || 'Erro ao comunicar com WhatsApp'); + + if (data.connected) { + setConnected(true); + setQrCode(null); + } else if (data.qrCode) { + setQrCode(data.qrCode); + setConnected(false); + } + } catch (err: any) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleLogout = async () => { + if (!token) return; + setLoading(true); + try { + await fetch('/api/admin/whatsapp/logout', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` } + }); + setConnected(false); + setQrCode(null); + } catch (err) { + console.error('Logout error:', err); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+
+ +
+
+

Conexão WhatsApp (Evolution API)

+

Escaneie o QR Code para conectar a API e enviar notificações automáticas.

+
+
+
+ + {error && ( +
+ +

{error}

+
+ )} + +
+ {loading ? ( +
+ +

Processando conexão...

+
+ ) : connected ? ( +
+
+ +
+
+

WhatsApp Conectado!

+

A plataforma já pode enviar notificações automáticas.

+
+ +
+ ) : qrCode ? ( +
+
+ WhatsApp QR Code +
+

+ Abra o WhatsApp no seu celular, vá em "Aparelhos Conectados" e escaneie o código acima. +

+ +
+ ) : ( +
+ +

Nenhuma instância conectada.

+ +
+ )} +
+
+ ); +}; diff --git a/src/types.ts b/src/types.ts index 6b94687..116094c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,11 @@ export interface SystemSettings { asaasWebhookSecret: string; helpText: string; enableBoleto?: boolean; + evolutionApiUrl?: string; + evolutionApiKey?: string; + evolutionInstance?: string; + whatsappConnected?: boolean; + whatsappPhoneNumber?: string; } export interface Course {