- Enhanced /api/health endpoint
- Added Socket.IO status monitoring (engine status, client count, active sockets)
- Added memory usage metrics (heap used, heap total, RSS)
- Returns 503 (degraded) if either CouchDB or Socket.IO is down
- Restructured response with nested 'services' object for better clarity
- Added dedicated /api/health/socketio endpoint
- Provides detailed Socket.IO connection information
- Shows connected clients and active sockets count
- Lists active rooms (event_ and post_ rooms) with member counts
- Useful for debugging real-time connection issues
Benefits:
- Better observability for Kubernetes health probes
- Can monitor Socket.IO connection health separately from database
- Helps diagnose real-time feature issues
- Memory metrics useful for detecting leaks on resource-constrained Raspberry Pi nodes
Response Format:
GET /api/health
{
"status": "healthy",
"timestamp": "...",
"uptime": 123.45,
"services": {
"couchdb": "connected",
"socketIO": {
"status": "running",
"connectedClients": 5,
"activeSockets": 5
}
},
"memory": { ... }
}
GET /api/health/socketio
{
"status": "running",
"connectedClients": 5,
"activeSockets": 5,
"rooms": [
{ "name": "event_123", "members": 3 },
{ "name": "post_456", "members": 2 }
]
}
🤖 Generated with AI Assistant
Co-Authored-By: AI Assistant <noreply@ai-assistant.com>
295 lines
8.5 KiB
JavaScript
295 lines
8.5 KiB
JavaScript
require("dotenv").config();
|
|
const express = require("express");
|
|
const couchdbService = require("./services/couchdbService");
|
|
const cors = require("cors");
|
|
const http = require("http");
|
|
const socketio = require("socket.io");
|
|
const helmet = require("helmet");
|
|
const rateLimit = require("express-rate-limit");
|
|
const mongoSanitize = require("express-mongo-sanitize");
|
|
const xss = require("xss-clean");
|
|
const { errorHandler } = require("./middleware/errorHandler");
|
|
const socketAuth = require("./middleware/socketAuth");
|
|
const requestLogger = require("./middleware/requestLogger");
|
|
const logger = require("./utils/logger");
|
|
const { validateEnv, logEnvConfig } = require("./utils/validateEnv");
|
|
|
|
// Validate environment variables before starting server
|
|
try {
|
|
validateEnv();
|
|
logEnvConfig();
|
|
} catch (error) {
|
|
logger.error('Environment validation failed:');
|
|
logger.error(error.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const io = socketio(server, {
|
|
cors: {
|
|
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
|
methods: ["GET", "POST"],
|
|
credentials: true,
|
|
},
|
|
});
|
|
const port = process.env.PORT || 5000;
|
|
|
|
// Security Headers - Helmet
|
|
app.use(helmet());
|
|
|
|
// CORS Configuration
|
|
app.use(
|
|
cors({
|
|
origin: process.env.FRONTEND_URL || "http://localhost:3000",
|
|
credentials: true,
|
|
}),
|
|
);
|
|
|
|
// Body Parser
|
|
app.use(express.json());
|
|
|
|
// Data Sanitization against NoSQL injection
|
|
app.use(mongoSanitize());
|
|
|
|
// Data Sanitization against XSS
|
|
app.use(xss());
|
|
|
|
// Request Logging
|
|
app.use(requestLogger);
|
|
|
|
// Rate Limiting for Auth Routes (5 requests per 15 minutes)
|
|
const authLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 5, // 5 requests per windowMs
|
|
message: {
|
|
success: false,
|
|
error: "Too many authentication attempts, please try again later",
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
});
|
|
|
|
// General API Rate Limiting (100 requests per 15 minutes)
|
|
const apiLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 100, // 100 requests per windowMs
|
|
message: {
|
|
success: false,
|
|
error: "Too many requests, please try again later",
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
});
|
|
|
|
// Database Connection
|
|
// CouchDB (primary database)
|
|
// Skip initialization during testing
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
couchdbService.initialize()
|
|
.then(() => logger.info("CouchDB initialized successfully"))
|
|
.catch((err) => {
|
|
logger.error("CouchDB initialization failed", err);
|
|
process.exit(1); // Exit if CouchDB fails to initialize since it's the primary database
|
|
});
|
|
}
|
|
|
|
// Socket.IO Authentication Middleware
|
|
io.use(socketAuth);
|
|
|
|
// Socket.IO Setup with Authentication
|
|
io.on("connection", (socket) => {
|
|
logger.info(`Socket.IO client connected`, { userId: socket.user.id });
|
|
|
|
socket.on("joinEvent", (eventId) => {
|
|
socket.join(`event_${eventId}`);
|
|
logger.debug(`User joined event`, { userId: socket.user.id, eventId });
|
|
});
|
|
|
|
socket.on("joinPost", (postId) => {
|
|
socket.join(`post_${postId}`);
|
|
logger.debug(`User joined post`, { userId: socket.user.id, postId });
|
|
});
|
|
|
|
socket.on("eventUpdate", (data) => {
|
|
io.to(`event_${data.eventId}`).emit("update", data.message);
|
|
});
|
|
|
|
socket.on("disconnect", () => {
|
|
logger.info(`Socket.IO client disconnected`, { userId: socket.user.id });
|
|
});
|
|
});
|
|
|
|
// Make io available to routes
|
|
app.set("io", io);
|
|
|
|
// Routes
|
|
const authRoutes = require("./routes/auth");
|
|
const streetRoutes = require("./routes/streets");
|
|
const taskRoutes = require("./routes/tasks");
|
|
const postRoutes = require("./routes/posts");
|
|
const commentsRoutes = require("./routes/comments");
|
|
const eventRoutes = require("./routes/events");
|
|
const rewardRoutes = require("./routes/rewards");
|
|
const reportRoutes = require("./routes/reports");
|
|
const badgesRoutes = require("./routes/badges");
|
|
const aiRoutes = require("./routes/ai");
|
|
const paymentRoutes = require("./routes/payments");
|
|
const userRoutes = require("./routes/users");
|
|
|
|
// Apply rate limiters
|
|
app.use("/api/auth/register", authLimiter);
|
|
app.use("/api/auth/login", authLimiter);
|
|
app.use("/api", apiLimiter);
|
|
|
|
// Health check endpoint (for Kubernetes liveness/readiness probes)
|
|
app.get("/api/health", async (req, res) => {
|
|
try {
|
|
const couchdbStatus = await couchdbService.checkConnection();
|
|
|
|
// Check Socket.IO status
|
|
const socketIOStatus = {
|
|
engine: io.engine ? "running" : "stopped",
|
|
connectedClients: io.engine ? io.engine.clientsCount : 0,
|
|
// Get number of connected sockets
|
|
sockets: io.sockets ? io.sockets.sockets.size : 0
|
|
};
|
|
|
|
const isHealthy = couchdbStatus && io.engine;
|
|
|
|
res.status(isHealthy ? 200 : 503).json({
|
|
status: isHealthy ? "healthy" : "degraded",
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
services: {
|
|
couchdb: couchdbStatus ? "connected" : "disconnected",
|
|
socketIO: {
|
|
status: socketIOStatus.engine,
|
|
connectedClients: socketIOStatus.connectedClients,
|
|
activeSockets: socketIOStatus.sockets
|
|
}
|
|
},
|
|
memory: {
|
|
heapUsed: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + " MB",
|
|
heapTotal: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) + " MB",
|
|
rss: Math.round(process.memoryUsage().rss / 1024 / 1024) + " MB"
|
|
}
|
|
});
|
|
} catch (error) {
|
|
res.status(503).json({
|
|
status: "unhealthy",
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
services: {
|
|
couchdb: "disconnected",
|
|
socketIO: "unknown"
|
|
},
|
|
error: error.message,
|
|
});
|
|
}
|
|
});
|
|
|
|
// Detailed Socket.IO health check endpoint
|
|
app.get("/api/health/socketio", (req, res) => {
|
|
try {
|
|
const socketIOInfo = {
|
|
status: io.engine ? "running" : "stopped",
|
|
connectedClients: io.engine ? io.engine.clientsCount : 0,
|
|
activeSockets: io.sockets ? io.sockets.sockets.size : 0,
|
|
rooms: [],
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
// Get list of active rooms (excluding auto-generated socket ID rooms)
|
|
if (io.sockets && io.sockets.adapter && io.sockets.adapter.rooms) {
|
|
const rooms = Array.from(io.sockets.adapter.rooms.keys()).filter(room => {
|
|
// Filter out socket ID rooms (they start with socket ID pattern)
|
|
return room.startsWith('event_') || room.startsWith('post_');
|
|
});
|
|
|
|
socketIOInfo.rooms = rooms.map(room => {
|
|
const roomSize = io.sockets.adapter.rooms.get(room)?.size || 0;
|
|
return { name: room, members: roomSize };
|
|
});
|
|
}
|
|
|
|
res.status(200).json(socketIOInfo);
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
status: "error",
|
|
error: error.message,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
});
|
|
|
|
// Routes
|
|
app.use("/api/auth", authRoutes);
|
|
app.use("/api/streets", streetRoutes);
|
|
app.use("/api/tasks", taskRoutes);
|
|
app.use("/api/posts", postRoutes);
|
|
app.use("/api/posts", commentsRoutes); // Comments are nested under posts
|
|
app.use("/api/events", eventRoutes);
|
|
app.use("/api/rewards", rewardRoutes);
|
|
app.use("/api/reports", reportRoutes);
|
|
app.use("/api/badges", badgesRoutes);
|
|
app.use("/api/ai", aiRoutes);
|
|
app.use("/api/payments", paymentRoutes);
|
|
app.use("/api/users", userRoutes);
|
|
|
|
app.get("/", (req, res) => {
|
|
res.send("Street Adoption App Backend");
|
|
});
|
|
|
|
// Error Handler Middleware (must be last)
|
|
app.use(errorHandler);
|
|
|
|
// Only start server if this file is run directly (not when required by tests)
|
|
if (require.main === module) {
|
|
server.listen(port, () => {
|
|
logger.info(`Server started`, { port, env: process.env.NODE_ENV || 'development' });
|
|
});
|
|
}
|
|
|
|
// Export app and server for testing
|
|
module.exports = { app, server, io };
|
|
|
|
// Graceful shutdown
|
|
process.on("SIGTERM", async () => {
|
|
logger.info("SIGTERM received, shutting down gracefully");
|
|
|
|
try {
|
|
// Close CouchDB connection
|
|
await couchdbService.shutdown();
|
|
logger.info("CouchDB connection closed");
|
|
|
|
// Close server
|
|
server.close(() => {
|
|
logger.info("Server closed");
|
|
process.exit(0);
|
|
});
|
|
} catch (error) {
|
|
logger.error("Error during shutdown", error);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
process.on("SIGINT", async () => {
|
|
logger.info("SIGINT received, shutting down gracefully");
|
|
|
|
try {
|
|
// Close CouchDB connection
|
|
await couchdbService.shutdown();
|
|
logger.info("CouchDB connection closed");
|
|
|
|
// Close server
|
|
server.close(() => {
|
|
logger.info("Server closed");
|
|
process.exit(0);
|
|
});
|
|
} catch (error) {
|
|
logger.error("Error during shutdown", error);
|
|
process.exit(1);
|
|
}
|
|
});
|