From 91eedc220bda3d1b21d245a837b342190c494572 Mon Sep 17 00:00:00 2001 From: fpereirasilv Date: Thu, 15 Oct 2026 13:08:16 -0300 Subject: [PATCH] fix: resolve Mixed Content (Failed to fetch) in hosted HTTPS environment via /api-wpp reverse proxy --- netlify.toml | 17 +++ nginx.conf | 11 ++ public/_redirects | 6 + src/utils/wppConnectApi.js | 253 +++++++++++++++++++++++-------------- vercel.json | 12 ++ vite.config.js | 9 ++ 6 files changed, 212 insertions(+), 96 deletions(-) create mode 100644 netlify.toml create mode 100644 public/_redirects create mode 100644 vercel.json diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..9a1ada8 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,17 @@ +[build] + publish = "dist" + command = "npm run build" + +# Proxy Reverso para a API WPPConnect C2 Sistemas +# Resolve o bloqueio de "Mixed Content" (Página HTTPS chamando API HTTP na porta 22700) +[[redirects]] + from = "/api-wpp/*" + to = "http://www.c2wpp.c2sistemas.com.br:22700/:splat" + status = 200 + force = true + +# Roteamento Single Page Application (SPA) +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 diff --git a/nginx.conf b/nginx.conf index f03400b..b5b72ec 100644 --- a/nginx.conf +++ b/nginx.conf @@ -24,6 +24,17 @@ server { try_files $uri $uri/ /index.html; } + # Proxy reverso para WPPConnect (Resolve Mixed Content HTTPS -> HTTP) + location /api-wpp/ { + proxy_pass http://www.c2wpp.c2sistemas.com.br:22700/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host www.c2wpp.c2sistemas.com.br:22700; + proxy_cache_bypass $http_upgrade; + proxy_read_timeout 90s; + } + # Cache de assets estáticos location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; diff --git a/public/_redirects b/public/_redirects new file mode 100644 index 0000000..cdd77b2 --- /dev/null +++ b/public/_redirects @@ -0,0 +1,6 @@ +# Proxy reverso para a API WPPConnect +# Permite que aplicações hospedadas em HTTPS façam chamadas para o servidor HTTP :22700 sem bloqueio de Mixed Content +/api-wpp/* http://www.c2wpp.c2sistemas.com.br:22700/:splat 200! + +# SPA Fallback (Single Page Application - React Router / Vite) +/* /index.html 200 diff --git a/src/utils/wppConnectApi.js b/src/utils/wppConnectApi.js index 5259da3..dafe28d 100644 --- a/src/utils/wppConnectApi.js +++ b/src/utils/wppConnectApi.js @@ -5,10 +5,32 @@ export function normalizeUrl(url) { if (!url) return ''; let u = url.trim().replace(/\/+$/, ''); + + // Se a página estiver rodando em HTTPS (ex: Netlify, Vercel ou servidor com SSL) + // e a URL apontar para o servidor WPP HTTP padrão (porta 22700), o navegador bloqueia por "Mixed Content". + // Redirecionamos automaticamente para o proxy reverso relativo /api-wpp provido pelo Netlify/Vercel/Nginx. + if (typeof window !== 'undefined' && window.location.protocol === 'https:') { + if (u.includes('c2wpp.c2sistemas.com.br:22700') || u.startsWith('/api-wpp')) { + return `${window.location.origin}/api-wpp`; + } + } + if (!/^https?:\/\//i.test(u)) u = 'http://' + u; return u; } +export function formatFetchError(err, baseUrl = '') { + if (!err) return 'Erro desconhecido de conexão.'; + const msg = err.message || ''; + if (msg.includes('Failed to fetch') || msg.includes('NetworkError') || err.name === 'TypeError') { + if (typeof window !== 'undefined' && window.location.protocol === 'https:' && !baseUrl.includes('/api-wpp')) { + return 'Bloqueio de Conteúdo Misto (Mixed Content): O site está em HTTPS, mas o servidor WPP está em HTTP. O proxy reverso /api-wpp intermediará as requisições.'; + } + return 'Falha na conexão com o servidor WPP. Verifique se o servidor está online e acessível.'; + } + return msg; +} + export function ensureQRPrefix(qrCode) { if (!qrCode) return ''; return qrCode.startsWith('data:') ? qrCode : `data:image/png;base64,${qrCode}`; @@ -24,18 +46,22 @@ export function getSessionAuth(server, session) { */ export async function testServerConnection(baseUrl, secretKey) { const normUrl = normalizeUrl(baseUrl); - const res = await fetch(`${normUrl}/api/${secretKey}/show-all-sessions`, { - headers: { 'Authorization': `Bearer ${secretKey}` }, - signal: AbortSignal.timeout(8000), - }); + try { + const res = await fetch(`${normUrl}/api/${secretKey}/show-all-sessions`, { + headers: { 'Authorization': `Bearer ${secretKey}` }, + signal: AbortSignal.timeout(10000), + }); - if (res.ok) { - return { ok: true, message: 'Conexão OK! Servidor respondeu com sucesso.' }; - } else if (res.status === 401) { - return { ok: false, error: 'Token incorreto. Verifique a Secret Key.' }; - } else { - const text = await res.text().catch(() => ''); - return { ok: false, error: `Erro ${res.status}: ${text || 'Resposta inesperada'}` }; + if (res.ok) { + return { ok: true, message: 'Conexão OK! Servidor WPPConnect respondeu com sucesso.' }; + } else if (res.status === 401) { + return { ok: false, error: 'Token incorreto. Verifique a Secret Key.' }; + } else { + const text = await res.text().catch(() => ''); + return { ok: false, error: `Erro ${res.status}: ${text || 'Resposta inesperada'}` }; + } + } catch (err) { + return { ok: false, error: formatFetchError(err, normUrl) }; } } @@ -52,21 +78,25 @@ export async function startSession(server, sessionName, options = {}) { if (session?.webhook) body.webhook = session.webhook; if (session?.proxy) body.proxy = session.proxy; - const res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${authToken}`, - }, - body: JSON.stringify(body), - }); + try { + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}`, + }, + body: JSON.stringify(body), + }); - if (!res.ok) { - const errText = await res.text(); - throw new Error(errText || `HTTP ${res.status}`); + if (!res.ok) { + const errText = await res.text().catch(() => ''); + throw new Error(errText || `HTTP ${res.status}`); + } + + return await res.json(); + } catch (err) { + throw new Error(formatFetchError(err, baseUrl)); } - - return await res.json(); } /** @@ -74,20 +104,25 @@ export async function startSession(server, sessionName, options = {}) { */ export async function generateToken(server, sessionName) { const baseUrl = normalizeUrl(server.baseUrl); - const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/generate-token`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${server.secretKey}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ session: sessionName }), - }); + try { + const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/generate-token`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${server.secretKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ session: sessionName }), + }); - if (res.ok) { - const data = await res.json(); - return data.token || null; + if (res.ok) { + const data = await res.json(); + return data.token || null; + } + return null; + } catch (err) { + console.warn('Erro ao gerar token:', formatFetchError(err, baseUrl)); + return null; } - return null; } /** @@ -98,16 +133,21 @@ export async function checkSessionStatus(server, sessionName) { const session = server.sessions?.find(s => s.name === sessionName); const authToken = getSessionAuth(server, session); - const res = await fetch(`${baseUrl}/api/${sessionName}/check-connection-session`, { - headers: { 'Authorization': `Bearer ${authToken}` }, - signal: AbortSignal.timeout(6000), - }); + try { + const res = await fetch(`${baseUrl}/api/${sessionName}/check-connection-session`, { + headers: { 'Authorization': `Bearer ${authToken}` }, + signal: AbortSignal.timeout(8000), + }); - if (res.ok) { - const data = await res.json(); - return data.status || 'unknown'; + if (res.ok) { + const data = await res.json(); + return data.status || 'unknown'; + } + return 'error'; + } catch (err) { + console.warn('Erro ao verificar status:', formatFetchError(err, baseUrl)); + return 'error'; } - return 'error'; } /** @@ -118,25 +158,30 @@ export async function fetchQRCode(server, sessionName) { const session = server.sessions?.find(s => s.name === sessionName); const authToken = getSessionAuth(server, session); - const qrRes = await fetch(`${baseUrl}/api/${sessionName}/qrcode-session`, { - headers: { 'Authorization': `Bearer ${authToken}` }, - signal: AbortSignal.timeout(10000), - }); + try { + const qrRes = await fetch(`${baseUrl}/api/${sessionName}/qrcode-session`, { + headers: { 'Authorization': `Bearer ${authToken}` }, + signal: AbortSignal.timeout(12000), + }); - if (!qrRes.ok) return null; + if (!qrRes.ok) return null; - const ct = qrRes.headers.get('content-type') || ''; - if (ct.includes('json')) { - const data = await qrRes.json(); - return data.qrcode ? ensureQRPrefix(data.qrcode) : null; - } else if (ct.includes('image')) { - const buf = await qrRes.arrayBuffer(); - const bytes = new Uint8Array(buf); - let binary = ''; - for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); - return `data:${ct};base64,${btoa(binary)}`; + const ct = qrRes.headers.get('content-type') || ''; + if (ct.includes('json')) { + const data = await qrRes.json(); + return data.qrcode ? ensureQRPrefix(data.qrcode) : null; + } else if (ct.includes('image')) { + const buf = await qrRes.arrayBuffer(); + const bytes = new Uint8Array(buf); + let binary = ''; + for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); + return `data:${ct};base64,${btoa(binary)}`; + } + return null; + } catch (err) { + console.warn('Erro ao buscar QR code:', formatFetchError(err, baseUrl)); + return null; } - return null; } /** @@ -147,17 +192,21 @@ export async function closeSession(server, sessionName) { const session = server.sessions?.find(s => s.name === sessionName); let authToken = getSessionAuth(server, session); - const res = await fetch(`${baseUrl}/api/${sessionName}/close-session`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, - body: JSON.stringify({ secretkey: server.secretKey }), - }); + try { + const res = await fetch(`${baseUrl}/api/${sessionName}/close-session`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, + body: JSON.stringify({ secretkey: server.secretKey }), + }); - if (!res.ok) { - const errBody = await res.text().catch(() => ''); - throw new Error(errBody || `HTTP ${res.status}`); + if (!res.ok) { + const errBody = await res.text().catch(() => ''); + throw new Error(errBody || `HTTP ${res.status}`); + } + return true; + } catch (err) { + throw new Error(formatFetchError(err, baseUrl)); } - return true; } /** @@ -168,17 +217,21 @@ export async function logoutSession(server, sessionName) { const session = server.sessions?.find(s => s.name === sessionName); let authToken = getSessionAuth(server, session); - const res = await fetch(`${baseUrl}/api/${sessionName}/logout-session`, { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, - body: JSON.stringify({ secretkey: server.secretKey }), - }); + try { + const res = await fetch(`${baseUrl}/api/${sessionName}/logout-session`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` }, + body: JSON.stringify({ secretkey: server.secretKey }), + }); - if (!res.ok) { - const errBody = await res.text().catch(() => ''); - throw new Error(errBody || `HTTP ${res.status}`); + if (!res.ok) { + const errBody = await res.text().catch(() => ''); + throw new Error(errBody || `HTTP ${res.status}`); + } + return true; + } catch (err) { + throw new Error(formatFetchError(err, baseUrl)); } - return true; } /** @@ -186,19 +239,23 @@ export async function logoutSession(server, sessionName) { */ export async function clearSessionData(server, sessionName) { const baseUrl = normalizeUrl(server.baseUrl); - const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/clear-session-data`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${server.secretKey}`, - }, - }); + try { + const res = await fetch(`${baseUrl}/api/${sessionName}/${server.secretKey}/clear-session-data`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${server.secretKey}`, + }, + }); - if (!res.ok) { - const errBody = await res.text().catch(() => ''); - throw new Error(errBody || `HTTP ${res.status}`); + if (!res.ok) { + const errBody = await res.text().catch(() => ''); + throw new Error(errBody || `HTTP ${res.status}`); + } + return true; + } catch (err) { + throw new Error(formatFetchError(err, baseUrl)); } - return true; } /** @@ -206,12 +263,16 @@ export async function clearSessionData(server, sessionName) { */ export async function listAllServerSessions(server) { const baseUrl = normalizeUrl(server.baseUrl); - const res = await fetch(`${baseUrl}/api/${server.secretKey}/show-all-sessions`, { - headers: { 'Authorization': `Bearer ${server.secretKey}` }, - signal: AbortSignal.timeout(10000), - }); + try { + const res = await fetch(`${baseUrl}/api/${server.secretKey}/show-all-sessions`, { + headers: { 'Authorization': `Bearer ${server.secretKey}` }, + signal: AbortSignal.timeout(12000), + }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const data = await res.json(); - return data.response || []; + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + return data.response || []; + } catch (err) { + throw new Error(formatFetchError(err, baseUrl)); + } } diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..4416eb6 --- /dev/null +++ b/vercel.json @@ -0,0 +1,12 @@ +{ + "rewrites": [ + { + "source": "/api-wpp/:match*", + "destination": "http://www.c2wpp.c2sistemas.com.br:22700/:match*" + }, + { + "source": "/(.*)", + "destination": "/index.html" + } + ] +} diff --git a/vite.config.js b/vite.config.js index 9982072..b7550b3 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,4 +4,13 @@ import { defineConfig } from 'vite' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + server: { + proxy: { + '/api-wpp': { + target: 'http://www.c2wpp.c2sistemas.com.br:22700', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api-wpp/, '') + } + } + } })