Spaces:
Configuration error
Configuration error
File size: 5,041 Bytes
bcce530 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | 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 }
);
}
}
|