"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ExportService = void 0; const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const puppeteer_1 = __importDefault(require("puppeteer")); const handlebars_1 = __importDefault(require("handlebars")); const markdown_it_1 = __importDefault(require("markdown-it")); const logger_1 = require("../lib/logger"); const prisma_1 = require("../lib/prisma"); class ExportService { markdownRenderer; constructor() { this.markdownRenderer = new markdown_it_1.default(); this.initializeHandlebarsHelpers(); } initializeHandlebarsHelpers() { handlebars_1.default.registerHelper('formatDate', (date) => { return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short' }).format(date); }); handlebars_1.default.registerHelper('formatDuration', (ms) => { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`; return `${(ms / 60000).toFixed(2)}m`; }); handlebars_1.default.registerHelper('formatUrl', (url, maxLength = 80) => { if (url.length <= maxLength) return url; return url.substring(0, maxLength - 3) + '...'; }); handlebars_1.default.registerHelper('statusBadge', (status) => { const badges = { 'OK': '🟢 OK', 'ERROR': '🔴 ERROR', 'TIMEOUT': '🟡 TIMEOUT', 'LOOP': '🟣 LOOP' }; return badges[status] || `⚪ ${status}`; }); handlebars_1.default.registerHelper('redirectType', (type) => { if (!type) return '-'; return type.replace('HTTP_', '').replace('_', ' '); }); handlebars_1.default.registerHelper('if_eq', function (a, b, options) { if (typeof options !== 'object' || !options.fn) { return false; } if (a === b) { return options.fn(this); } return options.inverse ? options.inverse(this) : ''; }); handlebars_1.default.registerHelper('lt', function (a, b, options) { if (typeof options !== 'object' || !options.fn) { return false; } if (a < b) { return options.fn(this); } return options.inverse ? options.inverse(this) : ''; }); handlebars_1.default.registerHelper('gte', function (a, b, options) { if (typeof options !== 'object' || !options.fn) { return false; } if (a >= b) { return options.fn(this); } return options.inverse ? options.inverse(this) : ''; }); } generateMermaidDiagram(hops) { if (hops.length === 0) { return 'graph TD\n A[No redirects found]'; } let diagram = 'graph TD\n'; for (let i = 0; i < hops.length; i++) { const hop = hops[i]; const nodeId = `H${i}`; const nextNodeId = `H${i + 1}`; const statusCode = hop.statusCode ? ` (${hop.statusCode})` : ''; const domain = new URL(hop.url).hostname; diagram += ` ${nodeId}["${domain}${statusCode}"]\n`; if (i < hops.length - 1) { const redirectType = hop.redirectType ? hop.redirectType.replace('HTTP_', '').replace('_', ' ') : 'REDIRECT'; diagram += ` ${nodeId} -->|"${redirectType}"| ${nextNodeId}\n`; } if (hop.statusCode) { if (hop.statusCode >= 300 && hop.statusCode < 400) { diagram += ` ${nodeId} --> ${nodeId}\n`; diagram += ` class ${nodeId} redirect\n`; } else if (hop.statusCode >= 400) { diagram += ` class ${nodeId} error\n`; } else { diagram += ` class ${nodeId} success\n`; } } } diagram += ` classDef redirect fill:#ffd700,stroke:#333,stroke-width:2px classDef error fill:#ff6b6b,stroke:#333,stroke-width:2px classDef success fill:#51cf66,stroke:#333,stroke-width:2px `; return diagram; } async getExportData(checkId, userId) { const check = await prisma_1.prisma.check.findUnique({ where: { id: checkId }, include: { hops: { orderBy: { hopIndex: 'asc' } }, sslInspections: true, seoFlags: true, securityFlags: true, project: { include: { organization: { include: { memberships: userId ? { include: { user: true }, where: { userId }, take: 1 } : true } } } } } }); if (!check) { throw new Error('Check not found'); } const exportData = { check: { id: check.id, inputUrl: check.inputUrl, finalUrl: check.finalUrl || '', method: check.method, status: check.status, totalTimeMs: check.totalTimeMs || 0, startedAt: check.startedAt, finishedAt: check.finishedAt || check.startedAt, redirectCount: check.hops?.length - 1 || 0, loopDetected: this.detectLoop(check.hops || []), error: undefined, hops: (check.hops || []).map((hop) => ({ hopIndex: hop.hopIndex, url: hop.url, statusCode: hop.statusCode || undefined, redirectType: hop.redirectType, latencyMs: hop.latencyMs || undefined, contentType: hop.contentType || undefined, reason: hop.reason || undefined, })) }, analysis: { ssl: (check.sslInspections || []).map((ssl) => ({ host: ssl.host, daysToExpiry: ssl.daysToExpiry || undefined, issuer: ssl.issuer || undefined, protocol: ssl.protocol || undefined, warningsJson: Array.isArray(ssl.warningsJson) ? ssl.warningsJson : [], })), seo: check.seoFlags ? { robotsTxtStatus: check.seoFlags.robotsTxtStatus, noindex: check.seoFlags.noindex, nofollow: check.seoFlags.nofollow, sitemapPresent: check.seoFlags.sitemapPresent, } : undefined, security: check.securityFlags ? { safeBrowsingStatus: check.securityFlags.safeBrowsingStatus, mixedContent: check.securityFlags.mixedContent, httpsToHttp: check.securityFlags.httpsToHttp, } : undefined, }, organization: { name: check.project?.organization?.name || 'Unknown', plan: check.project?.organization?.plan || 'free', }, user: check.project?.organization?.memberships?.[0]?.user ? { name: check.project.organization.memberships[0].user.name, email: check.project.organization.memberships[0].user.email, } : undefined, generatedAt: new Date(), }; return exportData; } detectLoop(hops) { const urls = new Set(); for (const hop of hops) { if (urls.has(hop.url)) { return true; } urls.add(hop.url); } return false; } async generateMarkdownReport(checkId, userId) { try { logger_1.logger.info(`Generating Markdown report for check: ${checkId}`); const data = await this.getExportData(checkId, userId); const template = await this.loadTemplate('markdown-report.hbs'); const compiledTemplate = handlebars_1.default.compile(template); const mermaidDiagram = this.generateMermaidDiagram(data.check.hops); const templateData = { ...data, mermaidDiagram }; const markdown = compiledTemplate(templateData); logger_1.logger.info(`Markdown report generated successfully for check: ${checkId}`); return markdown; } catch (error) { logger_1.logger.error(`Failed to generate Markdown report for check ${checkId}:`, error); throw new Error(`Failed to generate Markdown report: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async generatePdfReport(checkId, userId) { let browser = null; try { logger_1.logger.info(`Generating PDF report for check: ${checkId}`); const data = await this.getExportData(checkId, userId); const htmlTemplate = await this.loadTemplate('pdf-report.hbs'); const compiledTemplate = handlebars_1.default.compile(htmlTemplate); const mermaidDiagram = this.generateMermaidDiagram(data.check.hops); const templateData = { ...data, mermaidDiagram }; const html = compiledTemplate(templateData); browser = await puppeteer_1.default.launch({ headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--single-process', '--disable-gpu' ] }); const page = await browser.newPage(); await page.setContent(html, { waitUntil: 'networkidle0' }); await page.waitForFunction(() => document.querySelector('.mermaid svg') !== null, { timeout: 10000 }).catch(() => { logger_1.logger.warn('Mermaid diagram may not have rendered completely'); }); const pdf = await page.pdf({ format: 'A4', printBackground: true, margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' } }); await browser.close(); browser = null; logger_1.logger.info(`PDF report generated successfully for check: ${checkId}`); return pdf; } catch (error) { if (browser) { await browser.close().catch(() => { }); } logger_1.logger.error(`Failed to generate PDF report for check ${checkId}:`, error); throw new Error(`Failed to generate PDF report: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async loadTemplate(templateName) { const templatePath = path_1.default.join(__dirname, '../templates', templateName); try { return await promises_1.default.readFile(templatePath, 'utf-8'); } catch (error) { logger_1.logger.error(`Failed to load template: ${templateName}`, error); throw new Error(`Template not found: ${templateName}`); } } async saveReport(content, checkId, format) { try { const reportsDir = path_1.default.join(process.cwd(), 'reports'); await promises_1.default.mkdir(reportsDir, { recursive: true }); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const fileName = `redirect-report-${checkId}-${timestamp}.${format}`; const filePath = path_1.default.join(reportsDir, fileName); if (typeof content === 'string') { await promises_1.default.writeFile(filePath, content, 'utf-8'); } else { await promises_1.default.writeFile(filePath, content); } logger_1.logger.info(`Report saved: ${filePath}`); return filePath; } catch (error) { logger_1.logger.error(`Failed to save report:`, error); throw new Error(`Failed to save report: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async cleanupOldReports(maxAgeHours = 24) { try { const reportsDir = path_1.default.join(process.cwd(), 'reports'); const files = await promises_1.default.readdir(reportsDir); const cutoff = Date.now() - (maxAgeHours * 60 * 60 * 1000); for (const file of files) { const filePath = path_1.default.join(reportsDir, file); const stats = await promises_1.default.stat(filePath); if (stats.mtime.getTime() < cutoff) { await promises_1.default.unlink(filePath); logger_1.logger.info(`Cleaned up old report: ${file}`); } } } catch (error) { logger_1.logger.warn('Failed to cleanup old reports:', error); } } } exports.ExportService = ExportService; //# sourceMappingURL=export.service.js.map