const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { initPool, renderHtml, POOL_SIZE } = require('./browserPool'); const v2Routes = require('./v2Routes'); const { cleanupExpiredSessions, cleanupExpiredSnapshots } = require('./db'); const app = express(); const PORT = 3000; const CACHE_DIR = path.join(__dirname, 'cache'); // Create cache directory if it doesn't exist if (!fs.existsSync(CACHE_DIR)) { fs.mkdirSync(CACHE_DIR); } app.use(express.json({ limit: '1gb' })); app.use(express.urlencoded({ limit: '1gb', extended: true })); // enable CORS for all routes app.use(cors({ origin: '*', methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'User-Agent'], credentials: false })); // user-agent check middleware (only for v2 routes) app.use((req, res, next) => { // only check user-agent for v2 routes if (!req.url.startsWith('/v2')) { return next(); } const userAgent = req.get('User-Agent') || ''; // allow flutter app or web browsers const isFlutterApp = userAgent.includes('QuoteGen-Flutter/1.0'); const isBrowser = userAgent.includes('Mozilla') || userAgent.includes('Chrome') || userAgent.includes('Safari') || userAgent.includes('Firefox') || userAgent.includes('Edge'); if (!isFlutterApp && !isBrowser) { return res.status(403).json({ error: 'Forbidden: Invalid user agent' }); } next(); }); // Request logging middleware app.use((req, res, next) => { // skip logging health checks if (req.url === '/health') { return next(); } const timestamp = new Date().toISOString(); console.log(`\n[${timestamp}] ${req.method} ${req.url}`); if ((req.method === 'POST' || req.method === 'PATCH') && req.body) { const logBody = { ...req.body }; // Truncate long base64 strings for readability if (logBody.avatarUrl && logBody.avatarUrl.startsWith('data:')) { logBody.avatarUrl = logBody.avatarUrl.substring(0, 50) + '... (base64 truncated)'; } if (logBody.imageUrl && logBody.imageUrl.startsWith('data:')) { logBody.imageUrl = logBody.imageUrl.substring(0, 50) + '... (base64 truncated)'; } console.log('Body:', JSON.stringify(logBody, null, 2)); } if (req.method === 'GET' && Object.keys(req.query).length > 0) { const logQuery = { ...req.query }; // Truncate long base64 strings for readability if (logQuery.avatarUrl && logQuery.avatarUrl.startsWith('data:')) { logQuery.avatarUrl = logQuery.avatarUrl.substring(0, 50) + '... (base64 truncated)'; } if (logQuery.imageUrl && logQuery.imageUrl.startsWith('data:')) { logQuery.imageUrl = logQuery.imageUrl.substring(0, 50) + '... (base64 truncated)'; } console.log('Query:', JSON.stringify(logQuery, null, 2)); } next(); }); // mount v2 api app.use('/v2', v2Routes); function normalizeConfig(config) { // Remove null, undefined, and empty string values const normalized = {}; for (const [key, value] of Object.entries(config)) { if (value !== null && value !== undefined && value !== '') { normalized[key] = value; } } return normalized; } function hashConfig(config) { const normalized = normalizeConfig(config); const configString = JSON.stringify(normalized); return crypto.createHash('sha256').update(configString).digest('hex'); } function getCachePath(hash) { return path.join(CACHE_DIR, `${hash}.png`); } function getCachedImage(config) { const hash = hashConfig(config); const cachePath = getCachePath(hash); if (fs.existsSync(cachePath)) { // Update file modification time to mark as recently accessed const now = new Date(); fs.utimesSync(cachePath, now, now); return fs.readFileSync(cachePath); } return null; } function cacheImage(config, imageBuffer) { const hash = hashConfig(config); const cachePath = getCachePath(hash); fs.writeFileSync(cachePath, imageBuffer); } function clearCache() { if (!fs.existsSync(CACHE_DIR)) { return; } const files = fs.readdirSync(CACHE_DIR); files.forEach(file => { const filePath = path.join(CACHE_DIR, file); fs.unlinkSync(filePath); }); console.log(`Cleared ${files.length} cached image(s)`); } function cleanupOldCache() { const ONE_DAY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds const now = Date.now(); if (!fs.existsSync(CACHE_DIR)) { return; } const files = fs.readdirSync(CACHE_DIR); let deletedCount = 0; files.forEach(file => { const filePath = path.join(CACHE_DIR, file); const stats = fs.statSync(filePath); // Check if file was last modified more than 24 hours ago if (now - stats.mtime.getTime() > ONE_DAY) { fs.unlinkSync(filePath); deletedCount++; } }); if (deletedCount > 0) { console.log(`Cleaned up ${deletedCount} cached image(s)`); } } function formatCount(num) { if (num === null || num === undefined) return '0'; num = parseInt(num); if (num >= 1000000000) { return (num / 1000000000).toFixed(1).replace(/\.0$/, '') + 'B'; } if (num >= 1000000) { return (num / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; } if (num >= 1000) { return (num / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; } return num.toString(); } function formatTimestamp(epoch) { const date = new Date(epoch * 1000); const hours = date.getHours(); const minutes = date.getMinutes().toString().padStart(2, '0'); const ampm = hours >= 12 ? 'PM' : 'AM'; const hour12 = hours % 12 || 12; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const month = months[date.getMonth()]; const day = date.getDate(); const year = date.getFullYear(); return `${hour12}:${minutes} ${ampm} ยท ${month} ${day}, ${year}`; } const TEMPLATE_PATH = path.join(__dirname, 'template.html'); const templateHtml = fs.readFileSync(TEMPLATE_PATH, 'utf8'); function buildEngagementHtml(engagement) { if (!engagement) return ''; return `
`; } async function generateQuoteBuffer(config) { const avatarHtml = config.avatarUrl ? `