Files
module4_backend_project/server.js
T

487 lines
14 KiB
JavaScript
Raw 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');
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());
app.use(express.static('public'));
// 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();
});
// 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
});
});
// Health check endpoint
app.get('/health', async (req, res) => {
const redisHealthy = redisClient.isHealthy();
const fallbackActive = fallbackStore.isActive;
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', 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', 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 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]
});
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}`
}
};
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'
}
};
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;
}
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');
}
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');
}
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}`
}
});
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'
}
});
}
} 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;
}
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);
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', async (req, res) => {
2025-02-11 14:52:32 +01:00
try {
const purchaseId = req.params.purchaseId;
if (!pdfGenerator.ticketExists(purchaseId)) {
return res.status(404).json({
success: false,
message: 'Ticket not found'
2025-02-11 14:52:32 +01:00
});
}
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', 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', 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'
});
}
});
// 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');
// 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');
});
}
}
// 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();