Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions claim-db-worker/app/api/analytics/route.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
27 changes: 16 additions & 11 deletions claim-db-worker/app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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 }
);
}

Expand Down
23 changes: 16 additions & 7 deletions claim-db-worker/app/api/claim/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
Expand Down
17 changes: 17 additions & 0 deletions claim-db-worker/lib/server/ratelimit.ts
Original file line number Diff line number Diff line change
@@ -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}`;
}
43 changes: 38 additions & 5 deletions create-db-worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,47 @@ export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
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') {
Expand Down