copilot_work_3 / src /app /api /tools /route.ts
arshvinw5's picture
add rate limit to submittions
416bdd1
Raw History Blame
3.03 kB
import { NextResponse } from "next/server";
import { z } from "zod";
import { categories } from "@/lib/tool-types";
import { parseGithubRepoUrl, verifyRepo } from "@/lib/github";
import { checkRateLimit, getClientIp, rateLimitResponse } from "@/lib/rate-limit";
import { DuplicateRepoError, submitTool } from "@/lib/tools";
const SUBMISSIONS_PER_HOUR = 3;
const githubRepoUrl = z
.string()
.url()
.refine((value) => parseGithubRepoUrl(value) !== undefined, "Enter a public GitHub repository URL.");
const optionalWebsiteUrl = z
.union([z.literal(""), z.string().url()])
.refine((value) => {
if (!value) return true;
const url = new URL(value);
return url.protocol === "https:" && !url.username && !url.password;
}, "Website must be a public HTTPS URL.");
const submissionSchema = z.object({
name: z.string().trim().min(2).max(80),
summary: z.string().trim().min(20).max(300),
category: z.enum(categories),
repoUrl: githubRepoUrl,
websiteUrl: optionalWebsiteUrl,
submittedBy: z.string().trim().min(2).max(60),
});
export async function POST(request: Request) {
const limit = checkRateLimit(`tools:${getClientIp(request)}`, SUBMISSIONS_PER_HOUR, 60 * 60 * 1000);
if (!limit.allowed) return rateLimitResponse(limit);
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: "Request body must be valid JSON." }, { status: 400 });
}
const parsed = submissionSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json(
{ error: "Please review the submission fields and try again.", fields: parsed.error.flatten().fieldErrors },
{ status: 400 },
);
}
const repo = parseGithubRepoUrl(parsed.data.repoUrl);
if (!repo) {
return NextResponse.json({ error: "Enter a public GitHub repository URL." }, { status: 400 });
}
// A failed lookup (rate limit, timeout) must not block the submission: it stays
// pending and a moderator checks it. Only a definite "not found" is rejected.
const verification = await verifyRepo(repo);
if (verification.status === "not_found") {
return NextResponse.json(
{
error: "That GitHub repository could not be found. Make sure it exists and is public.",
fields: { repoUrl: ["That GitHub repository could not be found. Make sure it exists and is public."] },
},
{ status: 422 },
);
}
try {
await submitTool(parsed.data);
return NextResponse.json(
{ status: "pending", message: "Thanks! Your submission is awaiting review." },
{ status: 201 },
);
} catch (error) {
if (error instanceof DuplicateRepoError) {
return NextResponse.json(
{
error: error.message,
fields: { repoUrl: [error.message] },
},
{ status: 409 },
);
}
console.error("Tool submission failed", error);
return NextResponse.json(
{ error: "The tool could not be saved. Please try again later." },
{ status: 500 },
);
}
}