Spaces:
Configuration error
Configuration error
| 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 } | |
| ); | |
| } | |
| } | |