feat(newwhats): ownership de sessões, auto-provisionamento, assinatura e drawer na agenda
- Drawer de WhatsApp focado (WhatsChatDrawer) aberto do slot do agendamento, com dropdown de números do paciente + grupo familiar e fluxo de iniciar conversa - Auto-provisionamento de conta no motor: eager no cadastro/convite + self-heal no proxy (404 "conta não encontrada" → provisiona e refaz o request) - Ownership por sessão (wa_session_authz): dono do workspace (owner_id ou vínculo donoclinica/donoconsultorio) tem poder total; delega re-scan, excluir sessão, apagar conversa/mensagem, acesso ao Inbox e à Secretária — por conta - Enforcement no proxy: guardSessionAction (scan/rescan/delete sessão), guardAreaAccess (inbox/secretaria), guardInboxDestructive (apagar conversa/msg) - Endpoints /api/nw/authz (dono gerencia) e /api/nw/access (acesso agregado) - Assinatura do operador nas mensagens (*Nome*, desambiguação de homônimos → *Rui C.*) - UI: painel "Gerenciar acesso" com 5 toggles; menu e botões de apagar condicionados - Proxy de mídia /api/nw/media + re-download sob demanda Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,11 +6,23 @@
|
||||
// registerNewwhats(app, pool, { superadminGuard })
|
||||
import { createRoutes } from './routes.js'
|
||||
import { createWebhookReceiver } from './webhook-receiver.js'
|
||||
import { createRestProxy } from './proxy.js'
|
||||
import { createRestProxy, provisionNewwhatsAccount } from './proxy.js'
|
||||
import { getConfig, isConfigured, loadConfigFromDb, saveConfigToDb } from './config.js'
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
export { syncKnowledge, buildKnowledgeText } from './sync-knowledge.js'
|
||||
export { createWsTunnel } from './proxy.js'
|
||||
|
||||
// Provisiona (eager) a conta do email no motor NewWhats, best-effort e idempotente.
|
||||
// Chamado no cadastro de usuário/clínica do scoreodonto: "assim que a conta nasce
|
||||
// aqui, já nasce lá — pronta esperando quando a pessoa for mexer com WhatsApp".
|
||||
export async function provisionNewwhatsAccountEager(pool, email) {
|
||||
try {
|
||||
const cfg = getConfig()
|
||||
if (!cfg.motorUrl || !cfg.integrationKey || !email) return false
|
||||
return await provisionNewwhatsAccount(pool, cfg, email)
|
||||
} catch { return false }
|
||||
}
|
||||
import { createAgendaBridge } from './agenda-bridge.js'
|
||||
|
||||
// Valida uma chave contra o motor. Envia um email-sonda (a chave mestra exige
|
||||
@@ -36,6 +48,33 @@ export function registerNewwhats(app, pool, opts = {}) {
|
||||
// Auth própria por segredo compartilhado (x-nw-agenda-secret). Server-to-server.
|
||||
app.use('/api/nw/agenda', createAgendaBridge(pool));
|
||||
|
||||
// ── Proxy de MÍDIA: /api/nw/media/* → motor /media/* ───────────────────────
|
||||
// A mídia do WhatsApp vive no motor (servida do Wasabi/local, pública). O
|
||||
// satélite a repassa mantendo same-origin no <img>/<video> (sem exigir header
|
||||
// de auth no browser), com suporte a Range para áudio/vídeo.
|
||||
app.use('/api/nw/media', async (req, res) => {
|
||||
if (req.method !== 'GET') { res.status(405).end(); return; }
|
||||
const cfg = getConfig();
|
||||
if (!cfg.motorUrl) { res.status(503).end(); return; }
|
||||
const rest = req.path.replace(/^\//, '');
|
||||
const url = `${cfg.motorUrl.replace(/\/$/, '')}/media/${rest}`;
|
||||
try {
|
||||
const headers = {};
|
||||
if (req.headers.range) headers.range = req.headers.range;
|
||||
const r = await fetch(url, { headers });
|
||||
res.status(r.status);
|
||||
for (const h of ['content-type', 'content-length', 'content-range', 'accept-ranges', 'cache-control', 'content-disposition']) {
|
||||
const v = r.headers.get(h);
|
||||
if (v) res.setHeader(h, v);
|
||||
}
|
||||
if (!res.getHeader('cache-control')) res.setHeader('cache-control', 'private, max-age=3600');
|
||||
if (r.body) Readable.fromWeb(r.body).pipe(res);
|
||||
else res.end();
|
||||
} catch (e) {
|
||||
res.status(502).json({ error: `Mídia indisponível: ${e.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// Carrega a config do banco para o cache (não bloqueia o boot).
|
||||
loadConfigFromDb(pool).then((c) =>
|
||||
console.log(`[newwhats] config carregada — ${isConfigured() ? 'configurada' : 'AGUARDANDO config'} (clinica=${c.clinicaId || '—'})`)
|
||||
@@ -121,7 +160,7 @@ export function registerNewwhats(app, pool, opts = {}) {
|
||||
// ── Operação ────────────────────────────────────────────────────────────────
|
||||
app.use('/nw', createRoutes(pool)); // Secretária do motor consulta a clínica
|
||||
app.use(createWebhookReceiver(pool)); // webhook motor → satélite
|
||||
app.use('/api/nw/v1', createRestProxy()); // proxy do inbox → motor (express 5: use, não wildcard)
|
||||
app.use('/api/nw/v1', createRestProxy(pool)); // proxy do inbox → motor (express 5: use, não wildcard) + enforcement de ownership
|
||||
|
||||
console.log('[newwhats] integração registrada (/nw, /api/webhook/newwhats, /api/nw/v1/*, /api/nw/config)');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user