Skip to content
Closed
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
5 changes: 5 additions & 0 deletions src/app/api/models/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export async function GET(
return NextResponse.json({ error: "Model not found" }, { status: 404 });
}

// Prevent returning unverified models to the public API
if (!model.isVerified) {
return NextResponse.json({ error: "Model not found" }, { status: 404 });
}

return NextResponse.json({ data: model });
} catch (err) {
console.error("GET /api/models/[slug] error:", err);
Expand Down
40 changes: 23 additions & 17 deletions src/app/api/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@ export async function GET(request: Request) {
"inputPricePerMtok", "outputPricePerMtok", "speedToksPerSec", "createdAt",
];
const rawSort = searchParams.get("sort") ?? "";
const sort = allowedSorts.includes(rawSort) ? rawSort : "benchmarkGpqa";
const limit = Math.min(parseInt(searchParams.get("limit") || "50"), 100);
const offset = parseInt(searchParams.get("offset") || "0");
const sortValidated = allowedSorts.includes(rawSort) ? rawSort : "benchmarkGpqa";

if (!DB_ENABLED) {
// Fallback: filter mock data
Expand All @@ -63,8 +61,8 @@ export async function GET(request: Request) {
);
}
result.sort((a, b) => {
const aVal = (a as unknown as Record<string, unknown>)[sort];
const bVal = (b as unknown as Record<string, unknown>)[sort];
const aVal = (a as unknown as Record<string, unknown>)[sortValidated];
const bVal = (b as unknown as Record<string, unknown>)[sortValidated];
if (aVal === undefined || aVal === null) return 1;
if (bVal === undefined || bVal === null) return -1;
return (bVal as number) - (aVal as number);
Expand All @@ -87,8 +85,8 @@ export async function GET(request: Request) {
];
}

// sort is already validated against allowedSorts above.
const orderField = sort;
// sortValidated is already validated against allowedSorts above.
const orderField = sortValidated;

const [models, total] = await Promise.all([
prisma.model.findMany({
Expand Down Expand Up @@ -123,6 +121,11 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "name and provider are required" }, { status: 400 });
}

// Validate name length to prevent excessively long model names that may break UI rendering
if (typeof name !== "string" || name.trim().length === 0 || name.length > 200) {
return NextResponse.json({ error: "name must be a non-empty string between 1 and 200 characters" }, { status: 400 });
}

if (!DB_ENABLED) {
return NextResponse.json(
{ message: "Contribution received (DB not connected — configure DATABASE_URL to persist).", status: "pending" },
Expand Down Expand Up @@ -207,16 +210,19 @@ export async function POST(request: Request) {
},
});

// Create a feed event
await prisma.feedEvent.create({
data: {
userId: user.id,
eventType: "model_added",
entityType: "model",
entityId: model.id,
entityName: model.name,
},
});
// Only create a feed event when the model is verified and approved
// Pending/unverified submissions should not appear in the public activity feed
if (model.isVerified) {
await prisma.feedEvent.create({
data: {
userId: user.id,
eventType: "model_added",
entityType: "model",
entityId: model.id,
entityName: model.name,
},
});
}

return NextResponse.json(
{ message: "Model submitted for review.", data: model, status: "pending" },
Expand Down
38 changes: 34 additions & 4 deletions src/app/api/reviews/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,25 @@ export async function POST(request: Request) {
);
}

// Validate comment length to prevent oversized text from reaching the database.
// Validate and sanitize comment field to prevent spam submissions
let sanitizedComment = comment;
if (comment !== undefined && comment !== null) {
if (typeof comment !== "string") {
return NextResponse.json(
{ error: "comment must be a string." },
{ status: 400 }
);
}
if (comment.length > 2000) {
// Trim whitespace from both ends
sanitizedComment = comment.trim();
// Prevent all-whitespace submissions
if (sanitizedComment.length === 0 && comment.length > 0) {
return NextResponse.json(
{ error: "comment cannot contain only whitespace." },
{ status: 400 }
);
}
if (sanitizedComment.length > 2000) {
return NextResponse.json(
{ error: "comment must not exceed 2000 characters." },
{ status: 400 }
Expand All @@ -75,6 +85,26 @@ export async function POST(request: Request) {
);
}

// Verify the entity (model or tool) exists before allowing a review submission
// This prevents polluting the database with reviews for non-existent entities
let entityExists = false;
if (entityType === "model") {
const model = await prisma.model.findUnique({ where: { id: entityId } });
entityExists = !!model;
} else if (entityType === "tool") {
// Assuming tools also have a database representation
// Adjust this based on your actual data model
const tool = await prisma.tool.findUnique({ where: { id: entityId } });
entityExists = !!tool;
}

if (!entityExists) {
return NextResponse.json(
{ error: `${entityType} not found` },
{ status: 404 }
);
}

// Find or create user
const githubUsername = (session.user.name ?? session.user.email ?? "unknown").replace(/\s+/g, "-").toLowerCase();
const user = await prisma.user.upsert({
Expand All @@ -99,14 +129,14 @@ export async function POST(request: Request) {
},
update: {
rating: Math.round(rating),
comment: comment ?? undefined,
comment: sanitizedComment || undefined,
},
create: {
userId: user.id,
entityType,
entityId,
rating: Math.round(rating),
comment: comment ?? undefined,
comment: sanitizedComment || undefined,
},
});

Expand Down
Loading