Skip to content

Commit 59c5aab

Browse files
authored
Merge pull request #34 from Shashank0701-byte/feature/api-rate-limiting-issue-10
feat: implement comprehensive API rate limiting protection - #10
2 parents 114725a + d5256c9 commit 59c5aab

14 files changed

Lines changed: 4938 additions & 775 deletions

File tree

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@ NEXT_PUBLIC_GEMINI_API_KEY=your_gemini_api_key_here
55
# MongoDB Database Configuration
66
# Format: mongodb+srv://username:password@cluster.mongodb.net/database?retryWrites=true&w=majority
77
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/toolbox?retryWrites=true&w=majority
8+
9+
# Rate Limiting (Upstash Redis)
10+
UPSTASH_REDIS_REST_URL=your-upstash-redis-url
11+
UPSTASH_REDIS_REST_TOKEN=your-upstash-redis-token

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ ToolBox is revolutionizing how developers create and share productivity tools. W
2828
- **🌙 Dark Mode** - Full dark mode support for comfortable extended use
2929
- **📦 MongoDB Integration** - Robust data storage with user isolation
3030
- **🛡️ Type Safe** - Built with TypeScript for reliability and superior developer experience
31+
- **🚨 Rate Limiting** - API protection with intelligent rate limiting and IP-based tracking
3132

3233
### 🚀 Roadmap (Community Contributions Welcome!)
3334
- **🔌 Plugin Marketplace** - Discover and install community-created tools

__tests__/rate-limiting.test.ts

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/**
2+
* @jest-environment node
3+
*/
4+
5+
import { NextRequest } from "next/server";
6+
import { withRateLimit, checkRateLimit } from "../lib/middleware/rate-limit-middleware";
7+
import { RATE_LIMIT_CONFIG } from "../lib/rate-limit";
8+
9+
// Mock the rate limiter for testing
10+
jest.mock("../lib/rate-limit", () => ({
11+
...jest.requireActual("../lib/rate-limit"),
12+
rateLimiters: {
13+
default: {
14+
limit: jest.fn(),
15+
},
16+
api: {
17+
limit: jest.fn(),
18+
},
19+
templates: {
20+
limit: jest.fn(),
21+
},
22+
snippets: {
23+
limit: jest.fn(),
24+
},
25+
ai: {
26+
limit: jest.fn(),
27+
},
28+
auth: {
29+
limit: jest.fn(),
30+
},
31+
upload: {
32+
limit: jest.fn(),
33+
},
34+
},
35+
}));
36+
37+
describe("Rate Limiting", () => {
38+
beforeEach(() => {
39+
jest.clearAllMocks();
40+
});
41+
42+
describe("Rate Limit Configuration", () => {
43+
it("should have correct rate limit configurations", () => {
44+
expect(RATE_LIMIT_CONFIG.default).toEqual({ requests: 100, window: "1h" });
45+
expect(RATE_LIMIT_CONFIG.api).toEqual({ requests: 60, window: "1m" });
46+
expect(RATE_LIMIT_CONFIG.templates).toEqual({ requests: 30, window: "1m" });
47+
expect(RATE_LIMIT_CONFIG.snippets).toEqual({ requests: 50, window: "1m" });
48+
expect(RATE_LIMIT_CONFIG.ai).toEqual({ requests: 10, window: "1m" });
49+
expect(RATE_LIMIT_CONFIG.auth).toEqual({ requests: 5, window: "1m" });
50+
expect(RATE_LIMIT_CONFIG.upload).toEqual({ requests: 20, window: "1m" });
51+
});
52+
});
53+
54+
describe("Rate Limiting Middleware", () => {
55+
const mockHandler = jest.fn();
56+
const mockRequest = new NextRequest("http://localhost:3000/api/test", {
57+
headers: {
58+
"x-forwarded-for": "192.168.1.1",
59+
},
60+
});
61+
62+
beforeEach(() => {
63+
mockHandler.mockClear();
64+
});
65+
66+
it("should allow requests within rate limit", async () => {
67+
const { rateLimiters } = require("../lib/rate-limit");
68+
rateLimiters.default.limit.mockResolvedValue({
69+
success: true,
70+
limit: 100,
71+
remaining: 99,
72+
reset: Date.now() + 3600000,
73+
});
74+
75+
mockHandler.mockResolvedValue(new Response("OK"));
76+
77+
const response = await withRateLimit(mockRequest, mockHandler, "default");
78+
79+
expect(rateLimiters.default.limit).toHaveBeenCalledWith("192.168.1.1:default");
80+
expect(mockHandler).toHaveBeenCalledWith(mockRequest);
81+
expect(response.status).toBe(200);
82+
});
83+
84+
it("should block requests exceeding rate limit", async () => {
85+
const { rateLimiters } = require("../lib/rate-limit");
86+
rateLimiters.default.limit.mockResolvedValue({
87+
success: false,
88+
limit: 100,
89+
remaining: 0,
90+
reset: Date.now() + 3600000,
91+
});
92+
93+
const response = await withRateLimit(mockRequest, mockHandler, "default");
94+
95+
expect(rateLimiters.default.limit).toHaveBeenCalledWith("192.168.1.1:default");
96+
expect(mockHandler).not.toHaveBeenCalled();
97+
expect(response.status).toBe(429);
98+
99+
const body = await response.json();
100+
expect(body.error).toBe("Rate limit exceeded");
101+
});
102+
103+
it("should add rate limit headers to successful responses", async () => {
104+
const { rateLimiters } = require("../lib/rate-limit");
105+
rateLimiters.api.limit.mockResolvedValue({
106+
success: true,
107+
limit: 60,
108+
remaining: 59,
109+
reset: Date.now() + 60000,
110+
});
111+
112+
mockHandler.mockResolvedValue(new Response("OK"));
113+
114+
const response = await withRateLimit(mockRequest, mockHandler, "api");
115+
116+
expect(response.headers.get("X-RateLimit-Limit")).toBe("60");
117+
expect(response.headers.get("X-RateLimit-Remaining")).toBe("59");
118+
expect(response.headers.get("X-RateLimit-Policy")).toBe("sliding-window");
119+
});
120+
121+
it("should handle different rate limit types", async () => {
122+
const { rateLimiters } = require("../lib/rate-limit");
123+
124+
// Test AI rate limiting (strictest)
125+
rateLimiters.ai.limit.mockResolvedValue({
126+
success: true,
127+
limit: 10,
128+
remaining: 9,
129+
reset: Date.now() + 60000,
130+
});
131+
132+
mockHandler.mockResolvedValue(new Response("OK"));
133+
134+
const response = await withRateLimit(mockRequest, mockHandler, "ai");
135+
136+
expect(rateLimiters.ai.limit).toHaveBeenCalledWith("192.168.1.1:ai");
137+
expect(response.headers.get("X-RateLimit-Limit")).toBe("10");
138+
});
139+
140+
it("should handle rate limiting errors gracefully", async () => {
141+
const { rateLimiters } = require("../lib/rate-limit");
142+
rateLimiters.default.limit.mockRejectedValue(new Error("Redis connection failed"));
143+
144+
mockHandler.mockResolvedValue(new Response("OK"));
145+
146+
const response = await withRateLimit(mockRequest, mockHandler, "default");
147+
148+
// Should allow request to proceed if rate limiting fails
149+
expect(mockHandler).toHaveBeenCalledWith(mockRequest);
150+
expect(response.status).toBe(200);
151+
});
152+
});
153+
154+
describe("IP Address Detection", () => {
155+
it("should extract IP from X-Forwarded-For header", () => {
156+
const { getClientIP } = require("../lib/rate-limit");
157+
const request = new NextRequest("http://localhost:3000/api/test", {
158+
headers: {
159+
"x-forwarded-for": "192.168.1.1, 10.0.0.1",
160+
},
161+
});
162+
163+
const ip = getClientIP(request);
164+
expect(ip).toBe("192.168.1.1");
165+
});
166+
167+
it("should extract IP from X-Real-IP header", () => {
168+
const { getClientIP } = require("../lib/rate-limit");
169+
const request = new NextRequest("http://localhost:3000/api/test", {
170+
headers: {
171+
"x-real-ip": "192.168.1.2",
172+
},
173+
});
174+
175+
const ip = getClientIP(request);
176+
expect(ip).toBe("192.168.1.2");
177+
});
178+
179+
it("should fallback to default IP", () => {
180+
const { getClientIP } = require("../lib/rate-limit");
181+
const request = new NextRequest("http://localhost:3000/api/test");
182+
183+
const ip = getClientIP(request);
184+
expect(ip).toBe("127.0.0.1");
185+
});
186+
});
187+
});
188+
189+
describe("Rate Limit Error Responses", () => {
190+
it("should create proper error response format", () => {
191+
const { createRateLimitErrorResponse } = require("../lib/rate-limit");
192+
const result = {
193+
success: false,
194+
limit: 10,
195+
remaining: 0,
196+
reset: new Date("2024-01-01T12:00:00Z").getTime(),
197+
};
198+
199+
const response = createRateLimitErrorResponse(result);
200+
201+
expect(response.status).toBe(429);
202+
expect(response.headers.get("X-RateLimit-Limit")).toBe("10");
203+
expect(response.headers.get("X-RateLimit-Remaining")).toBe("0");
204+
expect(response.headers.get("Retry-After")).toBeTruthy();
205+
});
206+
});

app/api/ai/generate/route.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { rateLimitMiddleware } from "@/lib/middleware/rate-limit-middleware";
3+
4+
// Helper function to sanitize user input
5+
const sanitizeInput = (input: string): string => {
6+
return input.replace(/[<>'"]/g, (char) => {
7+
const entities: { [key: string]: string } = {
8+
'<': '&lt;',
9+
'>': '&gt;',
10+
"'": '&#39;',
11+
'"': '&quot;'
12+
};
13+
return entities[char] || char;
14+
}).trim().slice(0, 500); // Limit length
15+
};
16+
17+
// POST /api/ai/generate - AI-powered code generation
18+
async function handleAIGenerate(request: NextRequest): Promise<NextResponse> {
19+
try {
20+
const body = await request.json();
21+
const { prompt, type, language } = body;
22+
23+
// Validate required fields
24+
if (!prompt || !type) {
25+
return NextResponse.json(
26+
{ error: "Prompt and type are required" },
27+
{ status: 400 }
28+
);
29+
}
30+
31+
// Validate type
32+
const supportedTypes = ["code", "template", "snippet", "documentation"];
33+
if (!supportedTypes.includes(type)) {
34+
return NextResponse.json(
35+
{ error: `Unsupported type. Supported: ${supportedTypes.join(", ")}` },
36+
{ status: 400 }
37+
);
38+
}
39+
40+
// Sanitize inputs
41+
const sanitizedPrompt = sanitizeInput(prompt);
42+
const sanitizedLanguage = language ? sanitizeInput(language) : "javascript";
43+
44+
// Simulate AI generation (replace with actual AI service)
45+
let generatedContent = "";
46+
47+
switch (type) {
48+
case "code":
49+
generatedContent = `// Generated ${sanitizedLanguage} code based on: ${sanitizedPrompt}\nfunction generatedFunction() {\n // TODO: Implement based on prompt\n console.log("Generated from prompt");\n}`;
50+
break;
51+
case "template":
52+
generatedContent = `<!-- Generated template for: ${sanitizedPrompt} -->\n<div class="{{className}}">\n <h1>{{title}}</h1>\n <p>{{description}}</p>\n</div>`;
53+
break;
54+
case "snippet":
55+
generatedContent = `// Snippet: ${sanitizedPrompt}\nconst result = () => {\n // Implementation here\n return "Generated snippet";\n};`;
56+
break;
57+
case "documentation":
58+
generatedContent = `# ${sanitizedPrompt}\n\n## Overview\nGenerated documentation for ${sanitizedPrompt}.\n\n## Usage\n\`\`\`${sanitizedLanguage}\n// Example usage\n\`\`\`\n\n## Parameters\n- param1: Description\n- param2: Description`;
59+
break;
60+
}
61+
62+
const result = {
63+
id: crypto.randomUUID(),
64+
prompt,
65+
type,
66+
language: language || "javascript",
67+
content: generatedContent,
68+
generatedAt: new Date().toISOString(),
69+
model: "placeholder", // Update when real AI service is integrated
70+
};
71+
72+
return NextResponse.json({
73+
success: true,
74+
data: result,
75+
message: "Content generated successfully",
76+
});
77+
} catch (error) {
78+
console.error("Error generating AI content:", error);
79+
return NextResponse.json(
80+
{ error: "Failed to generate content" },
81+
{ status: 500 }
82+
);
83+
}
84+
}
85+
86+
// Apply strict rate limiting for AI endpoints
87+
export const POST = rateLimitMiddleware.ai(handleAIGenerate);

app/api/health/route.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { rateLimitMiddleware } from "@/lib/middleware/rate-limit-middleware";
3+
4+
// GET /api/health - Health check endpoint
5+
async function handleHealthCheck(request: NextRequest): Promise<NextResponse> {
6+
try {
7+
const health = {
8+
status: "healthy",
9+
timestamp: new Date().toISOString(),
10+
uptime: process.uptime(),
11+
version: process.env.npm_package_version || "1.0.0",
12+
environment: process.env.NODE_ENV || "development",
13+
services: {
14+
database: "connected", // Placeholder
15+
redis: process.env.UPSTASH_REDIS_REST_URL ? "connected" : "disabled",
16+
ai: "available", // Placeholder
17+
},
18+
rateLimit: {
19+
enabled: true,
20+
provider: process.env.UPSTASH_REDIS_REST_URL ? "redis" : "memory",
21+
}
22+
};
23+
24+
return NextResponse.json({
25+
success: true,
26+
data: health,
27+
});
28+
} catch (error) {
29+
console.error("Health check error:", error);
30+
return NextResponse.json(
31+
{
32+
success: false,
33+
status: "unhealthy",
34+
error: "Health check failed",
35+
timestamp: new Date().toISOString(),
36+
},
37+
{ status: 503 }
38+
);
39+
}
40+
}
41+
42+
// Apply default rate limiting to health check
43+
export const GET = rateLimitMiddleware.api(handleHealthCheck);

0 commit comments

Comments
 (0)