205 lines
9.2 KiB
JavaScript
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
|