diff --git a/claim-db-worker/app/api/analytics/route.ts b/claim-db-worker/app/api/analytics/route.ts index fde1a48..7a48827 100644 --- a/claim-db-worker/app/api/analytics/route.ts +++ b/claim-db-worker/app/api/analytics/route.ts @@ -1,14 +1,24 @@ import { NextRequest, NextResponse } from "next/server"; import { getEnv } from "@/lib/env"; +import { buildRateLimitKey } from "@/lib/server/ratelimit"; export async function POST(request: NextRequest) { const env = getEnv(); - const rateLimitResult = await env.CLAIM_DB_RATE_LIMITER.limit({ - key: request.url, - }); - if (!rateLimitResult.success) { - return NextResponse.json({ error: "Rate limit exceeded" }, { status: 429 }); + const url = new URL(request.url); + const key = buildRateLimitKey(request); + + // --- Simple rate limiting --- + const { success } = await env.CLAIM_DB_RATE_LIMITER.limit({ key }); + if (!success) { + return NextResponse.json( + { + error: "rate_limited", + message: "Rate limit exceeded. Please try again later.", + path: url.pathname, + }, + { status: 429 } + ); } if (!env.POSTHOG_API_KEY || !env.POSTHOG_API_HOST) { diff --git a/claim-db-worker/app/api/auth/callback/route.ts b/claim-db-worker/app/api/auth/callback/route.ts index d0f1d2b..2c2b544 100644 --- a/claim-db-worker/app/api/auth/callback/route.ts +++ b/claim-db-worker/app/api/auth/callback/route.ts @@ -1,4 +1,4 @@ -import { NextRequest } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { getEnv } from "@/lib/env"; import { exchangeCodeForToken, validateProject } from "@/lib/auth-utils"; import { @@ -7,6 +7,7 @@ import { getBaseUrl, } from "@/lib/response-utils"; import { transferProject } from "@/lib/project-transfer"; +import { buildRateLimitKey } from "@/lib/server/ratelimit"; async function sendServerAnalyticsEvent( event: string, @@ -46,21 +47,25 @@ async function sendServerAnalyticsEvent( export async function GET(request: NextRequest) { try { const env = getEnv(); - const { searchParams } = new URL(request.url); + const url = new URL(request.url); + const { searchParams } = url; const code = searchParams.get("code"); const state = searchParams.get("state"); const projectID = searchParams.get("projectID"); - // Rate limiting - const rateLimitResult = await env.CLAIM_DB_RATE_LIMITER.limit({ - key: request.url, - }); - if (!rateLimitResult.success) { - return redirectToError( - request, - "Rate Limited", - "We're experiencing high demand. Please try again later." + const key = buildRateLimitKey(request); + + // --- Simple rate limiting --- + const { success } = await env.CLAIM_DB_RATE_LIMITER.limit({ key }); + if (!success) { + return NextResponse.json( + { + error: "rate_limited", + message: "Rate limit exceeded. Please try again later.", + path: url.pathname, + }, + { status: 429 } ); } diff --git a/claim-db-worker/app/api/claim/route.ts b/claim-db-worker/app/api/claim/route.ts index 4dbdcb8..c0c9220 100644 --- a/claim-db-worker/app/api/claim/route.ts +++ b/claim-db-worker/app/api/claim/route.ts @@ -1,19 +1,28 @@ import { NextRequest, NextResponse } from "next/server"; import { getEnv } from "@/lib/env"; import { sendAnalyticsEvent } from "@/lib/analytics"; +import { buildRateLimitKey } from "@/lib/server/ratelimit"; export async function GET(request: NextRequest) { const env = getEnv(); - const { searchParams } = new URL(request.url); - const rateLimitResult = await env.CLAIM_DB_RATE_LIMITER.limit({ - key: request.url, - }); - if (!rateLimitResult.success) { - return NextResponse.json({ error: "Rate limit exceeded" }, { status: 429 }); + const url = new URL(request.url); + const key = buildRateLimitKey(request); + + // --- Simple rate limiting --- + const { success } = await env.CLAIM_DB_RATE_LIMITER.limit({ key }); + if (!success) { + return NextResponse.json( + { + error: "rate_limited", + message: "Rate limit exceeded. Please try again later.", + path: url.pathname, + }, + { status: 429 } + ); } - const projectID = searchParams.get("projectID"); + const projectID = url.searchParams.get("projectID"); if (!projectID || projectID === "undefined") { return NextResponse.json({ error: "Missing project ID" }, { status: 400 }); diff --git a/claim-db-worker/lib/server/ratelimit.ts b/claim-db-worker/lib/server/ratelimit.ts new file mode 100644 index 0000000..01c8a6e --- /dev/null +++ b/claim-db-worker/lib/server/ratelimit.ts @@ -0,0 +1,17 @@ +import type { NextRequest } from "next/server"; + +export function getClientIP(req: NextRequest): string { + // OpenNext on CF Workers exposes the Request headers directly + // Consider: prefer CF header in prod; only then fall back to XFF/X-Real-IP/Forwarded. + return ( + req.headers.get("cf-connecting-ip") || + req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || + req.headers.get("x-real-ip") || + "unknown-ip" + ); +} + +export function buildRateLimitKey(req: NextRequest) { + const url = new URL(req.url); + return `${req.method}:${getClientIP(req)}:${url.pathname}`; +} diff --git a/create-db-worker/src/index.ts b/create-db-worker/src/index.ts index a4c7066..1910241 100644 --- a/create-db-worker/src/index.ts +++ b/create-db-worker/src/index.ts @@ -15,14 +15,47 @@ export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const analytics = new PosthogEventCapture(env); - // --- Rate limiting --- - const { success } = await env.CREATE_DB_RATE_LIMITER.limit({ key: request.url }); + // --- Rate limiting + const url = new URL(request.url); - if (!success) { - return new Response(`429 Failure - rate limit exceeded for ${request.url}`, { status: 429 }); + // Prefer Cloudflare IP, then common proxy headers; fallback keeps key stable + const clientIP = + request.headers.get('cf-connecting-ip') || + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + request.headers.get('x-real-ip') || + 'unknown-ip'; + + // Key by IP + route so /health doesn't burn the same budget as /create + let success = true; + + try { + const res = await env.CREATE_DB_RATE_LIMITER.limit({ + key: `${request.method}:${clientIP}:${url.pathname}`, + }); + success = res.success; + } catch (e) { + // Keep it simple: don't block users if the limiter is unavailable. + // Flip to `success = false` if you prefer fail-closed. + console.error('Rate limiter error:', e); + success = true; } - const url = new URL(request.url); + if (!success) { + return new Response( + JSON.stringify({ + error: 'rate_limited', + message: 'Rate limit exceeded. Please try again later.', + path: url.pathname, + ip: clientIP, + }), + { + status: 429, + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + } // --- Test endpoint for rate limit testing --- if (url.pathname === '/test' && request.method === 'GET') {