110 lines
2.6 KiB
TypeScript
110 lines
2.6 KiB
TypeScript
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour in milliseconds
|
|
const MAX_REQUESTS_PER_WINDOW = 5;
|
|
const RATE_LIMIT_PREFIX = 'ratelimit:signup:';
|
|
|
|
const isDev = !process.env.KV_REST_API_URL;
|
|
|
|
// In-memory fallback for development
|
|
const devRateLimits = new Map<string, { count: number; resetAt: number }>();
|
|
|
|
export async function checkRateLimit(identifier: string): Promise<{
|
|
allowed: boolean;
|
|
remaining: number;
|
|
resetAt: number;
|
|
}> {
|
|
const now = Date.now();
|
|
|
|
if (isDev) {
|
|
console.warn('[DEV MODE] Using in-memory rate limiting - data will not persist');
|
|
|
|
const existing = devRateLimits.get(identifier);
|
|
|
|
if (existing && existing.resetAt > now) {
|
|
if (existing.count >= MAX_REQUESTS_PER_WINDOW) {
|
|
return {
|
|
allowed: false,
|
|
remaining: 0,
|
|
resetAt: existing.resetAt,
|
|
};
|
|
}
|
|
|
|
existing.count += 1;
|
|
devRateLimits.set(identifier, existing);
|
|
|
|
return {
|
|
allowed: true,
|
|
remaining: MAX_REQUESTS_PER_WINDOW - existing.count,
|
|
resetAt: existing.resetAt,
|
|
};
|
|
}
|
|
|
|
const resetAt = now + RATE_LIMIT_WINDOW;
|
|
devRateLimits.set(identifier, { count: 1, resetAt });
|
|
|
|
return {
|
|
allowed: true,
|
|
remaining: MAX_REQUESTS_PER_WINDOW - 1,
|
|
resetAt,
|
|
};
|
|
}
|
|
|
|
// Production: Use Vercel KV
|
|
const { kv } = await import('@vercel/kv');
|
|
const key = `${RATE_LIMIT_PREFIX}${identifier}`;
|
|
|
|
try {
|
|
const count = await kv.get<number>(key) || 0;
|
|
|
|
if (count >= MAX_REQUESTS_PER_WINDOW) {
|
|
const ttl = await kv.ttl(key);
|
|
const resetAt = now + (ttl * 1000);
|
|
|
|
return {
|
|
allowed: false,
|
|
remaining: 0,
|
|
resetAt,
|
|
};
|
|
}
|
|
|
|
const newCount = count + 1;
|
|
|
|
if (count === 0) {
|
|
await kv.set(key, newCount, {
|
|
px: RATE_LIMIT_WINDOW,
|
|
});
|
|
} else {
|
|
await kv.incr(key);
|
|
}
|
|
|
|
const ttl = await kv.ttl(key);
|
|
const resetAt = now + (ttl * 1000);
|
|
|
|
return {
|
|
allowed: true,
|
|
remaining: MAX_REQUESTS_PER_WINDOW - newCount,
|
|
resetAt,
|
|
};
|
|
} catch (error) {
|
|
console.error('Rate limit check failed:', error);
|
|
return {
|
|
allowed: true,
|
|
remaining: MAX_REQUESTS_PER_WINDOW,
|
|
resetAt: now + RATE_LIMIT_WINDOW,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get client IP from request headers
|
|
*/
|
|
export function getClientIp(request: Request): string {
|
|
// Try various headers that might contain the IP
|
|
const headers = request.headers;
|
|
|
|
return (
|
|
headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
|
headers.get('x-real-ip') ||
|
|
headers.get('cf-connecting-ip') || // Cloudflare
|
|
'unknown'
|
|
);
|
|
} |