Skip to content

Commit efcc924

Browse files
committed
feat(cache): Implement caching for user profiles, activities, and challenges with Redis and in-memory fallback
1 parent a8e8c52 commit efcc924

File tree

8 files changed

+812
-56
lines changed

8 files changed

+812
-56
lines changed

server/controllers/profileController.js

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,24 @@ import cloudinary from "../config/cloudinary.js"
44
import { v4 as uuidv4 } from "uuid"
55
import { Challenge } from "../models/Challenge.js"
66
import auditService from "../services/auditService.js"
7+
import userCacheService from "../services/userCacheService.js"
78

89
export const getUserProfile = async (req, res) => {
910
try {
10-
const user = await User.findById(req.user._id).select("-password -gfg -codechef -googleId -githubId -resetPasswordToken -resetPasswordExpires -otp -otpExpires")
11+
const userId = req.user._id;
12+
13+
// Try to get from cache first
14+
let user = await userCacheService.getCachedUserProfile(userId);
15+
16+
if (!user) {
17+
// If not in cache, fetch from database
18+
user = await User.findById(userId).select("-password -gfg -codechef -googleId -githubId -resetPasswordToken -resetPasswordExpires -otp -otpExpires");
19+
20+
if (user) {
21+
// Cache the user profile
22+
await userCacheService.cacheUserProfile(userId, user);
23+
}
24+
}
1125

1226
if (user) {
1327
res.status(200).json(user)
@@ -112,6 +126,12 @@ export const updateUserProfile = async (req, res) => {
112126

113127
const updatedUser = await user.save()
114128

129+
// Invalidate user cache after update
130+
await userCacheService.invalidateUserCache(req.user._id);
131+
132+
// Cache the updated user profile
133+
await userCacheService.cacheUserProfile(req.user._id, updatedUser);
134+
115135
res.status(200).json({
116136
message: "Profile updated successfully",
117137
user: {
@@ -177,22 +197,33 @@ export const updateUserProfile = async (req, res) => {
177197

178198
export const getUserActivity = async (req, res) => {
179199
try {
180-
const activities = await Activity.aggregate([
181-
{ $match: { user: req.user._id } },
182-
{
183-
$group: {
184-
_id: {
185-
$dateToString: { format: "%Y-%m-%d", date: "$date" },
200+
const userId = req.user._id;
201+
202+
// Try to get from cache first
203+
let activities = await userCacheService.getCachedUserActivity(userId);
204+
205+
if (!activities) {
206+
// If not in cache, fetch from database
207+
activities = await Activity.aggregate([
208+
{ $match: { user: userId } },
209+
{
210+
$group: {
211+
_id: {
212+
$dateToString: { format: "%Y-%m-%d", date: "$date" },
213+
},
214+
count: { $sum: 1 },
186215
},
187-
count: { $sum: 1 },
188216
},
189-
},
190-
{ $sort: { _id: 1 } },
191-
])
217+
{ $sort: { _id: 1 } },
218+
]);
219+
220+
// Cache the activities
221+
await userCacheService.cacheUserActivity(userId, activities);
222+
}
192223

193224
res.status(200).json(activities)
194225
} catch (error) {
195-
// console.error(error)
226+
auditService.error('user_activity_error', { userId: req.user._id, error: error.message });
196227
res.status(500).json({ message: "Server error" })
197228
}
198229
}
@@ -203,17 +234,28 @@ export const getUserByUsername = async (req, res) => {
203234
try {
204235
const { username } = req.params
205236

206-
const user = await User.findOne({ username }).select(
207-
"-password -resetPasswordToken -resetPasswordExpires -otp -otpExpires",
208-
)
237+
// Try to get from cache first
238+
let user = await userCacheService.getCachedUserByUsername(username);
239+
240+
if (!user) {
241+
// If not in cache, fetch from database
242+
user = await User.findOne({ username }).select(
243+
"-password -resetPasswordToken -resetPasswordExpires -otp -otpExpires",
244+
);
245+
246+
if (user) {
247+
// Cache the user data
248+
await userCacheService.cacheUserByUsername(username, user);
249+
}
250+
}
209251

210252
if (!user) {
211253
return res.status(404).json({ message: "User not found" })
212254
}
213255

214256
res.status(200).json({ user })
215257
} catch (error) {
216-
// console.error("Error fetching user by username:", error)
258+
auditService.error('get_user_by_username_error', { username: req.params.username, error: error.message });
217259
res.status(500).json({ message: "Server error" })
218260
}
219261
}

server/package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"license": "ISC",
1818
"description": "",
1919
"dependencies": {
20+
"@upstash/redis": "^1.35.6",
2021
"axios": "^1.8.4",
2122
"bcryptjs": "^3.0.2",
2223
"body-parser": "^1.20.3",

server/routes/cacheRoutes.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import express from "express";
2+
import cacheService from "../services/cacheService.js";
3+
import userCacheService from "../services/userCacheService.js";
4+
import challengeCacheService from "../services/challengeCacheService.js";
5+
import auditService from "../services/auditService.js";
6+
7+
const router = express.Router();
8+
9+
// Get cache statistics (admin only)
10+
router.get('/stats', async (req, res) => {
11+
try {
12+
const cacheStats = cacheService.getStats();
13+
const healthCheck = await cacheService.healthCheck();
14+
15+
auditService.systemEvent('cache_stats_requested', {
16+
adminId: req.user?.id,
17+
redisAvailable: healthCheck.redis
18+
});
19+
20+
res.status(200).json({
21+
success: true,
22+
stats: cacheStats,
23+
health: healthCheck,
24+
timestamp: new Date().toISOString()
25+
});
26+
} catch (error) {
27+
auditService.error('cache_stats_error', { error: error.message });
28+
res.status(500).json({
29+
success: false,
30+
message: 'Error fetching cache statistics'
31+
});
32+
}
33+
});
34+
35+
// Clear specific cache namespace (admin only)
36+
router.delete('/clear/:namespace', async (req, res) => {
37+
try {
38+
const { namespace } = req.params;
39+
const adminId = req.user?.id;
40+
41+
auditService.systemEvent('cache_clear_requested', {
42+
adminId,
43+
namespace
44+
});
45+
46+
switch (namespace) {
47+
case 'users':
48+
await userCacheService.clearAllUserCaches();
49+
break;
50+
case 'challenges':
51+
await challengeCacheService.clearAllChallengeCaches();
52+
break;
53+
case 'leaderboard':
54+
await cacheService.clearNamespace('leaderboard');
55+
break;
56+
case 'all':
57+
await userCacheService.clearAllUserCaches();
58+
await challengeCacheService.clearAllChallengeCaches();
59+
await cacheService.clearNamespace('leaderboard');
60+
break;
61+
default:
62+
return res.status(400).json({
63+
success: false,
64+
message: 'Invalid namespace. Use: users, challenges, leaderboard, or all'
65+
});
66+
}
67+
68+
auditService.systemEvent('cache_cleared', {
69+
adminId,
70+
namespace
71+
});
72+
73+
res.status(200).json({
74+
success: true,
75+
message: `Cache namespace '${namespace}' cleared successfully`,
76+
timestamp: new Date().toISOString()
77+
});
78+
} catch (error) {
79+
auditService.error('cache_clear_error', {
80+
namespace: req.params.namespace,
81+
error: error.message
82+
});
83+
res.status(500).json({
84+
success: false,
85+
message: 'Error clearing cache'
86+
});
87+
}
88+
});
89+
90+
// Warm up caches (admin only)
91+
router.post('/warmup', async (req, res) => {
92+
try {
93+
const adminId = req.user?.id;
94+
auditService.systemEvent('cache_warmup_requested', { adminId });
95+
96+
// Import leaderboard functions
97+
const { warmupLeaderboardCache } = await import("../utils/leaderBoardCache.js");
98+
99+
// Warm up leaderboard cache
100+
await warmupLeaderboardCache();
101+
102+
auditService.systemEvent('cache_warmup_completed', { adminId });
103+
104+
res.status(200).json({
105+
success: true,
106+
message: 'Cache warmup completed successfully',
107+
timestamp: new Date().toISOString()
108+
});
109+
} catch (error) {
110+
auditService.error('cache_warmup_error', { error: error.message });
111+
res.status(500).json({
112+
success: false,
113+
message: 'Error warming up cache'
114+
});
115+
}
116+
});
117+
118+
// Health check endpoint
119+
router.get('/health', async (req, res) => {
120+
try {
121+
const health = await cacheService.healthCheck();
122+
123+
res.status(200).json({
124+
success: true,
125+
health,
126+
timestamp: new Date().toISOString()
127+
});
128+
} catch (error) {
129+
auditService.error('cache_health_check_error', { error: error.message });
130+
res.status(500).json({
131+
success: false,
132+
message: 'Error checking cache health'
133+
});
134+
}
135+
});
136+
137+
export default router;

0 commit comments

Comments
 (0)