From 1a1887d664d07892e0310248dbb81ddc491f5a11 Mon Sep 17 00:00:00 2001 From: AI Worker Date: Sat, 13 Jun 2026 07:08:58 +0200 Subject: [PATCH 1/2] Fix referral quota duplicate recipients --- src/app/api/referrals/route.ts | 45 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/app/api/referrals/route.ts b/src/app/api/referrals/route.ts index 6a27788a..b0b72170 100644 --- a/src/app/api/referrals/route.ts +++ b/src/app/api/referrals/route.ts @@ -100,9 +100,27 @@ export async function POST(request: NextRequest) { ); } - // Spam throttling: max 50 invites per day, max 10 per hour - // Only count valid emails toward rate limits (#143) const svc = createServiceClient(); + + // Prevent duplicate invites to same email before quota math. + const { data: existingInvites } = await (svc as AnySupabase) + .from("referrals") + .select("referred_email") + .eq("referrer_id", user.id) + .in("referred_email", validEmails); + + const alreadyInvited = new Set((existingInvites || []).map((r: any) => r.referred_email)); + const newValidEmails = validEmails.filter((e: string) => !alreadyInvited.has(e)); + + if (newValidEmails.length === 0) { + return NextResponse.json( + { error: "All these emails have already been invited" }, + { status: 400 } + ); + } + + // Spam throttling: max 50 invites per day, max 10 per hour. + // Only new valid emails should count toward rate limits. const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString(); const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); @@ -112,7 +130,7 @@ export async function POST(request: NextRequest) { .eq("referrer_id", user.id) .gte("created_at", oneHourAgo); - if ((hourlyCount ?? 0) + validEmails.length > 10) { + if ((hourlyCount ?? 0) + newValidEmails.length > 10) { return NextResponse.json( { error: "Too many invites. Max 10 per hour." }, { status: 429 } @@ -125,22 +143,13 @@ export async function POST(request: NextRequest) { .eq("referrer_id", user.id) .gte("created_at", oneDayAgo); - if ((dailyCount ?? 0) + validEmails.length > 50) { + if ((dailyCount ?? 0) + newValidEmails.length > 50) { return NextResponse.json( { error: "Daily invite limit reached. Max 50 per day." }, { status: 429 } ); } - // Prevent duplicate invites to same email - const { data: existingInvites } = await (svc as AnySupabase) - .from("referrals") - .select("referred_email") - .eq("referrer_id", user.id) - .in("referred_email", validEmails); - - const alreadyInvited = new Set((existingInvites || []).map((r: any) => r.referred_email)); - // Get user's referral code const { data: profile } = await (supabase as any) .from("profiles") @@ -155,16 +164,6 @@ export async function POST(request: NextRequest) { const referralCode = profile.referral_code || profile.username; const inviterName = profile.full_name || profile.username || "Someone"; - // Filter valid emails that aren't already invited (#143) - const newValidEmails = validEmails.filter((e: string) => !alreadyInvited.has(e)); - - if (newValidEmails.length === 0) { - return NextResponse.json( - { error: "All these emails have already been invited" }, - { status: 400 } - ); - } - // Send emails BEFORE inserting into DB to avoid partial-state issues const emailContent = referralInviteEmail({ inviterName, referralCode }); const emailResults = await Promise.all( From 633c986d19229e2be2c1dd77f3dc517f977b5fcf Mon Sep 17 00:00:00 2001 From: AI Worker Date: Sun, 14 Jun 2026 03:16:45 +0200 Subject: [PATCH 2/2] Handle referral duplicate lookup errors --- src/app/api/referrals/route.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/api/referrals/route.ts b/src/app/api/referrals/route.ts index b0b72170..2070c6b6 100644 --- a/src/app/api/referrals/route.ts +++ b/src/app/api/referrals/route.ts @@ -103,12 +103,20 @@ export async function POST(request: NextRequest) { const svc = createServiceClient(); // Prevent duplicate invites to same email before quota math. - const { data: existingInvites } = await (svc as AnySupabase) + const { data: existingInvites, error: existingInvitesError } = await (svc as AnySupabase) .from("referrals") .select("referred_email") .eq("referrer_id", user.id) .in("referred_email", validEmails); + if (existingInvitesError) { + console.error("[REFERRALS] Duplicate invite lookup failed:", existingInvitesError); + return NextResponse.json( + { error: "Failed to check existing invites" }, + { status: 500 } + ); + } + const alreadyInvited = new Set((existingInvites || []).map((r: any) => r.referred_email)); const newValidEmails = validEmails.filter((e: string) => !alreadyInvited.has(e));