Add moderation APIs and admin UI

Add moderator/admin backend APIs and client features for managing novels and chapters. New endpoints include mod chapter routes (paginated list, single GET, PUT, DELETE, and bulk optimize), mod novel routes (create, GET by id, update, delete), genre CRUD, user bookmarks, novel comments, and rating endpoints. Update EPUB import to use a shared slugify util. Enhance moderator UI: chapter manager gains pagination, bulk optimization preview/apply, edit/delete dialogs; novel client adds genre management and edit/delete flows. Also update Prisma schema, add a DB wipe script, remove unused lib/data.ts, and adjust related types/utils and bookmark context.
This commit is contained in:
2026-03-06 17:30:56 +07:00
parent ce805adb08
commit 75ed8e233b
31 changed files with 1853 additions and 687 deletions
+70 -9
View File
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server"
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
import { slugify } from "@/lib/utils"
export async function GET() {
const session = await getServerSession(authOptions)
@@ -28,21 +29,22 @@ export async function POST(req: Request) {
try {
const data = await req.json()
const { title, authorName, description, genreIds = [] } = data
// Tạo slug từ title
const slug = data.title
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)+/g, "")
const slug = slugify(title)
const newNovel = await prisma.novel.create({
data: {
title: data.title,
title,
slug: slug,
authorName: data.authorName,
description: data.description,
authorName,
description,
uploaderId: session.user.id,
genres: {
create: genreIds.map((id: string) => ({
genre: { connect: { id } }
}))
}
},
})
return NextResponse.json(newNovel, { status: 201 })
@@ -50,3 +52,62 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Failed to create novel" }, { status: 500 })
}
}
export async function PUT(req: Request) {
const session = await getServerSession(authOptions)
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
try {
const data = await req.json()
const { id, title, authorName, description, status, genreIds } = data
// Update basic info and recreate genre relations
const updatedNovel = await prisma.novel.update({
where: { id: id, uploaderId: session.user.id }, // Make sure they own it
data: {
title,
authorName,
description,
status,
// Replace all existing genres if genreIds is provided
...(genreIds !== undefined && {
genres: {
deleteMany: {},
create: genreIds.map((gId: string) => ({
genre: { connect: { id: gId } }
}))
}
})
},
})
return NextResponse.json(updatedNovel)
} catch (error) {
return NextResponse.json({ error: "Failed to update novel" }, { status: 500 })
}
}
export async function DELETE(req: Request) {
const session = await getServerSession(authOptions)
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
try {
const url = new URL(req.url)
const id = url.searchParams.get("id")
if (!id) return NextResponse.json({ error: "Thiếu ID truyện" }, { status: 400 })
// Xóa truyện. (Chapters trong MongoDB nên được xóa bằng một cron job hoặc API khác để tránh block UI quá lâu,
// ở đây chúng ta chỉ xóa record của Postgres để ẩn truyện).
await prisma.novel.delete({
where: { id: id, uploaderId: session.user.id },
})
return NextResponse.json({ message: "Đã xóa truyện thành công" })
} catch (error) {
return NextResponse.json({ error: "Failed to delete novel" }, { status: 500 })
}
}