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