Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
# MONGODB_URI=mongodb://localhost:27017/iiitl-alumni

# MongoDB Atlas (production example)
# MONGODB_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/iiitl
# MONGODB_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/iiitl

# Upstash Redis
# UPSTASH_REDIS_REST_URL=
# UPSTASH_REDIS_REST_TOKEN=
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
18 changes: 17 additions & 1 deletion app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { connectDB } from "@/lib/db";
import { withRateLimit } from "@/lib/with-ratelimit";

export async function GET() {
const start = Date.now();
Expand All @@ -11,4 +12,19 @@ export async function GET() {
status: "ok",
latency,
});
}
}

export const POST = withRateLimit(async (_req: Request) => {
// Create logic
return Response.json({ success: true });
});

export const PUT = withRateLimit(async (_req: Request) => {
// Update logic
return Response.json({ success: true });
});

export const DELETE = withRateLimit(async (_req: Request) => {
// Delete logic
return Response.json({ success: true });
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
30 changes: 30 additions & 0 deletions lib/audit-log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// import { connectDB } from "./db";

interface AuditLogParams {
severity: 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL';
event: string;
target: string;
message: string;
}

export async function logToAudit(params: AuditLogParams) {
try {
// Establish/reuse the MongoDB connection
// await connectDB();

// Perform your database operation
// Have to create model for AuditLog first
// await AuditLog.create({
// severity: params.severity,
// event: params.event,
// target: params.target,
// message: params.message,
// timestamp: new Date(),
// });

console.log("[AuditLog]", params.message);

Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated
} catch (error) {
console.error("Failed to write to audit log:", error);
}
}
Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated
60 changes: 60 additions & 0 deletions lib/ratelimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Ratelimit } from '@upstash/ratelimit';
Comment thread
MrImmortal09 marked this conversation as resolved.
import { Redis } from '@upstash/redis';
import { logToAudit } from './audit-log';

const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated
Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated

// Define valid window formats expected by Upstash
Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated
type Unit = 'ms' | 's' | 'm' | 'h' | 'd';
type Duration = `${number} ${Unit}` | `${number}${Unit}`;

interface RateLimitConfig {
maxRequests: number;
window: Duration;
}

export async function limit(key: string, config: RateLimitConfig) {
// Initialize the sliding window limiter with the custom config
const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(config.maxRequests, config.window),
analytics: true,
});

const result = await ratelimit.limit(key);
Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated

// Calculate standard Retry-After in seconds
const retryAfterSeconds = Math.ceil((result.reset - Date.now()) / 1000);
Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated

// Handle breach logging
if (!result.success) {
const breachKey = `breach_count:${key}`;

// Increment a separate counter for breaches, expiring after 1 hour
const breachCount = await redis.incr(breachKey);
if (breachCount === 1) {
await redis.expire(breachKey, 3600);
}

// If they hit the 429 limit 3+ times in an hour, escalate to AuditLog
if (breachCount >= 3) {
await logToAudit({
severity: 'WARNING',
event: 'CONSISTENT_RATE_LIMIT_BREACH',
target: key,
message: `Key breached rate limits ${breachCount} times within the hour.`,
Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return {
success: result.success,
limit: result.limit,
remaining: result.remaining,
reset: result.reset,
retryAfter: retryAfterSeconds > 0 ? retryAfterSeconds : 1,
};
}
Comment thread
MrImmortal09 marked this conversation as resolved.
27 changes: 27 additions & 0 deletions lib/with-ratelimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { NextRequest, NextResponse } from "next/server";
import { limit } from "./ratelimit";

export function withRateLimit<T extends unknown[]>(
handler: (req: NextRequest, ...args: T) => Promise<Response> | Response
) {
return async (req: NextRequest, ...args: T) => {
// Determine the user's IP or identifier to use as the rate limit key
const ip = req.headers.get("x-forwarded-for") || req.headers.get("x-real-ip") || "127.0.0.1";
Comment thread
MrImmortal09 marked this conversation as resolved.

// Call your limit function (allow e.g. 5 requests per minute)
const { success, retryAfter } = await limit(ip, {
maxRequests: 5,
window: "1 m",
});
Comment thread
MrImmortal09 marked this conversation as resolved.
Outdated

if (!success) {
return NextResponse.json(
{ error: "Too many requests. Please try again later." },
{ status: 429, headers: { "Retry-After": retryAfter.toString() } }
);
}

// If rate limit checks pass, execute the actual API route logic
return handler(req, ...args);
};
}
Comment thread
MrImmortal09 marked this conversation as resolved.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"db:seed": "node scripts/seed.js"
},
"dependencies": {
"@upstash/ratelimit": "^2.0.8",
"@upstash/redis": "^1.37.0",
"dotenv": "^17.4.1",
"mongoose": "^9.4.1",
"next": "16.2.3",
Expand Down
36 changes: 36 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.