Skip to content
Open
Show file tree
Hide file tree
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
112 changes: 73 additions & 39 deletions app/api/bounties/[id]/submit/route.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,89 @@
import { NextResponse } from 'next/server';
import { BountyStore } from '@/lib/store';
import { Submission } from '@/types/participation';
import { NextResponse } from "next/server";
import { BountyStore } from "@/lib/store";
import { Submission } from "@/types/participation";
import { submissionFormSchema } from "@/components/bounty/forms/schemas";

const generateId = () => crypto.randomUUID();

export async function POST(
request: Request,
{ params }: { params: Promise<{ id: string }> }
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id: bountyId } = await params;
const { id: bountyId } = await params;

try {
const body = await request.json();
const { contributorId, content } = body;
try {
const body = await request.json();
const { contributorId, ...formData } = body;

if (!contributorId || !content) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
if (!contributorId) {
return NextResponse.json(
{ error: "Missing contributor ID" },
{ status: 400 },
);
}

const bounty = BountyStore.getBountyById(bountyId);
if (!bounty) {
return NextResponse.json({ error: 'Bounty not found' }, { status: 404 });
}
const parsed = submissionFormSchema.safeParse(formData);
if (!parsed.success) {
const fieldErrors = parsed.error.flatten().fieldErrors;
return NextResponse.json(
{ error: "Validation failed", fieldErrors },
{ status: 400 },
);
}

const allowedModels = ['single-claim', 'competition', 'multi-winner', 'application'];
if (!allowedModels.includes(bounty.claimingModel)) {
return NextResponse.json({ error: 'Submission not allowed for this bounty type' }, { status: 400 });
}
const bounty = BountyStore.getBountyById(bountyId);
if (!bounty) {
return NextResponse.json({ error: "Bounty not found" }, { status: 404 });
}

const existingSubmission = BountyStore.getSubmissionsByBounty(bountyId).find(
s => s.contributorId === contributorId
);
const allowedModels = [
"single-claim",
"competition",
"multi-winner",
"application",
];
if (!allowedModels.includes(bounty.claimingModel)) {
return NextResponse.json(
{ error: "Submission not allowed for this bounty type" },
{ status: 400 },
);
}
Comment on lines +34 to +50
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing bounty status check — submissions can be made to closed/claimed bounties.

The route verifies the bounty exists and checks the claiming model, but never validates bounty.status === "open". The frontend disables the button for non-open bounties, but a direct API call can bypass this, allowing submissions to closed or already-claimed bounties.

🐛 Proposed fix
     const bounty = BountyStore.getBountyById(bountyId);
     if (!bounty) {
       return NextResponse.json({ error: "Bounty not found" }, { status: 404 });
     }
 
+    if (bounty.status !== "open") {
+      return NextResponse.json(
+        { error: "This bounty is no longer accepting submissions" },
+        { status: 400 },
+      );
+    }
+
     const allowedModels = [
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/submit/route.ts around lines 34 - 50, The route
currently validates existence and claiming model (BountyStore.getBountyById,
allowedModels and bounty.claimingModel) but never checks bounty.status, so
submissions can be made to closed/claimed bounties; add a guard after fetching
the bounty that verifies bounty.status === "open" (or an allowed set of
statuses) and return a 400/409 JSON error (e.g., "Bounty is not open for
submissions") if the status is not open, keeping this check before any further
validation or processing.


if (existingSubmission) {
return NextResponse.json({ error: 'Duplicate submission' }, { status: 409 });
}
const existingSubmission = BountyStore.getSubmissionsByBounty(
bountyId,
).find((s) => s.contributorId === contributorId);

const submission: Submission = {
id: generateId(),
bountyId,
contributorId,
content,
status: 'pending',
submittedAt: new Date().toISOString(),
};
if (existingSubmission) {
return NextResponse.json(
{ error: "Duplicate submission" },
{ status: 409 },
);
}

BountyStore.addSubmission(submission);
const { explanation, walletAddress, githubUrl, demoUrl, attachments } =
parsed.data;

return NextResponse.json({ success: true, data: submission });
const submission: Submission = {
id: generateId(),
bountyId,
contributorId,
content: explanation,
explanation,
walletAddress,
githubUrl: githubUrl || undefined,
demoUrl: demoUrl || undefined,
attachments: attachments?.length ? attachments : undefined,
status: "pending",
submittedAt: new Date().toISOString(),
};

} catch {
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
BountyStore.addSubmission(submission);

return NextResponse.json({ success: true, data: submission });
} catch {
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
}
Loading
Loading