refactor: remove obsolete member management API module
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"./actions": "./src/actions/index.ts",
|
||||
"./safe-action": "./src/actions/safe-action-client.ts",
|
||||
"./routes": "./src/routes/index.ts",
|
||||
"./routes/rate-limit": "./src/routes/rate-limit.ts",
|
||||
"./route-helpers": "./src/routes/api-helpers.ts"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
102
packages/next/src/routes/rate-limit.ts
Normal file
102
packages/next/src/routes/rate-limit.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import 'server-only';
|
||||
|
||||
/**
|
||||
* Simple in-memory rate limiter for API routes.
|
||||
* Tracks request counts per key within sliding time windows.
|
||||
*
|
||||
* For production at scale, consider Upstash Redis or Vercel Edge Config.
|
||||
* This is sufficient for moderate traffic and single-instance deployments.
|
||||
*/
|
||||
|
||||
interface RateLimitEntry {
|
||||
count: number;
|
||||
resetAt: number;
|
||||
}
|
||||
|
||||
const store = new Map<string, RateLimitEntry>();
|
||||
|
||||
// Cleanup stale entries every 2 minutes
|
||||
const CLEANUP_INTERVAL = 2 * 60 * 1000;
|
||||
// Hard cap to prevent memory exhaustion under attack
|
||||
const MAX_STORE_SIZE = 10_000;
|
||||
let lastCleanup = Date.now();
|
||||
|
||||
function cleanup() {
|
||||
const now = Date.now();
|
||||
|
||||
if (now - lastCleanup < CLEANUP_INTERVAL) return;
|
||||
|
||||
lastCleanup = now;
|
||||
|
||||
for (const [key, entry] of store) {
|
||||
if (entry.resetAt <= now) {
|
||||
store.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// If still over limit after expiry cleanup, evict oldest entries
|
||||
if (store.size > MAX_STORE_SIZE) {
|
||||
const sorted = [...store.entries()].sort(
|
||||
(a, b) => a[1].resetAt - b[1].resetAt,
|
||||
);
|
||||
const toDelete = sorted.slice(0, store.size - MAX_STORE_SIZE);
|
||||
|
||||
for (const [key] of toDelete) {
|
||||
store.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check rate limit for a given key.
|
||||
*
|
||||
* @param key - Unique identifier (e.g., IP address, account ID)
|
||||
* @param maxRequests - Maximum requests allowed within the window
|
||||
* @param windowMs - Time window in milliseconds
|
||||
* @returns Object with `allowed` boolean and `remaining` count
|
||||
*/
|
||||
export function checkRateLimit(
|
||||
key: string,
|
||||
maxRequests: number,
|
||||
windowMs: number,
|
||||
): { allowed: boolean; remaining: number; resetAt: number } {
|
||||
cleanup();
|
||||
|
||||
const now = Date.now();
|
||||
const entry = store.get(key);
|
||||
|
||||
if (!entry || entry.resetAt <= now) {
|
||||
// New window
|
||||
store.set(key, { count: 1, resetAt: now + windowMs });
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: maxRequests - 1,
|
||||
resetAt: now + windowMs,
|
||||
};
|
||||
}
|
||||
|
||||
if (entry.count >= maxRequests) {
|
||||
return { allowed: false, remaining: 0, resetAt: entry.resetAt };
|
||||
}
|
||||
|
||||
entry.count++;
|
||||
|
||||
return {
|
||||
allowed: true,
|
||||
remaining: maxRequests - entry.count,
|
||||
resetAt: entry.resetAt,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract client IP from request headers.
|
||||
* Works with Vercel, Cloudflare, and standard proxies.
|
||||
*/
|
||||
export function getClientIp(request: Request): string {
|
||||
return (
|
||||
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
|
||||
request.headers.get('x-real-ip') ??
|
||||
request.headers.get('cf-connecting-ip') ??
|
||||
'unknown'
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user