Files
url_tracker_tool/apps/api/dist/services/export.service.js
Andrei 58f8093689 Rebrand from 'Redirect Intelligence v2' to 'URL Tracker Tool V2' throughout UI
- Updated all component headers and documentation
- Changed navbar and footer branding
- Updated homepage hero badge
- Modified page title in index.html
- Simplified footer text to 'Built with ❤️'
- Consistent V2 capitalization across all references
2025-08-19 19:12:23 +00:00

336 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 });
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