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 } ); } }