diff --git a/src/app/api/bounties/route.ts b/src/app/api/bounties/route.ts index 883d98b8..1ba8612f 100644 --- a/src/app/api/bounties/route.ts +++ b/src/app/api/bounties/route.ts @@ -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 } + ); + } 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 }); } } @@ -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 } ); } @@ -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 }); } }