Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 14 additions & 114 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ import routeSubmission from "./routes/submissionRoutes";
import userRoute from "./routes/userRoute";
import { initSocket } from "./utils/socket";

// ─── Rate Limiters ────────────────────────────────────────────────────────────
import rateLimit from "express-rate-limit";

import Redis from "ioredis";
import RedisStore from "rate-limit-redis";

dotenv.config();

Expand All @@ -43,86 +42,6 @@ const redisClient = new Redis({
maxRetriesPerRequest: null,
});

// ─── Limiter factory ──────────────────────────────────────────────────────────
function createLimiter(opts: {
windowMs: number;
max: number;
message: string;
keyPrefix: string;
}) {
return rateLimit({
windowMs: opts.windowMs,
max: opts.max,
standardHeaders: true,
legacyHeaders: false,
// use real IP even behind nginx / vercel proxy
keyGenerator: (req) =>
(req.headers["x-forwarded-for"] as string)?.split(",")[0].trim() ||
req.ip ||
"unknown",
store: new RedisStore({
sendCommand: (...args: string[]) => {
return redisClient.call(args[0], ...args.slice(1)) as any;
},
}),
handler: (_req, res) => {
res.status(429).json({
success: false,
error: opts.message,
retryAfter: Math.ceil(opts.windowMs / 1000),
});
},
});
}

// 100 req / 15 min — broad API protection
const apiLimiter = createLimiter({
windowMs: 15 * 60 * 1000,
max: 100,
message: "Too many requests. Please try again in 15 minutes.",
keyPrefix: "rl:api:",
});

// 10 req / 15 min — brute-force protection on auth
const authLimiter = createLimiter({
windowMs: 15 * 60 * 1000,
max: 10,
message: "Too many auth attempts. Please try again in 15 minutes.",
keyPrefix: "rl:auth:",
});

// 5 req / hour — expensive OpenAI call
const quizGenerationLimiter = createLimiter({
windowMs: 60 * 60 * 1000,
max: 5,
message:
"Quiz generation limit reached. You can generate 5 quizzes per hour.",
keyPrefix: "rl:quiz:",
});

// 3 req / hour — interview session is heaviest resource
const interviewLimiter = createLimiter({
windowMs: 60 * 60 * 1000,
max: 3,
message: "Interview limit reached. You can start 3 interviews per hour.",
keyPrefix: "rl:interview:",
});

// 10 req / hour — resume review (OpenAI + S3)
const resumeLimiter = createLimiter({
windowMs: 60 * 60 * 1000,
max: 10,
message: "Resume review limit reached. Try again in an hour.",
keyPrefix: "rl:resume:",
});

// 20 req / min — code execution (sandboxed but still expensive)
const codeExecutionLimiter = createLimiter({
windowMs: 60 * 1000,
max: 20,
message: "Code execution limit reached. Max 20 runs per minute.",
keyPrefix: "rl:code:",
});

// ─── App bootstrap ────────────────────────────────────────────────────────────
const app: Application = express();
Expand Down Expand Up @@ -230,39 +149,20 @@ io.on("connection", (socket) => {
logger.info(`User joined interview room: ${interviewId}`);
});
});
app.use("/v1", routesAuth);
app.use("/v1", getLeaderboard);

// ─── Routes with rate limiting ────────────────────────────────────────────────

// Auth — tight limit, no auth middleware needed
app.use("/v1", authLimiter, routesAuth);

// Leaderboard — public, covered by global apiLimiter
app.use("/v1", apiLimiter, getLeaderboard);

// AI-heavy routes — strictest limits, must be BEFORE the generic apiLimiter
app.use(
"/v1/quiz/generate",
isAuthenticated,
quizGenerationLimiter,
quizRoutes,
);
app.use(
"/v1/interview/start",
isAuthenticated,
interviewLimiter,
interviewRoutes,
);
app.use("/v1/resume", isAuthenticated, resumeLimiter, resumeReviewer);
app.use("/v1", isAuthenticated, codeExecutionLimiter, routeExecuteCode);

// Standard authenticated routes — general API limit
app.use("/v1", isAuthenticated, apiLimiter, quizAttempt);
app.use("/v1/problem", isAuthenticated, apiLimiter, routesProblem);
app.use("/v1/playlist", isAuthenticated, apiLimiter, routesPlaylist);
app.use("/v1", isAuthenticated, apiLimiter, routeSubmission);
app.use("/v1", isAuthenticated, apiLimiter, paymentRoutes);
app.use("/v1/user", isAuthenticated, apiLimiter, userRoute);
app.use("/v1/quiz/generate", isAuthenticated, quizRoutes);
app.use("/v1/interview/start", isAuthenticated, interviewRoutes);
app.use("/v1/resume", isAuthenticated, resumeReviewer);
app.use("/v1", isAuthenticated, routeExecuteCode);

app.use("/v1", isAuthenticated, quizAttempt);
app.use("/v1/problem", isAuthenticated, routesProblem);
app.use("/v1/playlist", isAuthenticated, routesPlaylist);
app.use("/v1", isAuthenticated, routeSubmission);
app.use("/v1", isAuthenticated, paymentRoutes);
app.use("/v1/user", isAuthenticated, userRoute);
// ─── Server start ─────────────────────────────────────────────────────────────
httpServer.listen(process.env.PORT, () => {
logger.info(`Server is running on port ${process.env.PORT}`);
Expand Down
9 changes: 6 additions & 3 deletions src/controller/authController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,16 +282,19 @@ export const getAdminStats = async (req: Request, res: Response) => {
};


export const getUserActivity = async (req:Request, res:Response) => {
export const getUserActivity = async (req: Request, res: Response) => {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({ error: "Unauthorized" });
}

const data = await prisma.problemSolved.findMany({
where: { userId },
select: { createdAt: true },
});

const map = {};

const map: Record<string, number> = {};
console.log(data)

data.forEach((item) => {
Expand Down
2 changes: 1 addition & 1 deletion src/worker/interView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ Q8: Graceful close. Thank them warmly. Trigger isFinalReport: true + "command":
});
}
},
{ connection: { host: process.env.REDIS_HOST|"redis", port: 6379, maxRetriesPerRequest: null } },
{ connection: { host: process.env.REDIS_HOST||"redis", port: 6379, maxRetriesPerRequest: null } },
);

interviewWorker.on("completed", (job) => {
Expand Down