Skip to content
Closed
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
96 changes: 8 additions & 88 deletions src/app/api/referrals/route.ts
Original file line number Diff line number Diff line change
@@ -1,88 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { getAuthContext } from "@/lib/auth/get-user";
import { referralInviteEmail, sendEmail } from "@/lib/email";
import { createServiceClient } from "@/lib/supabase/service";

type AnySupabase = any;

// GET /api/referrals - List my referrals
export async function GET(request: NextRequest) {
try {
const auth = await getAuthContext(request);
if (!auth) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { user, supabase } = auth;

const { data: referrals, error } = await (supabase as AnySupabase)
.from("referrals")
.select("*")
.eq("referrer_id", user.id)
.order("created_at", { ascending: false });

if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}

const total = referrals?.length || 0;
const registered = referrals?.filter((r: any) => r.status !== "pending").length || 0;

return NextResponse.json({
data: referrals,
stats: {
total_invited: total,
total_registered: registered,
conversion_rate: total > 0 ? Math.round((registered / total) * 100) : 0,
},
});
} catch {
return NextResponse.json(
{ error: "An unexpected error occurred" },
{ status: 500 }
);
}
}

// POST /api/referrals - Send invites
export async function POST(request: NextRequest) {
try {
const auth = await getAuthContext(request);
if (!auth) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { user, supabase } = auth;

const body = await request.json();
const { emails } = body;

if (!emails || !Array.isArray(emails) || emails.length === 0) {
return NextResponse.json(
{ error: "Please provide an array of emails" },
{ status: 400 }
);
}

if (!emails.every((email: unknown) => typeof email === "string")) {
return NextResponse.json(
{ error: "All email entries must be strings" },
{ status: 400 }
);
}

if (emails.length > 20) {
return NextResponse.json(
{ error: "Maximum 20 invites at a time" },
{ status: 400 }
);
}

// Validate email syntax BEFORE rate-limit checks (#143)
// Only valid emails should count toward throttle limits
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const normalizedEmails = emails.map((e: string) => e.trim().toLowerCase());
const validEmails = normalizedEmails.filter((e: string) => emailRegex.test(e));
const validEmails = normalizedEmails.filter((e: string) => emailRegex.test(e));
const userEmail = user.email?.toLowerCase();
const filteredEmails = validEmails.filter((e: string) => e !== userEmail);
Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Entire route file is broken — imports, GET handler, and POST function declaration are all gone

The diff replaced lines 1–88 of the original file (all imports, the full GET handler, and the top ~80 lines of the POST handler including the function declaration, auth check, body parsing, and all early validation) with just 3 lines that reference variables that no longer exist: normalizedEmails, emailRegex, user, supabase, NextResponse, createServiceClient, referralInviteEmail, and sendEmail are all used later in the file but are never imported or declared. The module also no longer exports GET or POST, so Next.js will not route any requests to it. This file cannot compile and the entire /api/referrals endpoint is dead.


if (validEmails.length === 0) {
if (filteredEmails.length === 0) {
return NextResponse.json(
{ error: "No valid email addresses provided" },
{ status: 400 }
Expand All @@ -101,7 +21,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) + filteredEmails.length > 10) {
return NextResponse.json(
{ error: "Too many invites. Max 10 per hour." },
{ status: 429 }
Expand All @@ -114,7 +34,7 @@ export async function POST(request: NextRequest) {
.eq("referrer_id", user.id)
.gte("created_at", oneDayAgo);

if ((dailyCount ?? 0) + validEmails.length > 50) {
if ((dailyCount ?? 0) + filteredEmails.length > 50) {
return NextResponse.json(
{ error: "Daily invite limit reached. Max 50 per day." },
{ status: 429 }
Expand All @@ -126,7 +46,7 @@ export async function POST(request: NextRequest) {
.from("referrals")
.select("referred_email")
.eq("referrer_id", user.id)
.in("referred_email", validEmails);
.in("referred_email", filteredEmails);

const alreadyInvited = new Set((existingInvites || []).map((r: any) => r.referred_email));

Expand All @@ -145,7 +65,7 @@ export async function POST(request: NextRequest) {
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));
const newValidEmails = filteredEmails.filter((e: string) => !alreadyInvited.has(e));

if (newValidEmails.length === 0) {
return NextResponse.json(
Expand Down
Loading