open-prompt / src /app /api /votes /route.ts
GitHub Action
Automated sync to Hugging Face
bcce530
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { getAuthUser } from "@/lib/auth";
import { voteSchema, parseBody } from "@/lib/validations";
import { checkRateLimit, getClientIdentifier, getRateLimitHeaders } from "@/lib/rate-limit";
// POST /api/votes — requires authentication
export async function POST(request: NextRequest) {
try {
// Auth check — no more trusting client-supplied userId
const authUser = await getAuthUser();
if (!authUser) {
return NextResponse.json(
{ error: "Authentication required" },
{ status: 401 }
);
}
// Rate limit
const identifier = getClientIdentifier(request, authUser.id);
const rateLimit = await checkRateLimit(`votes:${identifier}`, true);
if (!rateLimit.success) {
return NextResponse.json(
{ error: rateLimit.error },
{ status: 429, headers: Object.fromEntries(getRateLimitHeaders(rateLimit).entries()) }
);
}
const body = await request.json();
const parsed = parseBody(voteSchema, body);
if (!parsed.success) return parsed.response;
const { targetType, targetId, value } = parsed.data;
const userId = authUser.id;
// Clamp value to -1 or 1 (prevents inflation attacks)
const safeValue = value >= 0 ? 1 : -1;
// Handle different target types
if (targetType === "imagePrompt") {
const existingVote = await prisma.imagePromptVote.findUnique({
where: {
imagePromptId_userId: { imagePromptId: targetId, userId },
},
});
if (existingVote) {
// Toggle vote off — use transaction for consistency
await prisma.$transaction([
prisma.imagePromptVote.delete({ where: { id: existingVote.id } }),
prisma.imagePrompt.update({
where: { id: targetId },
data: { votesCount: { decrement: existingVote.value } },
}),
]);
return NextResponse.json({ voted: false, change: -existingVote.value });
} else {
await prisma.$transaction([
prisma.imagePromptVote.create({
data: { imagePromptId: targetId, userId, value: safeValue },
}),
prisma.imagePrompt.update({
where: { id: targetId },
data: { votesCount: { increment: safeValue } },
}),
]);
return NextResponse.json({ voted: true, change: safeValue });
}
}
if (targetType === "character") {
const existingVote = await prisma.characterVote.findUnique({
where: {
characterId_userId: { characterId: targetId, userId },
},
});
if (existingVote) {
await prisma.$transaction([
prisma.characterVote.delete({ where: { id: existingVote.id } }),
prisma.character.update({
where: { id: targetId },
data: { votesCount: { decrement: existingVote.value } },
}),
]);
return NextResponse.json({ voted: false, change: -existingVote.value });
} else {
await prisma.$transaction([
prisma.characterVote.create({
data: { characterId: targetId, userId, value: safeValue },
}),
prisma.character.update({
where: { id: targetId },
data: { votesCount: { increment: safeValue } },
}),
]);
return NextResponse.json({ voted: true, change: safeValue });
}
}
return NextResponse.json({ error: "Invalid targetType" }, { status: 400 });
} catch (error) {
console.error("Error processing vote:", error);
return NextResponse.json(
{ error: "Failed to process vote" },
{ status: 500 }
);
}
}
// GET /api/votes - Check if current user voted (uses auth, not query param)
export async function GET(request: NextRequest) {
try {
const authUser = await getAuthUser();
if (!authUser) {
return NextResponse.json({ hasVoted: false });
}
const { searchParams } = new URL(request.url);
const targetType = searchParams.get("targetType");
const targetId = searchParams.get("targetId");
if (!targetType || !targetId) {
return NextResponse.json(
{ error: "targetType and targetId are required" },
{ status: 400 }
);
}
const userId = authUser.id;
let hasVoted = false;
if (targetType === "imagePrompt") {
const vote = await prisma.imagePromptVote.findUnique({
where: { imagePromptId_userId: { imagePromptId: targetId, userId } },
});
hasVoted = !!vote;
}
if (targetType === "character") {
const vote = await prisma.characterVote.findUnique({
where: { characterId_userId: { characterId: targetId, userId } },
});
hasVoted = !!vote;
}
return NextResponse.json({ hasVoted });
} catch (error) {
console.error("Error checking vote:", error);
return NextResponse.json(
{ error: "Failed to check vote" },
{ status: 500 }
);
}
}