Files

731 lines
21 KiB
JavaScript
Raw Permalink Normal View History

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");
2025-02-11 14:52:32 +01:00
const app = express();
2025-02-11 15:06:41 +01:00
const port = process.env.PORT || 3049;
2025-02-11 14:52:32 +01:00
// Middleware
2025-02-11 14:52:32 +01:00
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",
});
}
});
2025-02-11 14:52:32 +01:00
// 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,
});
}
}
}
);
2025-02-11 14:52:32 +01:00
// 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();
2025-02-11 14:52:32 +01:00
} else {
globalStats = fallbackStore.getGlobalStats();
events = fallbackStore.getAllEvents();
2025-02-11 14:52:32 +01:00
}
// 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);
2025-02-11 14:52:32 +01:00
} catch (error) {
logger.error("Error generating metrics:", error);
res.status(500).json({
success: false,
message: "Failed to generate metrics",
});
2025-02-11 14:52:32 +01:00
}
});
// 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);
2025-02-11 14:52:32 +01:00
});
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();