diff --git a/backend/newwhats/index.js b/backend/newwhats/index.js index 3fad96d..88aea13 100644 --- a/backend/newwhats/index.js +++ b/backend/newwhats/index.js @@ -51,6 +51,40 @@ export function registerNewwhats(app, pool, opts = {}) { } }); + // Aplica um TOKEN de pareamento — auto-configura URL + chave + segredo do webhook. + // Token = "nwpair_" + base64url(JSON({ u: motorUrl, k: integrationKey, s: webhookSecret })). + // A clínica continua sendo escolhida localmente (o motor não conhece os IDs daqui). + app.post('/api/nw/config/token', superadminGuard, async (req, res) => { + try { + let raw = String((req.body || {}).token || '').trim(); + raw = raw.replace(/^nwpair[_:]\s*/i, ''); + let p; + try { p = JSON.parse(Buffer.from(raw, 'base64url').toString('utf8')); } + catch { return res.status(400).json({ error: 'Token inválido (não foi possível decodificar).' }); } + + const motorUrl = (p.u || p.url || '').replace(/\/$/, ''); + const integrationKey = p.k || p.key || p.integrationKey || ''; + const webhookSecret = p.s || p.secret || p.webhookSecret || ''; + if (!motorUrl || !integrationKey) { + return res.status(400).json({ error: 'Token incompleto (faltam URL ou chave).' }); + } + + // Valida contra o motor ANTES de salvar. + let motorOk = false, detail = ''; + try { + const r = await fetch(`${motorUrl}/api/ext/v1/sessions`, { headers: { 'x-nw-key': integrationKey } }); + motorOk = r.ok; detail = r.ok ? 'Conexão OK' : `Motor respondeu ${r.status}`; + } catch (e) { detail = `Motor inacessível: ${e.message}`; } + if (!motorOk) return res.status(400).json({ error: `Token não validou no motor: ${detail}` }); + + const cur = getConfig(); + await saveConfigToDb(pool, { motorUrl, integrationKey, webhookSecret: webhookSecret || cur.webhookSecret, clinicaId: cur.clinicaId }); + res.json({ ok: true, configured: isConfigured(), motorUrl, hasWebhookSecret: Boolean(webhookSecret || cur.webhookSecret), detail }); + } catch (e) { + res.status(500).json({ error: e.message }); + } + }); + // Lista de clínicas para o dropdown. app.get('/api/nw/clinicas', superadminGuard, async (_req, res) => { try { diff --git a/frontend/views/PluginsView.tsx b/frontend/views/PluginsView.tsx index 51e4c1f..ecfc8f3 100644 --- a/frontend/views/PluginsView.tsx +++ b/frontend/views/PluginsView.tsx @@ -215,25 +215,47 @@ const NewwhatsConfigModal: React.FC<{ plugin: PluginDef; onClose: () => void }> const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [test, setTest] = useState<{ ok: boolean; msg: string } | null>(null); + const [token, setToken] = useState(''); + const [applying, setApplying] = useState(false); + const [advanced, setAdvanced] = useState(false); + + const loadCfg = async () => { + try { + const cfgR = await fetch(`${NW_API}/nw/config`, { headers: nwAuthHeaders() }); + if (cfgR.ok) { + const c = await cfgR.json(); + setMotorUrl(c.motorUrl || ''); setClinicaId(c.clinicaId || ''); + setHasKey(!!c.hasIntegrationKey); setHasSecret(!!c.hasWebhookSecret); + } + } catch { /* noop */ } + }; useEffect(() => { (async () => { try { - const [cfgR, clR] = await Promise.all([ - fetch(`${NW_API}/nw/config`, { headers: nwAuthHeaders() }), - fetch(`${NW_API}/nw/clinicas`, { headers: nwAuthHeaders() }), - ]); - if (cfgR.ok) { - const c = await cfgR.json(); - setMotorUrl(c.motorUrl || ''); setClinicaId(c.clinicaId || ''); - setHasKey(!!c.hasIntegrationKey); setHasSecret(!!c.hasWebhookSecret); - } + const clR = await fetch(`${NW_API}/nw/clinicas`, { headers: nwAuthHeaders() }); if (clR.ok) setClinics(await clR.json()); + await loadCfg(); } catch { /* noop */ } finally { setLoading(false); } })(); }, []); + const applyToken = async () => { + if (!token.trim()) return; + setApplying(true); setTest(null); + try { + const r = await fetch(`${NW_API}/nw/config/token`, { method: 'POST', headers: nwAuthHeaders(), body: JSON.stringify({ token: token.trim() }) }); + const j = await r.json(); + if (!r.ok) throw new Error(j.error || 'Falha ao aplicar token'); + await loadCfg(); + setToken(''); + setTest({ ok: true, msg: 'Token aplicado — ' + (j.detail || 'configurado') }); + toast.success('TOKEN APLICADO! Conexão configurada.'); + } catch (e: any) { setTest({ ok: false, msg: String(e.message).slice(0, 120) }); } + finally { setApplying(false); } + }; + const handleSave = async () => { setSaving(true); try { @@ -272,10 +294,22 @@ const NewwhatsConfigModal: React.FC<{ plugin: PluginDef; onClose: () => void }>
{loading ?
Carregando…
: <> + {/* Caminho principal: colar o token de pareamento → auto-configura */}
- - setMotorUrl(e.target.value)} placeholder="https://newwhats.clube67.com" /> + +