Remove getAllUsers endpoint, add checks to image proxy endpoint, setup local in memory cache for rate limitng in dev, added NODE_ENV variable

This commit is contained in:
austinkelsay 2024-09-30 20:35:07 -05:00
parent 735ed2b7db
commit 7953bb641f
4 changed files with 83 additions and 30 deletions

View File

@ -24,4 +24,18 @@ module.exports = removeImports({
}
];
},
env: {
KV_URL: process.env.NODE_ENV !== 'production'
? process.env.REDIS_URL
: process.env.KV_URL,
KV_REST_API_URL: process.env.NODE_ENV !== 'production'
? process.env.REDIS_URL
: process.env.KV_REST_API_URL,
KV_REST_API_TOKEN: process.env.NODE_ENV !== 'production'
? 'dummy_token'
: process.env.KV_REST_API_TOKEN,
KV_REST_API_READ_ONLY_TOKEN: process.env.NODE_ENV !== 'production'
? 'dummy_token'
: process.env.KV_REST_API_READ_ONLY_TOKEN,
},
});

View File

@ -2,13 +2,45 @@ import { NextResponse } from 'next/server';
import { Ratelimit } from '@upstash/ratelimit';
import { kv } from '@vercel/kv';
const ratelimit = new Ratelimit({
redis: kv,
// 5 requests from the same IP in 10 seconds
limiter: Ratelimit.slidingWindow(5, '10 s'),
analytics: true,
timeout: 1000, // 1 second
});
// In-memory store for development
const inMemoryStore = new Map();
// Simple in-memory rate limiter for development
const localRatelimit = {
limit: async (key) => {
const now = Date.now();
const windowMs = 10 * 1000; // 10 seconds
const maxRequests = 5;
const requestLog = inMemoryStore.get(key) || [];
const windowStart = now - windowMs;
const recentRequests = requestLog.filter(timestamp => timestamp > windowStart);
const isRateLimited = recentRequests.length >= maxRequests;
if (!isRateLimited) {
recentRequests.push(now);
inMemoryStore.set(key, recentRequests);
}
return {
success: !isRateLimited,
limit: maxRequests,
remaining: Math.max(0, maxRequests - recentRequests.length),
reset: windowStart + windowMs,
};
},
};
// Use local rate limiter for development, Upstash for production
const ratelimit = process.env.NODE_ENV === 'production'
? new Ratelimit({
redis: kv,
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
timeout: 1000,
})
: localRatelimit;
// Define which routes you want to rate limit
export const config = {
@ -17,7 +49,7 @@ export const config = {
export default async function middleware(request) {
const ip = request.ip ?? '127.0.0.1';
const { success, pending, limit, reset, remaining } = await ratelimit.limit(
const { success, limit, remaining, reset } = await ratelimit.limit(
`ratelimit_middleware_${ip}`
);
@ -25,10 +57,15 @@ export default async function middleware(request) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': reset.toString(),
'Retry-After': Math.ceil((reset - Date.now()) / 1000).toString(),
},
});
}
return NextResponse.next();
const response = NextResponse.next();
response.headers.set('X-RateLimit-Limit', limit.toString());
response.headers.set('X-RateLimit-Remaining', remaining.toString());
response.headers.set('X-RateLimit-Reset', reset.toString());
return response;
}

View File

@ -1,11 +1,25 @@
import axios from 'axios';
import { URL } from 'url';
export default async function handler(req, res) {
const { imageUrl } = req.query;
// Validate the imageUrl query parameter
if (!imageUrl) {
return res.status(400).json({ error: 'Missing imageUrl query parameter' });
if (!imageUrl || typeof imageUrl !== 'string') {
return res.status(400).json({ error: 'Invalid or missing imageUrl query parameter' });
}
// Validate the URL
let parsedUrl;
try {
parsedUrl = new URL(imageUrl);
} catch (error) {
return res.status(400).json({ error: 'Invalid URL' });
}
// Only allow http and https protocols
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
return res.status(403).json({ error: 'Invalid protocol' });
}
try {
@ -13,9 +27,9 @@ export default async function handler(req, res) {
method: 'GET',
url: imageUrl,
responseType: 'arraybuffer',
timeout: 8000, // Set a timeout to prevent long-running requests
// limit the size of the response to 100MB
maxContentLength: 100 * 1024 * 1024,
timeout: 5000, // Reduced timeout to 5 seconds
maxContentLength: 10 * 1024 * 1024, // Reduced max content length to 10MB
// ... rest of the axios config
});
// Validate content type
@ -34,6 +48,6 @@ export default async function handler(req, res) {
res.send(response.data);
} catch (error) {
console.error('Image proxy error:', error);
res.status(500).json({ error: 'Failed to fetch image' });
res.status(error.response?.status || 500).json({ error: 'Failed to fetch image' });
}
}

View File

@ -3,20 +3,8 @@ import { getServerSession } from "next-auth/next"
import { authOptions } from "@/pages/api/auth/[...nextauth].js"
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
const session = await getServerSession(req, res, authOptions)
if (!session) {
return res.status(401).json({ error: 'Unauthorized' });
}
const users = await getAllUsers();
res.status(200).json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
} else if (req.method === 'POST') {
// const session = await getServerSession(req, res, authOptions);
if (req.method === 'POST') {
try {
const user = await createUser(req.body);
res.status(201).json(user);