Skip to content

Commit 43f7c06

Browse files
committed
refactor: Clean up middleware and error handling, improve leaderboard data fetching and caching logic
1 parent efcc924 commit 43f7c06

File tree

9 files changed

+183
-221
lines changed

9 files changed

+183
-221
lines changed

server/app.js

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import authRoutes from "./routes/authRoutes.js";
44
import profileRoutes from "./routes/profileRoutes.js";
55
import challengeRoutes from "./routes/challengeRoutes.js";
66
import leaderboardRoutes from "./routes/leaderboardRoutes.js";
7-
import { warmupLeaderboardCache } from "./utils/leaderBoardCache.js";
87
import passport from "passport";
98
import cookieParser from "cookie-parser";
109
import dotenv from "dotenv";
@@ -17,47 +16,19 @@ import typeSenseRoutes from "./routes/typeSenseRoutes.js";
1716
import { startStreakCronJob } from './utils/streakResetJob.js';
1817
import { appLifecycle } from './utils/appLifecycle.js';
1918
import { globalErrorHandler } from './middleware/errorHandler.js';
20-
import auditService from './services/auditService.js';
21-
import { httpLogger, auditContextMiddleware, auditErrorMiddleware, performanceMonitor } from './middleware/auditMiddleware.js';
2219
dotenv.config();
2320

2421
const app = express();
2522

2623
app.use(cors({ origin: process.env.CLIENT_URL, credentials: true }));
2724

28-
// Memory-safe payload size limits with proper error handling
29-
// Audit context middleware (must be early in middleware chain)
30-
app.use(auditContextMiddleware);
25+
app.use(express.json({ limit: '2mb' }));
3126

32-
app.use(express.json({
33-
limit: '2mb', // Reduced from 5mb to prevent memory issues
34-
verify: (req, res, buf) => {
35-
// Monitor large requests with audit service
36-
if (buf.length > 1024 * 1024) { // 1MB threshold
37-
auditService.security('large_request_detected', {
38-
requestId: req.auditContext?.requestId,
39-
size: buf.length,
40-
ip: req.ip,
41-
url: req.originalUrl,
42-
userAgent: req.get('User-Agent')
43-
});
44-
}
45-
}
46-
}));
47-
48-
app.use(express.urlencoded({
49-
limit: '2mb',
50-
extended: false,
51-
parameterLimit: 1000 // Limit number of parameters
52-
}));
27+
app.use(express.urlencoded({ limit: '2mb', extended: false }));
5328

5429
app.use(cookieParser());
5530
app.use(passport.initialize());
5631

57-
// Replace Morgan with our audit-integrated HTTP logger
58-
app.use(httpLogger);
59-
app.use(performanceMonitor);
60-
6132

6233
app.use('/api/auth', authRoutes);
6334
app.use('/api/profile', profileRoutes);
@@ -70,24 +41,11 @@ app.use('/api', statsRoutes);
7041
app.use('/api/user', userRoutes);
7142
app.use('/api', typeSenseRoutes);
7243

73-
// Error handling middleware
74-
app.use(auditErrorMiddleware);
75-
app.use(globalErrorHandler);
76-
77-
// Initialize application services with audit logging
78-
auditService.systemEvent('app_initializing', {
79-
environment: process.env.NODE_ENV,
80-
version: process.env.npm_package_version
81-
});
82-
83-
warmupLeaderboardCache();
8444
startStreakCronJob();
8545
appLifecycle.initialize();
8646

8747
// Export cleanup function for server.js
8848
export const cleanup = async () => {
89-
auditService.systemEvent('app_shutting_down');
90-
await auditService.shutdown();
9149
return appLifecycle.cleanup();
9250
};
9351

server/controllers/leaderboardController.js

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,29 +57,54 @@ export const updateUserPoints = async (req, res) => {
5757

5858
export const getLeaderboardData = async (req, res) => {
5959
try {
60-
const { search = "", page, limit } = req.query;
60+
const { search = "", page = 1, limit = 10 } = req.query;
6161

62-
let leaderboard = getCachedLeaderboard();
62+
let leaderboard = await getCachedLeaderboard();
6363
if (!leaderboard) {
6464
leaderboard = await updateRanks();
6565
}
6666

67+
// Ensure leaderboard is an array
68+
if (!Array.isArray(leaderboard)) {
69+
console.error("Leaderboard is not an array:", typeof leaderboard, leaderboard);
70+
leaderboard = [];
71+
}
72+
73+
// Handle case where leaderboard is empty
74+
if (leaderboard.length === 0) {
75+
return res.json({
76+
users: [],
77+
totalUsers: 0,
78+
currentPage: 1,
79+
totalPages: 1,
80+
});
81+
}
82+
6783
// Filter based on search query, if any
6884
let filteredLeaderboard = leaderboard;
6985
if (search.trim() !== "") {
7086
const searchLower = search.toLowerCase();
7187
filteredLeaderboard = leaderboard.filter(user =>
72-
user.username.toLowerCase().includes(searchLower)
88+
user.username && user.username.toLowerCase().includes(searchLower)
7389
);
7490
}
7591

76-
// Pagination logic
92+
// Ensure filtered result is also an array
93+
if (!Array.isArray(filteredLeaderboard)) {
94+
console.error("Filtered leaderboard is not an array:", typeof filteredLeaderboard);
95+
filteredLeaderboard = [];
96+
}
97+
98+
// Pagination logic with proper defaults
99+
const pageNum = Math.max(parseInt(page) || 1, 1);
100+
const limitNum = Math.min(Math.max(parseInt(limit) || 10, 1), 100); // Max 100 per page
101+
77102
const totalUsers = filteredLeaderboard.length;
78-
const totalPages = Math.ceil(totalUsers / limit);
79-
const currentPage = Math.min(Math.max(parseInt(page), 1), totalPages);
103+
const totalPages = Math.max(Math.ceil(totalUsers / limitNum), 1);
104+
const currentPage = Math.min(pageNum, totalPages);
80105

81-
const startIndex = (currentPage - 1) * limit;
82-
const pagedUsers = filteredLeaderboard.slice(startIndex, startIndex + parseInt(limit));
106+
const startIndex = (currentPage - 1) * limitNum;
107+
const pagedUsers = filteredLeaderboard.slice(startIndex, startIndex + limitNum);
83108

84109
res.json({
85110
users: pagedUsers,
@@ -88,7 +113,7 @@ export const getLeaderboardData = async (req, res) => {
88113
totalPages,
89114
});
90115
} catch (error) {
91-
// console.error("Error fetching leaderboard:", error);
116+
console.error("Error fetching leaderboard:", error);
92117
res.status(500).json({ error: "Internal server error" });
93118
}
94119
};

server/lib/db.js

Lines changed: 17 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,34 @@
11
import mongoose from "mongoose"
2-
import { setupDatabase } from "./dbIndexes.js"
3-
import auditService from "../services/auditService.js"
4-
// import { setupMongoChangeStream } from "../typesense/liveSync.js"
52

63
const connectDB = async () => {
74
try {
8-
// Connection options with resource limits
95
const connectionOptions = {
10-
maxPoolSize: 10, // Limit connection pool size
11-
serverSelectionTimeoutMS: 5000, // How long to try selecting a server
12-
socketTimeoutMS: 45000, // How long to wait for a response
13-
bufferMaxEntries: 0, // Disable mongoose buffering
14-
bufferCommands: false, // Disable mongoose buffering
6+
maxPoolSize: 10,
7+
serverSelectionTimeoutMS: 30000,
8+
socketTimeoutMS: 45000,
9+
bufferCommands: false,
1510
};
1611

1712
const conn = await mongoose.connect(process.env.MONGO_URI, connectionOptions);
13+
console.log(`MongoDB Connected: ${conn.connection.host}`);
1814

19-
auditService.systemEvent('database_connected', {
20-
host: conn.connection.host,
21-
database: conn.connection.name,
22-
connectionPoolSize: connectionOptions.maxPoolSize
23-
});
24-
25-
// Monitor connection events for memory management
26-
mongoose.connection.on('connected', () => {
27-
auditService.systemEvent('database_connection_established', {
28-
readyState: mongoose.connection.readyState
29-
});
30-
});
31-
32-
mongoose.connection.on('disconnected', () => {
33-
auditService.systemEvent('database_disconnected', {
34-
reason: 'connection_lost'
35-
});
36-
});
37-
15+
// Simple error handling
3816
mongoose.connection.on('error', (error) => {
39-
auditService.error('Database connection error', error, {
40-
host: conn.connection.host,
41-
readyState: mongoose.connection.readyState
42-
});
17+
console.error('Database connection error:', error.message);
4318
});
4419

45-
// Setup indexes for performance and race condition prevention
46-
const audit = auditService.startTrace('database_setup');
47-
await setupDatabase();
48-
audit.complete({ indexesSetup: true });
49-
50-
//setupMongoChangeStream();
51-
52-
auditService.systemEvent('database_initialization_complete', {
53-
environment: process.env.NODE_ENV
54-
});
20+
// Warmup leaderboard cache after DB is ready (with delay for Redis connection)
21+
setTimeout(async () => {
22+
try {
23+
const { warmupLeaderboardCache } = await import("../utils/leaderBoardCache.js");
24+
warmupLeaderboardCache();
25+
} catch (error) {
26+
console.error('Failed to warmup leaderboard cache:', error.message);
27+
}
28+
}, 2000); // 2 second delay
5529

5630
} catch (error) {
57-
auditService.critical('Database connection failed', {
58-
error: error.message,
59-
mongoUri: process.env.MONGO_URI ? 'configured' : 'missing',
60-
environment: process.env.NODE_ENV
61-
});
31+
console.error('Database connection failed:', error.message);
6232
process.exit(1);
6333
}
6434
}

server/lib/dbIndexes.js

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,67 @@
11
import mongoose from "mongoose";
22

3-
// Database indexes to prevent race conditions and improve performance
3+
// Drop and recreate all performance indexes
44
export const createOptimalIndexes = async () => {
55
try {
6-
console.log('Creating database indexes for race condition prevention...');
7-
8-
// User model indexes
9-
await mongoose.connection.db.collection('users').createIndexes([
10-
// Unique constraints to prevent duplicate registrations
11-
{ key: { email: 1 }, unique: true, partialFilterExpression: { isVerified: true } },
12-
{ key: { username: 1 }, unique: true, partialFilterExpression: { isVerified: true } },
13-
{ key: { RegistrationNumber: 1 }, unique: true, partialFilterExpression: { isVerified: true } },
14-
15-
// Performance indexes
16-
{ key: { points: -1, rank: 1 }, name: 'leaderboard_sort' },
17-
{ key: { isVerified: 1, points: -1 }, name: 'leaderboard_filter' },
18-
{ key: { 'solveChallenges.easy.timestamp': -1 }, name: 'streak_easy' },
19-
{ key: { 'solveChallenges.medium.timestamp': -1 }, name: 'streak_medium' },
20-
{ key: { 'solveChallenges.hard.timestamp': -1 }, name: 'streak_hard' },
21-
22-
// Platform data indexes
23-
{ key: { 'leetCode.username': 1 }, sparse: true },
24-
{ key: { 'gfg.username': 1 }, sparse: true },
25-
{ key: { 'codeforces.username': 1 }, sparse: true },
26-
{ key: { 'codechef.username': 1 }, sparse: true }
27-
]);
28-
29-
// Challenge model indexes
30-
await mongoose.connection.db.collection('challenges').createIndexes([
31-
// Query performance indexes
32-
{ key: { category: 1, difficulty: 1, createdAt: -1 }, name: 'challenge_filter' },
33-
{ key: { createdAt: -1 }, name: 'daily_challenge' },
34-
{ key: { title: 'text', category: 'text' }, name: 'challenge_search' },
6+
console.log('Setting up database indexes...');
7+
8+
const usersCollection = mongoose.connection.db.collection('users');
9+
const challengesCollection = mongoose.connection.db.collection('challenges');
10+
11+
// Drop problematic indexes individually
12+
try {
13+
// Drop known problematic indexes
14+
const problematicIndexes = [
15+
'username_1', 'email_1', 'UserTextIndex', 'ChallengeTextIndex',
16+
'email_unique_idx', 'username_unique_idx', 'regno_unique_idx',
17+
'challenge_search_idx', 'leaderboard_sort_idx'
18+
];
3519

36-
// Solved users index
37-
{ key: { solvedUsers: 1 }, sparse: true }
38-
]);
39-
20+
for (const indexName of problematicIndexes) {
21+
try {
22+
await usersCollection.dropIndex(indexName);
23+
} catch (e) { /* Index doesn't exist */ }
24+
try {
25+
await challengesCollection.dropIndex(indexName);
26+
} catch (e) { /* Index doesn't exist */ }
27+
}
28+
} catch (error) {
29+
// Continue anyway
30+
}
31+
32+
// Wait for drops to complete
33+
await new Promise(resolve => setTimeout(resolve, 1000));
34+
35+
// Create essential user indexes with explicit names
36+
await usersCollection.createIndex({ email: 1 }, { unique: true, sparse: true, name: 'email_unique_idx' });
37+
await usersCollection.createIndex({ username: 1 }, { unique: true, sparse: true, name: 'username_unique_idx' });
38+
// Skip RegistrationNumber for now - has duplicates
39+
40+
// Performance indexes for leaderboard
41+
await usersCollection.createIndex({ points: -1, rank: 1 }, { name: 'leaderboard_sort_idx' });
42+
await usersCollection.createIndex({ isVerified: 1, points: -1 }, { name: 'verified_points_idx' });
43+
44+
// Streak tracking indexes
45+
await usersCollection.createIndex({ 'solveChallenges.easy.timestamp': -1 }, { name: 'easy_streak_idx' });
46+
await usersCollection.createIndex({ 'solveChallenges.medium.timestamp': -1 }, { name: 'medium_streak_idx' });
47+
await usersCollection.createIndex({ 'solveChallenges.hard.timestamp': -1 }, { name: 'hard_streak_idx' });
48+
49+
// Platform data indexes
50+
await usersCollection.createIndex({ 'leetCode.username': 1 }, { sparse: true, name: 'leetcode_username_idx' });
51+
await usersCollection.createIndex({ 'gfg.username': 1 }, { sparse: true, name: 'gfg_username_idx' });
52+
await usersCollection.createIndex({ 'codeforces.username': 1 }, { sparse: true, name: 'cf_username_idx' });
53+
await usersCollection.createIndex({ 'codechef.username': 1 }, { sparse: true, name: 'cc_username_idx' });
54+
55+
// Challenge indexes
56+
await challengesCollection.createIndex({ category: 1, difficulty: 1, createdAt: -1 }, { name: 'challenge_filter_idx' });
57+
await challengesCollection.createIndex({ createdAt: -1 }, { name: 'challenge_date_idx' });
58+
await challengesCollection.createIndex({ title: 'text', category: 'text' }, { name: 'challenge_search_idx' });
59+
await challengesCollection.createIndex({ solvedUsers: 1 }, { sparse: true, name: 'solved_users_idx' });
60+
4061
console.log('Database indexes created successfully');
4162

4263
} catch (error) {
43-
console.error('Error creating database indexes:', error);
64+
console.error('Error setting up database indexes:', error);
4465
}
4566
};
4667

0 commit comments

Comments
 (0)