Files
clube67_newwhats.local/newwhats.clube67.com/newwhats.local/backend/src/modules/pair/pair.service.js
T
VPS 4 Deploy Agent 5ec6bd6354
continuous-integration/webhook Falha no deploy de clube67_newwhats.local (VPS 4)
chore(ops): migrate clube67 to newwhats.clube67.com directory
2026-05-18 03:26:41 +02:00

205 lines
9.2 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PairService = void 0;
const crypto_1 = __importDefault(require("crypto"));
const nodemailer_1 = __importDefault(require("nodemailer"));
const prisma_1 = require("../../infra/database/prisma");
const dragonfly_1 = require("../../infra/cache/dragonfly");
const logger_1 = require("../../config/logger");
const OTP_TTL = 10 * 60; // 10 minutos
const KEY_TTL = 90 * 24 * 3600; // 90 dias
// ─── Keys do Dragonfly ────────────────────────────────────────────────────────
const otpKey = (email) => `pair:otp:${email}`;
const intKey = (integrationKey) => `pair:key:${integrationKey}`;
// ─── Email sender ─────────────────────────────────────────────────────────────
async function sendOtpEmail(to, code, clientName) {
// Lê config SMTP do admin settings
const raw = await dragonfly_1.dragonfly.getJson('system:settings');
const smtp = raw?.smtp ?? {};
if (!smtp.host || !smtp.user) {
logger_1.logger.warn('[pair] SMTP não configurado — email de OTP não enviado. Configure em Admin → Configurações.');
logger_1.logger.info(`[pair] OTP para ${to}: ${code}`); // fallback: loga localmente
return false;
}
const transporter = nodemailer_1.default.createTransport({
host: smtp.host,
port: smtp.port ?? 587,
secure: smtp.secure ?? false,
auth: { user: smtp.user, pass: smtp.password },
});
await transporter.sendMail({
from: smtp.from ?? smtp.user,
to,
subject: `[NewWhats] Código de pareamento — ${clientName}`,
text: [
`Olá!`,
``,
`O sistema "${clientName}" solicitou conexão com sua conta NewWhats.`,
``,
`Seu código de confirmação: ${code}`,
``,
`Válido por 10 minutos.`,
``,
`Se você não solicitou isso, ignore este email.`,
].join('\n'),
html: `
<p>Olá!</p>
<p>O sistema <strong>${clientName}</strong> solicitou conexão com sua conta NewWhats.</p>
<h2 style="letter-spacing:8px;font-size:32px">${code}</h2>
<p style="color:#888">Válido por 10 minutos. Se não foi você, ignore este email.</p>
`,
});
logger_1.logger.info(`[pair] OTP enviado para ${to}`);
return true;
}
// ─── PairService ──────────────────────────────────────────────────────────────
class PairService {
// Etapa 1 — solicitar pareamento
async request(input) {
const user = await prisma_1.prisma.user.findUnique({ where: { email: input.email } });
if (!user) {
// Resposta genérica — não revela se o email existe ou não
return { ok: true, message: 'Se o email existir, um código será enviado.' };
}
if (!user.isActive) {
throw Object.assign(new Error('Conta desativada'), { statusCode: 403 });
}
// Gera OTP de 6 dígitos
const code = Math.floor(100000 + Math.random() * 900000).toString();
const payload = {
code,
clientSystem: input.clientSystem,
clientName: input.clientName,
clientUrl: input.clientUrl,
};
await dragonfly_1.dragonfly.setJson(otpKey(input.email), payload, OTP_TTL);
const emailSent = await sendOtpEmail(input.email, code, input.clientName);
// Quando SMTP não está configurado, devolve o código na resposta
// para que o admin possa completar o pareamento manualmente.
if (!emailSent) {
return { ok: true, message: 'SMTP não configurado.', dev_code: code };
}
return { ok: true, message: 'Se o email existir, um código será enviado.' };
}
// Etapa 2 — confirmar com o código recebido
async confirm(input) {
const stored = await dragonfly_1.dragonfly.getJson(otpKey(input.email));
if (!stored) {
throw Object.assign(new Error('Código expirado ou inexistente. Solicite um novo.'), { statusCode: 400 });
}
if (stored.code !== input.code) {
throw Object.assign(new Error('Código incorreto.'), { statusCode: 400 });
}
if (stored.clientSystem !== input.clientSystem) {
throw Object.assign(new Error('Sistema não corresponde ao da solicitação.'), { statusCode: 400 });
}
const user = await prisma_1.prisma.user.findUnique({ where: { email: input.email } });
if (!user || !user.isActive) {
throw Object.assign(new Error('Conta não encontrada ou inativa.'), { statusCode: 404 });
}
// Revoga par anterior do mesmo sistema (evita duplicatas)
await prisma_1.prisma.pluginPair.updateMany({
where: { userId: user.id, clientSystem: input.clientSystem, revokedAt: null },
data: { revokedAt: new Date() },
});
// Gera integration_key exclusiva
const integrationKey = `nwk_${crypto_1.default.randomUUID().replace(/-/g, '')}`;
const pair = await prisma_1.prisma.pluginPair.create({
data: {
userId: user.id,
clientSystem: input.clientSystem,
clientName: input.clientName,
clientUrl: input.clientUrl,
integrationKey,
},
});
// Cache rápido para validação sem bater no banco
const keyPayload = {
userId: user.id,
email: user.email,
clientSystem: input.clientSystem,
clientName: input.clientName,
};
await dragonfly_1.dragonfly.setJson(intKey(integrationKey), keyPayload, KEY_TTL);
// Limpa OTP usado
await dragonfly_1.dragonfly.del(otpKey(input.email));
logger_1.logger.info(`[pair] ${user.email}${input.clientName} pareados (${pair.id})`);
return {
integration_key: integrationKey,
account: { name: user.name, email: user.email },
paired_at: pair.createdAt,
};
}
// Verifica se uma integration_key é válida (chamada pelo sistema cliente)
async verify(integrationKey) {
// Fast path — Dragonfly
const cached = await dragonfly_1.dragonfly.getJson(intKey(integrationKey));
if (cached) {
// Atualiza lastSeenAt de forma assíncrona (sem bloquear a resposta)
prisma_1.prisma.pluginPair.updateMany({
where: { integrationKey, revokedAt: null },
data: { lastSeenAt: new Date() },
}).catch(() => { });
return { valid: true, account: { email: cached.email }, client: cached.clientSystem };
}
// Slow path — banco
const pair = await prisma_1.prisma.pluginPair.findUnique({
where: { integrationKey },
include: { user: { select: { name: true, email: true, isActive: true } } },
});
if (!pair || pair.revokedAt || !pair.user.isActive) {
return { valid: false };
}
// Restaura cache
const keyPayload = {
userId: pair.userId,
email: pair.user.email,
clientSystem: pair.clientSystem,
clientName: pair.clientName,
};
await dragonfly_1.dragonfly.setJson(intKey(integrationKey), keyPayload, KEY_TTL);
await prisma_1.prisma.pluginPair.update({
where: { integrationKey },
data: { lastSeenAt: new Date() },
});
return { valid: true, account: { name: pair.user.name, email: pair.user.email }, client: pair.clientSystem };
}
// Lista pares ativos do usuário autenticado
async list(userId) {
const pairs = await prisma_1.prisma.pluginPair.findMany({
where: { userId, revokedAt: null },
orderBy: { createdAt: 'desc' },
select: {
id: true,
clientSystem: true,
clientName: true,
clientUrl: true,
lastSeenAt: true,
createdAt: true,
},
});
return pairs;
}
// Revoga um par (usuário autenticado só pode revogar os seus)
async revoke(userId, pairId) {
const pair = await prisma_1.prisma.pluginPair.findFirst({
where: { id: pairId, userId, revokedAt: null },
});
if (!pair) {
throw Object.assign(new Error('Conexão não encontrada'), { statusCode: 404 });
}
await prisma_1.prisma.pluginPair.update({
where: { id: pairId },
data: { revokedAt: new Date() },
});
// Remove do cache imediatamente
await dragonfly_1.dragonfly.del(intKey(pair.integrationKey));
logger_1.logger.info(`[pair] Par ${pairId} revogado por ${userId}`);
return { ok: true };
}
}
exports.PairService = PairService;
//# sourceMappingURL=pair.service.js.map