-
Notifications
You must be signed in to change notification settings - Fork 37
feat: add structured submission form with draft saving and success state #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 }, | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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 }, | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TOCTOU race between duplicate check and write allows double submissions. Under concurrent requests from the same contributor, both can pass the The 💡 Suggested mitigation (store-level guard)- BountyStore.addSubmission(submission);
+ const added = BountyStore.addSubmissionIfNotDuplicate(submission);
+ if (!added) {
+ return NextResponse.json({ error: "Duplicate submission" }, { status: 409 });
+ }Move the uniqueness check inside the store's 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| 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, | ||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, find and read the route file
find . -name "route.ts" -path "*bounties*submit*" 2>/dev/null | head -5Repository: boundlessfi/bounties Length of output: 103 🏁 Script executed: # Search for the file mentioned in the review
fd -e ts "route" | grep -E "bounties.*submit" | head -10Repository: boundlessfi/bounties Length of output: 101 🏁 Script executed: # Try a broader search
git ls-files | grep -E "(bounties|submit)" | head -20Repository: boundlessfi/bounties Length of output: 602 🏁 Script executed: # Search for getCurrentUser implementation
rg "getCurrentUser" --type ts -lRepository: boundlessfi/bounties Length of output: 486 🏁 Script executed: # Read the submit route file
cat -n app/api/bounties/[id]/submit/route.tsRepository: boundlessfi/bounties Length of output: 3418 🏁 Script executed: # Read the server-auth file to understand getCurrentUser and User type
cat -n lib/server-auth.tsRepository: boundlessfi/bounties Length of output: 1132
The submission currently accepts However, the
The current implementation creates a security vulnerability where reward payments can be intercepted by submitting a different wallet address. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| 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 }, | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+90
to
+95
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Catch block silently swallows errors — add logging. Any unexpected exception disappears with no server-side trace. This makes diagnosing production failures nearly impossible. 🛠️ Proposed fix- } catch {
+ } catch (error) {
+ console.error("[POST /api/bounties/:id/submit] Unexpected error:", error);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.