Spaces:
Configuration error
Configuration error
| import { NextRequest, NextResponse } from "next/server"; | |
| import { prisma } from "@/lib/prisma"; | |
| import { getAuthUser } from "@/lib/auth"; | |
| import { createCommentSchema, updateCommentSchema, parseBody } from "@/lib/validations"; | |
| import { checkRateLimit, getClientIdentifier } from "@/lib/rate-limit"; | |
| import { sanitizeText } from "@/lib/api-utils"; | |
| // GET /api/comments?promptId=xxx — public, with pagination | |
| export async function GET(request: NextRequest) { | |
| try { | |
| const { searchParams } = new URL(request.url); | |
| const promptId = searchParams.get("promptId"); | |
| const limit = Math.min(parseInt(searchParams.get("limit") || "20"), 50); | |
| const offset = Math.max(parseInt(searchParams.get("offset") || "0"), 0); | |
| if (!promptId) { | |
| return NextResponse.json( | |
| { error: "promptId is required" }, | |
| { status: 400 } | |
| ); | |
| } | |
| const [comments, total] = await Promise.all([ | |
| prisma.comment.findMany({ | |
| where: { | |
| promptId, | |
| parentId: null, // Only top-level comments | |
| }, | |
| include: { | |
| user: { | |
| select: { | |
| id: true, | |
| name: true, | |
| username: true, | |
| image: true, | |
| rank: true, | |
| }, | |
| }, | |
| replies: { | |
| include: { | |
| user: { | |
| select: { | |
| id: true, | |
| name: true, | |
| username: true, | |
| image: true, | |
| rank: true, | |
| }, | |
| }, | |
| }, | |
| orderBy: { createdAt: "asc" }, | |
| take: 50, // Limit nested replies | |
| }, | |
| }, | |
| orderBy: { createdAt: "desc" }, | |
| take: limit, | |
| skip: offset, | |
| }), | |
| prisma.comment.count({ where: { promptId, parentId: null } }), | |
| ]); | |
| return NextResponse.json({ comments, total, hasMore: offset + comments.length < total }); | |
| } catch (error) { | |
| console.error("Error fetching comments:", error); | |
| return NextResponse.json( | |
| { error: "Failed to fetch comments" }, | |
| { status: 500 } | |
| ); | |
| } | |
| } | |
| // POST /api/comments — requires authentication + rate limited | |
| export async function POST(request: NextRequest) { | |
| try { | |
| 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(`comments:${identifier}`, true); | |
| if (!rateLimit.success) { | |
| return NextResponse.json({ error: rateLimit.error }, { status: 429 }); | |
| } | |
| const body = await request.json(); | |
| const parsed = parseBody(createCommentSchema, body); | |
| if (!parsed.success) return parsed.response; | |
| const { promptId, content, parentId } = parsed.data; | |
| // Sanitize content — strip HTML tags, limit length | |
| const sanitizedContent = sanitizeText(content, 5000); | |
| if (!sanitizedContent) { | |
| return NextResponse.json( | |
| { error: "Comment content cannot be empty" }, | |
| { status: 400 } | |
| ); | |
| } | |
| const comment = await prisma.comment.create({ | |
| data: { | |
| promptId, | |
| userId: authUser.id, | |
| content: sanitizedContent, | |
| parentId: parentId || null, | |
| }, | |
| include: { | |
| user: { | |
| select: { | |
| id: true, | |
| name: true, | |
| username: true, | |
| image: true, | |
| rank: true, | |
| }, | |
| }, | |
| }, | |
| }); | |
| return NextResponse.json(comment); | |
| } catch (error) { | |
| console.error("Error creating comment:", error); | |
| return NextResponse.json( | |
| { error: "Failed to create comment" }, | |
| { status: 500 } | |
| ); | |
| } | |
| } | |
| // PATCH /api/comments — requires authentication | |
| // Like/unlike uses a Redis-based per-user tracking to prevent spam | |
| export async function PATCH(request: NextRequest) { | |
| try { | |
| const authUser = await getAuthUser(); | |
| if (!authUser) { | |
| return NextResponse.json( | |
| { error: "Authentication required" }, | |
| { status: 401 } | |
| ); | |
| } | |
| const body = await request.json(); | |
| const parsed = parseBody(updateCommentSchema, body); | |
| if (!parsed.success) return parsed.response; | |
| const { commentId, action, content } = parsed.data; | |
| if (action === "like" || action === "unlike") { | |
| if (action === "like") { | |
| try { | |
| // Create a like record — unique constraint prevents double-liking | |
| await prisma.commentLike.create({ | |
| data: { commentId, userId: authUser.id }, | |
| }) | |
| } catch { | |
| // Unique constraint violation = already liked | |
| return NextResponse.json({ error: "Already liked" }, { status: 409 }) | |
| } | |
| // Return updated like count | |
| const count = await prisma.commentLike.count({ where: { commentId } }) | |
| return NextResponse.json({ liked: true, likes: count }) | |
| } | |
| if (action === "unlike") { | |
| const deleted = await prisma.commentLike.deleteMany({ | |
| where: { commentId, userId: authUser.id }, | |
| }) | |
| if (deleted.count === 0) { | |
| return NextResponse.json({ error: "Not liked" }, { status: 409 }) | |
| } | |
| const count = await prisma.commentLike.count({ where: { commentId } }) | |
| return NextResponse.json({ liked: false, likes: count }) | |
| } | |
| } | |
| if (action === "edit" && content) { | |
| // Verify ownership before editing | |
| const existingComment = await prisma.comment.findUnique({ | |
| where: { id: commentId }, | |
| select: { userId: true }, | |
| }); | |
| if (!existingComment || existingComment.userId !== authUser.id) { | |
| return NextResponse.json( | |
| { error: "You can only edit your own comments" }, | |
| { status: 403 } | |
| ); | |
| } | |
| const sanitizedContent = sanitizeText(content, 5000); | |
| const comment = await prisma.comment.update({ | |
| where: { id: commentId }, | |
| data: { content: sanitizedContent }, | |
| }); | |
| return NextResponse.json(comment); | |
| } | |
| return NextResponse.json( | |
| { error: "Invalid action" }, | |
| { status: 400 } | |
| ); | |
| } catch (error) { | |
| console.error("Error updating comment:", error); | |
| return NextResponse.json( | |
| { error: "Failed to update comment" }, | |
| { status: 500 } | |
| ); | |
| } | |
| } | |
| // DELETE /api/comments — requires authentication, delete only own comments | |
| export async function DELETE(request: NextRequest) { | |
| try { | |
| const authUser = await getAuthUser(); | |
| if (!authUser) { | |
| return NextResponse.json( | |
| { error: "Authentication required" }, | |
| { status: 401 } | |
| ); | |
| } | |
| const { searchParams } = new URL(request.url); | |
| const commentId = searchParams.get("commentId"); | |
| if (!commentId) { | |
| return NextResponse.json( | |
| { error: "commentId is required" }, | |
| { status: 400 } | |
| ); | |
| } | |
| // Verify ownership before deleting | |
| const existingComment = await prisma.comment.findUnique({ | |
| where: { id: commentId }, | |
| select: { userId: true }, | |
| }); | |
| if (!existingComment || existingComment.userId !== authUser.id) { | |
| return NextResponse.json( | |
| { error: "You can only delete your own comments" }, | |
| { status: 403 } | |
| ); | |
| } | |
| await prisma.comment.delete({ | |
| where: { id: commentId }, | |
| }); | |
| return NextResponse.json({ success: true }); | |
| } catch (error) { | |
| console.error("Error deleting comment:", error); | |
| return NextResponse.json( | |
| { error: "Failed to delete comment" }, | |
| { status: 500 } | |
| ); | |
| } | |
| } | |