open-prompt / src /app /api /forum /route.ts
GitHub Action
Automated sync to Hugging Face
bcce530
import { NextRequest, NextResponse } from "next/server"
import prisma from "@/lib/prisma"
import { getAuthUser } from "@/lib/auth"
import { createForumPostSchema, parseBody } from "@/lib/validations"
import { checkRateLimit, getClientIdentifier } from "@/lib/rate-limit"
import { sanitizeText } from "@/lib/api-utils"
const FORUM_CATEGORIES = [
"general",
"help",
"showcase",
"feature-request",
"tips",
]
// GET - List forum posts (uses Prisma include instead of separate user query)
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const category = searchParams.get("category")
const search = searchParams.get("search")
const sort = searchParams.get("sort") || "recent"
const limit = Math.min(parseInt(searchParams.get("limit") || "20"), 100)
const offset = Math.max(parseInt(searchParams.get("offset") || "0"), 0)
// Build where clause
const whereClause: Record<string, unknown> = {}
if (category && category !== "all") {
whereClause.category = category
}
if (search) {
whereClause.OR = [
{ title: { contains: search, mode: "insensitive" } },
{ content: { contains: search, mode: "insensitive" } },
]
}
// Build order by
let orderBy: Record<string, string>[] = [{ createdAt: "desc" }]
if (sort === "popular") {
orderBy = [{ views: "desc" }, { likes: "desc" }]
} else if (sort === "unanswered") {
whereClause.replies = { none: {} }
}
// Use Prisma include to get user data in single query (fixes N+1)
const [posts, total] = await Promise.all([
prisma.forumPost.findMany({
where: whereClause,
orderBy,
take: limit,
skip: offset,
include: {
user: {
select: {
id: true,
name: true,
username: true,
image: true,
},
},
_count: {
select: { replies: true }
}
}
}),
prisma.forumPost.count({ where: whereClause }),
])
const postsWithCounts = posts.map(post => ({
...post,
replyCount: post._count.replies,
}))
return NextResponse.json({
posts: postsWithCounts,
total,
categories: FORUM_CATEGORIES,
hasMore: offset + posts.length < total,
}, {
headers: {
'Cache-Control': 'public, s-maxage=30, stale-while-revalidate=120',
},
})
} catch (error) {
console.error("Error fetching forum posts:", error)
return NextResponse.json(
{ error: "Failed to fetch posts" },
{ status: 500 }
)
}
}
// POST - Create a new forum post — requires auth, rate limited, validated, sanitized
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(`forum:${identifier}`, true)
if (!rateLimit.success) {
return NextResponse.json({ error: rateLimit.error }, { status: 429 })
}
const body = await request.json()
const parsed = parseBody(createForumPostSchema, body)
if (!parsed.success) return parsed.response
const { title, content, category } = parsed.data
// Sanitize content (strip HTML)
const sanitizedContent = sanitizeText(content, 20000)
const sanitizedTitle = sanitizeText(title, 300)
const post = await prisma.forumPost.create({
data: {
title: sanitizedTitle,
content: sanitizedContent,
category,
userId: authUser.id,
},
})
return NextResponse.json(post)
} catch (error) {
console.error("Error creating forum post:", error)
return NextResponse.json(
{ error: "Failed to create post" },
{ status: 500 }
)
}
}