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 }); } } diff --git a/src/app/api/reviews/route.ts b/src/app/api/reviews/route.ts index 58e7421a..ee4cea0a 100644 --- a/src/app/api/reviews/route.ts +++ b/src/app/api/reviews/route.ts @@ -21,8 +21,47 @@ export async function GET(request: NextRequest) { const supabase = await createClient(); const { searchParams } = new URL(request.url); const gigId = searchParams.get("gig_id"); - const limit = parsePaginationParam(searchParams.get("limit"), 20, 1, 50); - const offset = parsePaginationParam(searchParams.get("offset"), 0, 0, 100_000); + + // Validate gig_id as UUID if provided + if (gigId) { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(gigId)) { + return NextResponse.json( + { error: "Invalid gig_id. Must be a valid UUID." }, + { status: 400 } + ); + } + } + + // Validate limit + const MAX_REVIEW_LIMIT = 50; + const MAX_REVIEW_OFFSET = 100_000; + const limitRaw = searchParams.get("limit"); + let limit = 20; + if (limitRaw !== null) { + const v = Number(limitRaw); + if (!Number.isFinite(v) || !Number.isInteger(v) || v < 1) { + return NextResponse.json( + { error: "Invalid limit. Must be a positive integer (>= 1)." }, + { status: 400 } + ); + } + limit = Math.min(v, MAX_REVIEW_LIMIT); + } + + // Validate offset + const offsetRaw = searchParams.get("offset"); + let offset = 0; + if (offsetRaw !== null) { + const v = Number(offsetRaw); + if (!Number.isFinite(v) || !Number.isInteger(v) || v < 0) { + return NextResponse.json( + { error: "Invalid offset. Must be a non-negative integer (>= 0)." }, + { status: 400 } + ); + } + offset = Math.min(v, MAX_REVIEW_OFFSET); + } let query = supabase .from("reviews") @@ -58,6 +97,7 @@ export async function GET(request: NextRequest) { const { data: reviews, error, count } = await query; if (error) { + console.error("[GET /api/reviews] Supabase error:", error); return NextResponse.json({ error: error.message }, { status: 400 }); } @@ -69,7 +109,8 @@ export async function GET(request: NextRequest) { offset, }, }); - } catch { + } catch (err) { + console.error("[GET /api/reviews] Unexpected error:", err); return NextResponse.json( { error: "An unexpected error occurred" }, { status: 500 } @@ -91,7 +132,7 @@ export async function POST(request: NextRequest) { if (!validationResult.success) { return NextResponse.json( - { error: validationResult.error.issues[0].message }, + { error: "Invalid review payload", issues: validationResult.error.issues }, { status: 400 } ); } @@ -203,6 +244,7 @@ export async function POST(request: NextRequest) { .single(); if (createError) { + console.error("[POST /api/reviews] Supabase error:", createError); return NextResponse.json({ error: createError.message }, { status: 400 }); }