228 lines
14 KiB
JavaScript
228 lines
14 KiB
JavaScript
"use strict";
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
require("dotenv/config");
|
|
const http_1 = __importDefault(require("http"));
|
|
const fs_1 = require("fs");
|
|
const express_1 = __importDefault(require("express"));
|
|
const cors_1 = __importDefault(require("cors"));
|
|
const helmet_1 = __importDefault(require("helmet"));
|
|
const path_1 = __importDefault(require("path"));
|
|
const env_1 = require("./config/env");
|
|
const logger_1 = require("./config/logger");
|
|
const prisma_1 = require("./infra/database/prisma");
|
|
// prisma re-usado inline abaixo (plan/status route)
|
|
const dragonfly_1 = require("./infra/cache/dragonfly");
|
|
const socketServer_1 = require("./infra/socket/socketServer");
|
|
const hook_bus_1 = require("./core/hook-bus");
|
|
const plugin_config_1 = require("./core/plugin-config");
|
|
const db_1 = require("./core/db");
|
|
const plugin_loader_1 = require("./core/plugin-loader");
|
|
const plugin_registry_1 = require("./core/plugin-registry");
|
|
const WhatsAppConnectionManager_1 = require("./modules/whatsapp/connection/WhatsAppConnectionManager");
|
|
const whatsapp_routes_1 = require("./modules/whatsapp/whatsapp.routes");
|
|
const chat_routes_1 = require("./modules/chats/chat.routes");
|
|
const whatsapp_send_routes_1 = require("./modules/whatsapp/whatsapp.send.routes");
|
|
const whatsapp_media_routes_1 = require("./modules/whatsapp/whatsapp.media.routes");
|
|
const chatbot_routes_1 = require("./modules/chatbot/chatbot.routes");
|
|
const contact_routes_1 = require("./modules/contacts/contact.routes");
|
|
const sector_routes_1 = require("./modules/sectors/sector.routes");
|
|
const apikey_routes_1 = require("./modules/api-keys/apikey.routes");
|
|
const auth_routes_1 = require("./modules/auth/auth.routes");
|
|
const admin_routes_1 = require("./modules/auth/admin.routes");
|
|
const pair_routes_1 = require("./modules/pair/pair.routes");
|
|
const template_routes_1 = require("./modules/templates/template.routes");
|
|
const scheduled_routes_1 = require("./modules/scheduled/scheduled.routes");
|
|
const sticker_routes_1 = require("./modules/stickers/sticker.routes");
|
|
const StorageProvider_1 = require("./core/StorageProvider");
|
|
const scheduler_service_1 = require("./modules/scheduled/scheduler.service");
|
|
const auth_middleware_1 = require("./shared/middlewares/auth.middleware");
|
|
const admin_middleware_1 = require("./shared/middlewares/admin.middleware");
|
|
const error_middleware_1 = require("./shared/middlewares/error.middleware");
|
|
async function bootstrap() {
|
|
// ── Express ──────────────────────────────────────────────────────────────
|
|
const app = (0, express_1.default)();
|
|
app.use((0, helmet_1.default)({ crossOriginResourcePolicy: { policy: 'cross-origin' } }));
|
|
app.use((0, cors_1.default)({ origin: env_1.env.FRONTEND_URL, credentials: true }));
|
|
app.use(express_1.default.json({ limit: '10mb' }));
|
|
// Servir arquivos de mídia baixados (ambos os prefixos: acesso direto e via proxy nginx)
|
|
const mediaStatic = express_1.default.static(path_1.default.resolve('./media'), { maxAge: '7d' });
|
|
app.use('/media', mediaStatic);
|
|
app.use('/api/media', mediaStatic);
|
|
// ── HTTP Server + Socket.IO ───────────────────────────────────────────────
|
|
const httpServer = http_1.default.createServer(app);
|
|
const io = (0, socketServer_1.buildSocketServer)(httpServer);
|
|
// ── WhatsApp Connection Manager ───────────────────────────────────────────
|
|
const whatsAppManager = new WhatsAppConnectionManager_1.WhatsAppConnectionManager(io);
|
|
globalThis.__whatsAppManager = whatsAppManager;
|
|
// ── Rotas ─────────────────────────────────────────────────────────────────
|
|
app.use('/api/auth', auth_routes_1.authRouter);
|
|
app.use('/api/pair', pair_routes_1.pairRouter);
|
|
// Configurações públicas de branding (sem autenticação — usadas pelo frontend antes do login)
|
|
app.get('/api/admin/settings/public', async (_req, res) => {
|
|
try {
|
|
const raw = await dragonfly_1.dragonfly.getJson('system:settings');
|
|
const defaults = { systemName: 'NewWhats', logoUrl: null, faviconUrl: null, accentColor: '#3b82f6', maintenance: { enabled: false, message: '' } };
|
|
const cfg = { ...defaults, ...(raw ?? {}) };
|
|
res.json({ systemName: cfg.systemName, logoUrl: cfg.logoUrl, faviconUrl: cfg.faviconUrl, accentColor: cfg.accentColor, maintenance: cfg.maintenance });
|
|
}
|
|
catch {
|
|
res.status(500).json({ error: 'Erro' });
|
|
}
|
|
});
|
|
app.use('/api/admin', auth_middleware_1.authMiddleware, admin_middleware_1.adminMiddleware, admin_routes_1.adminRouter);
|
|
app.use('/api/chats', auth_middleware_1.authMiddleware, (0, chat_routes_1.buildChatRoutes)(whatsAppManager));
|
|
app.use('/api/instances', auth_middleware_1.authMiddleware, (0, whatsapp_routes_1.buildWhatsAppRoutes)(whatsAppManager));
|
|
app.use('/api/instances/:instanceId', auth_middleware_1.authMiddleware, (0, whatsapp_send_routes_1.buildSendRoutes)(whatsAppManager));
|
|
app.use('/api/instances/:instanceId', auth_middleware_1.authMiddleware, (0, whatsapp_media_routes_1.buildMediaRoutes)(whatsAppManager));
|
|
app.use('/api/chatbot', auth_middleware_1.authMiddleware, (0, chatbot_routes_1.buildChatbotRoutes)());
|
|
app.use('/api/contacts', auth_middleware_1.authMiddleware, (0, contact_routes_1.buildContactRoutes)());
|
|
app.use('/api/sectors', auth_middleware_1.authMiddleware, (0, sector_routes_1.buildSectorRoutes)());
|
|
app.use('/api/api-keys', auth_middleware_1.authMiddleware, (0, apikey_routes_1.buildApiKeyRoutes)());
|
|
app.use('/api/inbox', auth_middleware_1.authMiddleware, (0, whatsapp_send_routes_1.buildAvatarRoute)(whatsAppManager));
|
|
// ── Templates e Agendamentos ──────────────────────────────────────────────
|
|
const scheduler = new scheduler_service_1.SchedulerService(whatsAppManager);
|
|
app.use('/api/templates', auth_middleware_1.authMiddleware, (0, template_routes_1.buildTemplateRoutes)());
|
|
app.use('/api/scheduled', auth_middleware_1.authMiddleware, (0, scheduled_routes_1.buildScheduledRoutes)(scheduler));
|
|
app.use('/api/stickers', auth_middleware_1.authMiddleware, (0, sticker_routes_1.buildStickerRoutes)());
|
|
// Proxy genérico para arquivos no Wasabi — sem authMiddleware porque <img>/<audio>/<video>
|
|
// do browser não enviam JWT. Paths são UUID-based (obscuros) — mesmo nível de segurança
|
|
// que a rota /media/ estática que também não requer autenticação.
|
|
app.get('/api/storage/view/*', async (req, res) => {
|
|
const filePath = req.params[0];
|
|
if (!filePath) {
|
|
res.status(400).end();
|
|
return;
|
|
}
|
|
try {
|
|
const buffer = await StorageProvider_1.storageProvider.getBuffer(filePath);
|
|
if (!buffer) {
|
|
res.status(404).end();
|
|
return;
|
|
}
|
|
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
const mime = ext === 'webp' ? 'image/webp' :
|
|
ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' :
|
|
ext === 'png' ? 'image/png' :
|
|
ext === 'mp4' ? 'video/mp4' :
|
|
ext === 'ogg' ? 'audio/ogg' :
|
|
'application/octet-stream';
|
|
res.setHeader('Content-Type', mime);
|
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
res.send(buffer);
|
|
}
|
|
catch {
|
|
res.status(500).end();
|
|
}
|
|
});
|
|
// Banner do frontend — dias restantes do plano
|
|
app.get('/api/plan/status', auth_middleware_1.authMiddleware, async (req, res) => {
|
|
const user = await prisma_1.prisma.user.findUnique({
|
|
where: { id: req.tenantId },
|
|
select: { trialEndsAt: true, plan: { select: { name: true } } },
|
|
});
|
|
const daysRemaining = user?.trialEndsAt
|
|
? Math.max(0, Math.ceil((new Date(user.trialEndsAt).getTime() - Date.now()) / 86_400_000))
|
|
: null;
|
|
res.json({ daysRemaining, planName: user?.plan?.name ?? 'Trial' });
|
|
});
|
|
// Presença: retorna se o próprio usuário está online (chave ainda viva no Dragonfly)
|
|
// Útil para self-check; para listar agentes online em admin, use a mesma lógica com prefixo
|
|
app.get('/api/presence/me', auth_middleware_1.authMiddleware, async (req, res) => {
|
|
const online = await dragonfly_1.dragonfly.exists(`presence:user:${req.tenantId}`);
|
|
res.json({ online });
|
|
});
|
|
// Health check (sem autenticação)
|
|
app.get('/health', (_req, res) => {
|
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
});
|
|
// Retorna menu items de plugins ativos — usado pelo frontend após login
|
|
app.get('/api/plugins/menu', auth_middleware_1.authMiddleware, (_req, res) => {
|
|
const items = plugin_registry_1.pluginRegistry.getActiveMenuItems();
|
|
res.json(items);
|
|
});
|
|
app.use(error_middleware_1.errorMiddleware);
|
|
// ── Infra: conectar Dragonfly e Prisma ───────────────────────────────────
|
|
await dragonfly_1.dragonfly.connect();
|
|
await prisma_1.prisma.$connect();
|
|
logger_1.logger.info('PostgreSQL conectado via Prisma');
|
|
// ── Plugin System ─────────────────────────────────────────────────────────
|
|
await (0, db_1.ensurePluginTables)(db_1.db);
|
|
const loadedPlugins = await (0, plugin_loader_1.loadPlugins)();
|
|
const pluginNames = loadedPlugins.map((p) => p.manifest.name);
|
|
await plugin_config_1.pluginConfig.loadAll(pluginNames);
|
|
const pluginCtx = {
|
|
app,
|
|
db: db_1.db,
|
|
prisma: prisma_1.prisma,
|
|
logger: logger_1.logger,
|
|
hooks: hook_bus_1.hookBus,
|
|
config: plugin_config_1.pluginConfig,
|
|
io,
|
|
httpServer,
|
|
};
|
|
plugin_registry_1.pluginRegistry.setContext(pluginCtx);
|
|
plugin_registry_1.pluginRegistry.register(loadedPlugins);
|
|
await plugin_registry_1.pluginRegistry.activateAll();
|
|
logger_1.logger.info({ count: pluginNames.length }, '🔌 Plugins carregados');
|
|
// ── Auto-reconectar instâncias ativas após reinício ──────────────────────
|
|
// Reseta CONNECTING/QR_PENDING para DISCONNECTED antes de reconectar:
|
|
// impede que a API retorne um status "fantasma" de antes do crash.
|
|
await prisma_1.prisma.instance.updateMany({
|
|
where: { status: { in: ['CONNECTING', 'QR_PENDING'] } },
|
|
data: { status: 'DISCONNECTED' },
|
|
});
|
|
// Broadcasts travados em RUNNING (processo morreu durante o loop de envio):
|
|
// marca como FAILED para não aparecerem eternamente como "Enviando…" no frontend.
|
|
const stuckCount = await prisma_1.prisma.broadcast.updateMany({
|
|
where: { status: 'RUNNING' },
|
|
data: { status: 'FAILED', finishedAt: new Date() },
|
|
});
|
|
if (stuckCount.count > 0) {
|
|
logger_1.logger.warn({ count: stuckCount.count }, '[Broadcast] Jobs RUNNING marcados como FAILED após reinício');
|
|
}
|
|
// Reconecta toda instância não-banida que tenha credenciais persistidas
|
|
// no disco (creds.json). O filesystem é a fonte de verdade para "essa
|
|
// instância já foi pareada" — o status no DB pode estar DISCONNECTED
|
|
// temporariamente porque o watchdog matou a conexão ANTES do timer de
|
|
// reconexão rodar (ex: restart do tsx watch durante o backoff de 3s).
|
|
// Sem esta lógica, a instância fica órfã até alguém chamar connect()
|
|
// pela API, causando perda de mensagens durante o hiato.
|
|
const candidates = await prisma_1.prisma.instance.findMany({
|
|
where: { status: { notIn: ['BANNED'] } },
|
|
});
|
|
for (const inst of candidates) {
|
|
const credsPath = path_1.default.join(env_1.env.BAILEYS_SESSIONS_PATH, inst.id, 'creds.json');
|
|
if (!(0, fs_1.existsSync)(credsPath)) {
|
|
// Nunca foi pareada — pular pra não disparar QR indesejado no boot
|
|
continue;
|
|
}
|
|
logger_1.logger.info({ instanceId: inst.id, previousStatus: inst.status }, 'Reconectando instância após reinício (creds persistidas)');
|
|
whatsAppManager.connect(inst.id, inst.tenantId).catch((err) => logger_1.logger.error({ err, instanceId: inst.id }, 'Falha ao reconectar instância'));
|
|
}
|
|
// ── Scheduler de mensagens ────────────────────────────────────────────────
|
|
scheduler.start();
|
|
// ── Iniciar servidor ──────────────────────────────────────────────────────
|
|
const port = parseInt(env_1.env.PORT, 10);
|
|
httpServer.listen(port, () => {
|
|
logger_1.logger.info(`🚀 NewWhats backend rodando em http://localhost:${port}`);
|
|
logger_1.logger.info(` Socket.IO pronto — frontend: ${env_1.env.FRONTEND_URL}`);
|
|
});
|
|
// ── Graceful shutdown ─────────────────────────────────────────────────────
|
|
const shutdown = async (signal) => {
|
|
logger_1.logger.info({ signal }, 'Encerrando servidor...');
|
|
scheduler.stop();
|
|
httpServer.close();
|
|
await prisma_1.prisma.$disconnect();
|
|
process.exit(0);
|
|
};
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
}
|
|
bootstrap().catch((err) => {
|
|
console.error('Falha fatal ao iniciar servidor:', err);
|
|
process.exit(1);
|
|
});
|