const express = require("express"); const { v4: uuidv4 } = require("uuid"); require("dotenv").config(); // Import utilities const redisClient = require("./src/utils/redis-client"); const fallbackStore = require("./src/utils/fallback-store"); const logger = require("./src/utils/logger"); const pdfGenerator = require("./src/utils/pdf-generator"); const metrics = require("./src/utils/metrics"); const security = require("./src/utils/security"); const app = express(); const port = process.env.PORT || 3049; // Middleware app.use(express.json()); // Security middleware app.use(security.securityHeaders); app.use(security.requestSizeLimit); app.use(security.securityLogging); // Request logging middleware app.use((req, res, next) => { const start = Date.now(); res.on("finish", () => { const responseTime = Date.now() - start; logger.logRequest(req, res, responseTime); }); next(); }); // Prometheus metrics middleware app.use(metrics.metricsMiddleware); // Apply general rate limiting to all routes app.use(security.generalLimiter); // Health check endpoint app.get("/health", async (req, res) => { const redisHealthy = redisClient.isHealthy(); const fallbackActive = fallbackStore.isActive; // Update metrics metrics.updateRedisStatus(redisHealthy); metrics.updateFallbackStatus(fallbackActive); res.json({ status: "ok", timestamp: new Date().toISOString(), redis: { connected: redisHealthy, fallbackActive: fallbackActive, }, uptime: process.uptime(), }); }); // Get all events endpoint app.get("/events", async (req, res) => { try { let events; if (redisClient.isHealthy()) { events = await redisClient.getAllEvents(); } else { events = fallbackStore.getAllEvents(); } res.json({ success: true, events, usingFallback: fallbackStore.isActive, }); } catch (error) { logger.error("Error fetching events:", error); res.status(500).json({ success: false, message: "Failed to fetch events", }); } }); // Get specific event stats app.get("/events/:eventId", security.validateEventId, async (req, res) => { try { const eventId = req.params.eventId; let eventStats; if (redisClient.isHealthy()) { eventStats = await redisClient.getEventStats(eventId); } else { eventStats = fallbackStore.getEventStats(eventId); } if (!eventStats) { return res.status(404).json({ success: false, message: "Event not found", }); } res.json({ success: true, event: eventStats, usingFallback: fallbackStore.isActive, }); } catch (error) { logger.error("Error fetching event stats:", error); res.status(500).json({ success: false, message: "Failed to fetch event stats", }); } }); // Purchase ticket endpoint (multi-event) app.post( "/buy/:eventId", security.purchaseLimiter, security.validateEventId, async (req, res) => { const startTime = Date.now(); const eventId = req.params.eventId; const purchaseId = uuidv4(); const timestamp = new Date().toISOString(); try { let result; // Try Redis first if (redisClient.isHealthy()) { try { const luaResult = await redisClient.purchaseTicket( eventId, purchaseId, timestamp ); if (luaResult[0]) { // Success - generate PDF ticket try { // Get event details for PDF const eventStats = await redisClient.getEventStats(eventId); const pdfStartTime = Date.now(); const pdfResult = await pdfGenerator.generateTicketPDF({ ticketId: luaResult[0], eventId, purchaseId, eventName: eventStats?.name || `Event ${eventId}`, eventDescription: eventStats?.description || "Event description not available", timestamp, soldCount: luaResult[2], }); // Record PDF generation metrics const pdfDuration = (Date.now() - pdfStartTime) / 1000; metrics.recordPDFGeneration( pdfResult.success ? "success" : "failed", pdfDuration ); result = { success: true, ticket: luaResult[0], purchaseId, eventId, soldCount: luaResult[2], message: "Ticket purchased successfully!", usingFallback: false, pdf: { generated: pdfResult.success, filename: pdfResult.filename, downloadUrl: `/tickets/${purchaseId}`, }, }; // Record metrics for successful purchase metrics.recordTicketSale(eventId, "success"); metrics.updateTicketMetrics( eventId, luaResult[2], luaResult[3] || 0 ); logger.logPurchase(eventId, luaResult[0], purchaseId, true); logger.info(`PDF ticket generated for purchase ${purchaseId}`); } catch (pdfError) { logger.error("PDF generation failed:", pdfError); // Still return success for ticket purchase, but note PDF failure result = { success: true, ticket: luaResult[0], purchaseId, eventId, soldCount: luaResult[2], message: "Ticket purchased successfully! (PDF generation failed)", usingFallback: false, pdf: { generated: false, error: "PDF generation failed", }, }; // Record metrics for successful purchase (even with PDF failure) metrics.recordTicketSale(eventId, "success"); metrics.updateTicketMetrics( eventId, luaResult[2], luaResult[3] || 0 ); logger.logPurchase(eventId, luaResult[0], purchaseId, true); } } else { // Failed - handle specific error const errorCode = luaResult[1]; let statusCode = 400; let message = "Purchase failed"; switch (errorCode) { case "EVENT_NOT_FOUND": statusCode = 404; message = "Event not found"; break; case "NO_TICKETS_AVAILABLE": statusCode = 409; message = "No tickets available for this event"; break; } // Record metrics for failed purchase metrics.recordTicketSale(eventId, "failed"); logger.logPurchase(eventId, null, purchaseId, false, errorCode); return res.status(statusCode).json({ success: false, message, errorCode, eventId, purchaseId, }); } } catch (redisError) { logger.error( "Redis purchase failed, attempting fallback:", redisError ); // Activate fallback if not already active if (!fallbackStore.isActive) { fallbackStore.activate("Redis purchase operation failed"); // Try to sync with Redis data if possible setTimeout(() => { if (fallbackStore.isActive && fallbackStore.events.size === 0) { fallbackStore.attemptReseed(); } }, 100); } throw redisError; // Will be caught by outer try-catch for fallback } } else { throw new Error("Redis not available"); } const responseTime = Date.now() - startTime; result.responseTime = `${responseTime}ms`; res.json(result); } catch (error) { // Fallback to in-memory store try { if (!fallbackStore.isActive) { fallbackStore.activate("Redis connection failed during purchase"); // Try to sync with Redis data if possible setTimeout(() => { if (fallbackStore.isActive && fallbackStore.events.size === 0) { fallbackStore.attemptReseed(); } }, 100); } const fallbackResult = fallbackStore.purchaseTicket( eventId, purchaseId ); if (fallbackResult.success) { // Generate PDF for fallback purchase try { const eventStats = fallbackStore.getEventStats(eventId); const pdfResult = await pdfGenerator.generateTicketPDF({ ticketId: fallbackResult.ticket, eventId, purchaseId, eventName: eventStats?.name || `Event ${eventId}`, eventDescription: eventStats?.description || "Event description not available", timestamp, soldCount: fallbackResult.soldCount, }); const responseTime = Date.now() - startTime; res.json({ success: true, ticket: fallbackResult.ticket, purchaseId, eventId, soldCount: fallbackResult.soldCount, message: "Ticket purchased successfully (fallback mode)!", usingFallback: true, responseTime: `${responseTime}ms`, pdf: { generated: pdfResult.success, filename: pdfResult.filename, downloadUrl: `/tickets/${purchaseId}`, }, }); // Record metrics for successful fallback purchase metrics.recordTicketSale(eventId, "success_fallback"); metrics.updateTicketMetrics( eventId, fallbackResult.soldCount, fallbackResult.remainingTickets || 0 ); logger.info( `PDF ticket generated for fallback purchase ${purchaseId}` ); } catch (pdfError) { logger.error("PDF generation failed in fallback mode:", pdfError); const responseTime = Date.now() - startTime; res.json({ success: true, ticket: fallbackResult.ticket, purchaseId, eventId, soldCount: fallbackResult.soldCount, message: "Ticket purchased successfully (fallback mode, PDF generation failed)!", usingFallback: true, responseTime: `${responseTime}ms`, pdf: { generated: false, error: "PDF generation failed", }, }); // Record metrics for successful fallback purchase (even with PDF failure) metrics.recordTicketSale(eventId, "success_fallback"); metrics.updateTicketMetrics( eventId, fallbackResult.soldCount, fallbackResult.remainingTickets || 0 ); } } else { let statusCode = 400; let message = "Purchase failed"; switch (fallbackResult.error) { case "EVENT_NOT_FOUND": statusCode = 404; message = "Event not found"; break; case "NO_TICKETS_AVAILABLE": statusCode = 409; message = "No tickets available for this event"; break; } // Record metrics for failed fallback purchase metrics.recordTicketSale(eventId, "failed_fallback"); logger.logPurchase( eventId, null, purchaseId, false, fallbackResult.error ); res.status(statusCode).json({ success: false, message, errorCode: fallbackResult.error, eventId, purchaseId, usingFallback: true, }); } } catch (fallbackError) { logger.error("Both Redis and fallback failed:", fallbackError); // Record metrics for system failure metrics.recordTicketSale(eventId, "system_error"); logger.logPurchase(eventId, null, purchaseId, false, fallbackError); res.status(500).json({ success: false, message: "System temporarily unavailable", eventId, purchaseId, }); } } } ); // Download ticket PDF endpoint app.get( "/tickets/:purchaseId", security.validatePurchaseId, async (req, res) => { try { const purchaseId = req.params.purchaseId; if (!pdfGenerator.ticketExists(purchaseId)) { return res.status(404).json({ success: false, message: "Ticket not found", }); } const filepath = pdfGenerator.getTicketPath(purchaseId); const filename = `ticket-${purchaseId}.pdf`; res.setHeader("Content-Type", "application/pdf"); res.setHeader( "Content-Disposition", `attachment; filename="${filename}"` ); const fileStream = require("fs").createReadStream(filepath); fileStream.pipe(res); logger.info(`PDF ticket downloaded: ${purchaseId}`); } catch (error) { logger.error("Error downloading ticket:", error); res.status(500).json({ success: false, message: "Failed to download ticket", }); } } ); // PDF management endpoint app.get("/admin/pdf-stats", security.adminLimiter, async (req, res) => { try { const stats = pdfGenerator.getStats(); res.json({ success: true, stats, }); } catch (error) { logger.error("Error getting PDF stats:", error); res.status(500).json({ success: false, message: "Failed to get PDF statistics", }); } }); // Cleanup old tickets endpoint app.post( "/admin/cleanup-tickets", security.adminLimiter, security.validateCleanupRequest, async (req, res) => { try { const maxAgeHours = req.body.maxAgeHours || 24; const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours); res.json({ success: true, message: `Cleaned up ${deletedCount} old tickets`, deletedCount, }); } catch (error) { logger.error("Error cleaning up tickets:", error); res.status(500).json({ success: false, message: "Failed to cleanup tickets", }); } } ); // Seed fallback store endpoint app.post("/admin/seed-fallback", security.adminLimiter, async (req, res) => { try { if (redisClient.isHealthy()) { // Activate fallback store temporarily for seeding fallbackStore.activate("Manual seeding from admin endpoint"); // Get all events from Redis and seed fallback store const events = await redisClient.getAllEvents(); const globalStats = await redisClient.getGlobalStats(); for (const event of events) { // Get remaining tickets for this event const remainingTickets = await redisClient.getRemainingTickets( event.eventId ); // Create metadata object const metadata = { eventId: event.eventId, totalTickets: event.totalTickets, soldTickets: event.soldTickets, createdAt: event.createdAt, name: event.name, description: event.description, lastSoldAt: event.lastSoldAt, }; // Seed the event in fallback store fallbackStore.seedEvent(event.eventId, remainingTickets, metadata); // Update sold tickets count const fallbackEvent = fallbackStore.events.get(event.eventId); if (fallbackEvent) { fallbackEvent.soldTickets = event.soldTickets; } } // Update global stats if (globalStats) { fallbackStore.globalStats.totalSold = globalStats.totalSold; fallbackStore.globalStats.lastSeeded = new Date().toISOString(); } // Deactivate fallback store (will be activated when needed) fallbackStore.deactivate(); res.json({ success: true, message: `Fallback store seeded with ${events.length} events`, eventsCount: events.length, totalTickets: globalStats?.totalTickets || 0, totalSold: globalStats?.totalSold || 0, }); } else { res.status(503).json({ success: false, message: "Redis not available - cannot seed fallback store", }); } } catch (error) { logger.error("Error seeding fallback store:", error); res.status(500).json({ success: false, message: "Failed to seed fallback store", }); } }); // Metrics endpoint (Prometheus compatible) app.get("/metrics", async (req, res) => { try { let globalStats, events; if (redisClient.isHealthy()) { globalStats = await redisClient.getGlobalStats(); events = await redisClient.getAllEvents(); } else { globalStats = fallbackStore.getGlobalStats(); events = fallbackStore.getAllEvents(); } // Get PDF stats const pdfStats = pdfGenerator.getStats(); // Calculate metrics const metrics = { timestamp: new Date().toISOString(), global: globalStats, events: events, system: { usingFallback: fallbackStore.isActive, redisConnected: redisClient.isHealthy(), uptime: process.uptime(), memoryUsage: process.memoryUsage(), }, pdf: pdfStats, }; res.json(metrics); } catch (error) { logger.error("Error generating metrics:", error); res.status(500).json({ success: false, message: "Failed to generate metrics", }); } }); // Initialize server async function initializeServer() { try { // Connect to Redis await redisClient.connect(); logger.info("Redis connected successfully"); // Ensure fallback store is seeded with current Redis data await ensureFallbackStoreSeeded(); // Start server app.listen(port, () => { logger.info(`🚀 Ticket Microservice running on port ${port}`); logger.info(`📊 Health check: http://localhost:${port}/health`); logger.info(`📈 Metrics: http://localhost:${port}/metrics`); logger.info(`🎫 Events: http://localhost:${port}/events`); }); } catch (error) { logger.error("Failed to initialize server:", error); logger.warn("Starting server with fallback store only"); fallbackStore.activate("Redis connection failed at startup"); app.listen(port, () => { logger.warn(`⚠️ Server running in FALLBACK MODE on port ${port}`); logger.warn("Redis connection failed - using in-memory store"); }); } } // Ensure fallback store is seeded with current Redis data async function ensureFallbackStoreSeeded() { try { // Check if fallback store needs seeding if (fallbackStore.events.size === 0) { logger.info( "Fallback store is empty, seeding with current Redis data..." ); // Temporarily activate fallback store for seeding fallbackStore.activate("Seeding during server initialization"); // Get all events from Redis and seed fallback store const events = await redisClient.getAllEvents(); const globalStats = await redisClient.getGlobalStats(); for (const event of events) { // Get remaining tickets for this event const remainingTickets = await redisClient.getRemainingTickets( event.eventId ); // Create metadata object const metadata = { eventId: event.eventId, totalTickets: event.totalTickets, soldTickets: event.soldTickets, createdAt: event.createdAt, name: event.name, description: event.description, lastSoldAt: event.lastSoldAt, }; // Seed the event in fallback store fallbackStore.seedEvent(event.eventId, remainingTickets, metadata); // Update sold tickets count const fallbackEvent = fallbackStore.events.get(event.eventId); if (fallbackEvent) { fallbackEvent.soldTickets = event.soldTickets; } } // Update global stats if (globalStats) { fallbackStore.globalStats.totalSold = globalStats.totalSold; fallbackStore.globalStats.lastSeeded = new Date().toISOString(); } logger.info(`Fallback store seeded with ${events.length} events`); // Deactivate fallback store (will be activated when needed) fallbackStore.deactivate(); } } catch (error) { logger.error("Error seeding fallback store during initialization:", error); // Don't fail server startup if fallback seeding fails } } // Global error handler app.use((err, req, res, next) => { logger.error("Unhandled error:", err); res.status(500).json({ success: false, message: "Internal Server Error", error: process.env.NODE_ENV === "development" ? err.message : undefined, }); }); // Graceful shutdown process.on("SIGINT", async () => { logger.info("Received SIGINT, shutting down gracefully..."); try { await redisClient.disconnect(); logger.info("Redis disconnected"); } catch (error) { logger.error("Error disconnecting Redis:", error); } process.exit(0); }); process.on("SIGTERM", async () => { logger.info("Received SIGTERM, shutting down gracefully..."); try { await redisClient.disconnect(); logger.info("Redis disconnected"); } catch (error) { logger.error("Error disconnecting Redis:", error); } process.exit(0); }); // Start the server initializeServer();