feat(newwhats): pareamento por TOKEN — cole 1 token e auto-configura
- Backend POST /api/nw/config/token (superadmin): decodifica nwpair_<base64url({u,k,s})>,
VALIDA no motor (/api/ext/v1/sessions) e salva URL+chave+webhook. Token inválido/
sem conexão é rejeitado antes de salvar.
- Frontend: campo 'Token de pareamento' como caminho principal (textarea + Aplicar);
estado da conexão; clínica no dropdown (local); campos manuais em 'configuração
manual' (avançado).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user