Skip to content
Merged
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
60 changes: 49 additions & 11 deletions src/app/api/bounties/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,75 @@ import { createClient } from "@/lib/supabase/server";
import { getAuthContext } from "@/lib/auth/get-user";
import { createBountySchema } from "@/lib/bounties";

// GET /api/bounties — public list of open bounties
const BOUNTY_STATUSES = ["open", "paused", "closed"] as const;
type BountyStatus = (typeof BOUNTY_STATUSES)[number];

// GET /api/bounties — public list of bounties
export async function GET(request: NextRequest) {
try {
const params = request.nextUrl.searchParams;
const status = params.get("status") || "open";
const defaultLimit = 50;
const statusParam = params.get("status") || "open";
if (!(BOUNTY_STATUSES as readonly string[]).includes(statusParam)) {
return NextResponse.json(
{ error: `Invalid status. Must be one of: ${BOUNTY_STATUSES.join(", ")}` },
{ status: 400 }
);
}
const status = statusParam as BountyStatus;

const defaultLimit = 50;
const limitRaw = Number(params.get("limit"));
if (params.get("limit") !== null && (!Number.isFinite(limitRaw) || limitRaw <= 0)) {
return NextResponse.json(
{ error: "Invalid limit. Must be a positive integer." },
{ status: 400 }
);
}
const limitCandidate =
Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : defaultLimit;
const limit = Math.min(limitCandidate, 100);

const pageRaw = Number(params.get("page"));
if (params.get("page") !== null && (!Number.isFinite(pageRaw) || pageRaw <= 0)) {
return NextResponse.json(
{ error: "Invalid page. Must be a positive integer." },
{ status: 400 }
);
Comment on lines +25 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Float values bypass "positive integer" validation

The validation rejects non-finite and non-positive values but accepts fractional numbers — ?limit=1.5 and ?page=2.7 both pass the guard and are silently floored to 1 and 2 respectively. The error message says "Must be a positive integer" but the actual contract accepted is any positive finite number. A caller following the error message would not expect floats to succeed, so the guard should also reject limitRaw !== Math.floor(limitRaw) (and the same for pageRaw).

}
const page = Number.isFinite(pageRaw) && pageRaw > 0 ? Math.floor(pageRaw) : 1;
const offset = (page - 1) * limit;

const supabase = await createClient();

const { data, error } = await supabase
const { data, error, count } = await supabase
.from("bounties" as any)
.select(
`
id, title, description, payout_usd, payout_currency, max_submissions,
status, closes_at, created_at,
id, title, description, payout_usd, payout_currency, payment_coin,
max_submissions, status, closes_at, questions, created_at, updated_at,
creator:profiles!creator_id (id, username, full_name, avatar_url)
`
`,
{ count: "exact" }
)
.eq("status", status)
.order("created_at", { ascending: false })
.range(offset, offset + limit - 1);

if (error) {
console.error("[GET /api/bounties] Supabase error:", error);
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ data: data || [] });
} catch {
return NextResponse.json({
data: data || [],
pagination: {
page,
limit,
total: count ?? 0,
total_pages: count ? Math.ceil(count / limit) : 0,
},
});
} catch (err) {
console.error("[GET /api/bounties] Unexpected error:", err);
return NextResponse.json({ error: "Unexpected error" }, { status: 500 });
}
}
Expand All @@ -57,7 +90,10 @@ export async function POST(request: NextRequest) {
const parsed = createBountySchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.issues[0].message },
{
error: parsed.error.issues[0].message,
issues: parsed.error.issues,
},
{ status: 400 }
);
}
Expand Down Expand Up @@ -85,10 +121,12 @@ export async function POST(request: NextRequest) {
.single();

if (error) {
console.error("[POST /api/bounties] Supabase error:", error);
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ data }, { status: 201 });
} catch {
} catch (err) {
console.error("[POST /api/bounties] Unexpected error:", err);
return NextResponse.json({ error: "Unexpected error" }, { status: 500 });
}
}
Loading