301 lines
10 KiB
TypeScript
301 lines
10 KiB
TypeScript
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"
|
|
import connectToMongoDB from "@/lib/mongoose"
|
|
import { Chapter } from "@/lib/models/chapter"
|
|
import { deleteR2ObjectByUrl } from "@/lib/r2"
|
|
|
|
function normalizeOptionalText(value: any): string {
|
|
return typeof value === "string" ? value.trim() : ""
|
|
}
|
|
|
|
async function resolveSeriesIdForWrite(
|
|
seriesIdInput: any,
|
|
seriesNameInput: any,
|
|
userRole: "USER" | "MOD" | "ADMIN",
|
|
userId: string
|
|
): Promise<string | null> {
|
|
const seriesId = normalizeOptionalText(seriesIdInput)
|
|
const seriesName = normalizeOptionalText(seriesNameInput)
|
|
|
|
if (seriesId) {
|
|
const series = await prisma.series.findFirst({
|
|
where: userRole === "ADMIN"
|
|
? { id: seriesId }
|
|
: {
|
|
id: seriesId,
|
|
OR: [
|
|
{ novels: { some: { uploaderId: userId } } },
|
|
{ novels: { some: { uploaderId: null } } },
|
|
{ novels: { none: {} } },
|
|
],
|
|
},
|
|
select: { id: true },
|
|
})
|
|
|
|
if (!series) {
|
|
throw new Error("Series không tồn tại hoặc bạn không có quyền sử dụng")
|
|
}
|
|
|
|
return series.id
|
|
}
|
|
|
|
if (!seriesName) return null
|
|
|
|
const existingSeries = await prisma.series.findFirst({
|
|
where: { name: { equals: seriesName, mode: "insensitive" } },
|
|
select: { id: true },
|
|
})
|
|
|
|
if (existingSeries) {
|
|
return existingSeries.id
|
|
}
|
|
|
|
const baseSlug = slugify(seriesName)
|
|
let slug = baseSlug
|
|
let counter = 1
|
|
|
|
while (await prisma.series.findUnique({ where: { slug } })) {
|
|
slug = `${baseSlug}-${counter}`
|
|
counter += 1
|
|
}
|
|
|
|
const createdSeries = await prisma.series.create({
|
|
data: {
|
|
name: seriesName,
|
|
slug,
|
|
},
|
|
select: { id: true },
|
|
})
|
|
|
|
return createdSeries.id
|
|
}
|
|
|
|
export async function GET() {
|
|
const session = await getServerSession(authOptions)
|
|
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
|
}
|
|
|
|
try {
|
|
const novels = await prisma.novel.findMany({
|
|
where: session.user.role === "ADMIN"
|
|
? undefined
|
|
: {
|
|
OR: [
|
|
{ uploaderId: session.user.id },
|
|
{ uploaderId: null },
|
|
],
|
|
},
|
|
include: {
|
|
series: {
|
|
select: { id: true, name: true, slug: true }
|
|
}
|
|
},
|
|
orderBy: { updatedAt: "desc" },
|
|
})
|
|
return NextResponse.json(novels)
|
|
} catch (error) {
|
|
return NextResponse.json({ error: "Failed to fetch novels" }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
export async function POST(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 { title, originalTitle, authorName, originalAuthorName, description, coverUrl, genreIds = [] } = data
|
|
const seriesId = await resolveSeriesIdForWrite(data?.seriesId, data?.seriesName, session.user.role, session.user.id)
|
|
// Tạo slug từ title
|
|
const slug = slugify(title)
|
|
|
|
const newNovel = await prisma.novel.create({
|
|
data: {
|
|
title,
|
|
originalTitle,
|
|
slug: slug,
|
|
authorName,
|
|
originalAuthorName,
|
|
description,
|
|
coverUrl,
|
|
seriesId,
|
|
uploaderId: session.user.id,
|
|
genres: {
|
|
create: genreIds.map((id: string) => ({
|
|
genre: { connect: { id } }
|
|
}))
|
|
}
|
|
},
|
|
})
|
|
return NextResponse.json(newNovel, { status: 201 })
|
|
} catch (error) {
|
|
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, originalTitle, authorName, originalAuthorName, description, coverUrl, status, genreIds } = data
|
|
const targetNovel = await prisma.novel.findFirst({
|
|
where: session.user.role === "ADMIN"
|
|
? { id }
|
|
: {
|
|
id,
|
|
OR: [
|
|
{ uploaderId: session.user.id },
|
|
{ uploaderId: null },
|
|
],
|
|
},
|
|
select: { id: true, seriesId: true },
|
|
})
|
|
|
|
if (!targetNovel) {
|
|
return NextResponse.json({ error: "Truyện không tồn tại hoặc không đủ quyền" }, { status: 404 })
|
|
}
|
|
|
|
// Disable editing series relation from novel edit form: keep current seriesId.
|
|
const fixedSeriesId = targetNovel.seriesId
|
|
|
|
if (fixedSeriesId) {
|
|
const seriesNovels = await prisma.novel.findMany({
|
|
where: { seriesId: fixedSeriesId },
|
|
select: { id: true },
|
|
})
|
|
const seriesNovelIds = seriesNovels.map((novel) => novel.id)
|
|
|
|
const updatedNovel = await prisma.$transaction(async (tx) => {
|
|
// Sync shared metadata for all novels in the same series.
|
|
await tx.novel.updateMany({
|
|
where: { id: { in: seriesNovelIds } },
|
|
data: {
|
|
originalTitle,
|
|
authorName,
|
|
originalAuthorName,
|
|
description,
|
|
status,
|
|
},
|
|
})
|
|
|
|
if (genreIds !== undefined) {
|
|
await tx.novelGenre.deleteMany({
|
|
where: { novelId: { in: seriesNovelIds } },
|
|
})
|
|
|
|
if (genreIds.length > 0) {
|
|
await tx.novelGenre.createMany({
|
|
data: seriesNovelIds.flatMap((novelId) =>
|
|
genreIds.map((genreId: string) => ({ novelId, genreId }))
|
|
),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Only current novel keeps its own title and cover.
|
|
return tx.novel.update({
|
|
where: { id },
|
|
data: {
|
|
title,
|
|
coverUrl,
|
|
...(session.user.role === "MOD" && { uploaderId: session.user.id }),
|
|
},
|
|
})
|
|
})
|
|
|
|
return NextResponse.json(updatedNovel)
|
|
}
|
|
|
|
const updatedNovel = await prisma.novel.update({
|
|
where: { id },
|
|
data: {
|
|
title,
|
|
originalTitle,
|
|
authorName,
|
|
originalAuthorName,
|
|
description,
|
|
coverUrl,
|
|
status,
|
|
seriesId: fixedSeriesId,
|
|
...(session.user.role === "MOD" && { uploaderId: session.user.id }),
|
|
...(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 })
|
|
|
|
const novel = await prisma.novel.findFirst({
|
|
where: session.user.role === "ADMIN"
|
|
? { id }
|
|
: {
|
|
id,
|
|
OR: [
|
|
{ uploaderId: session.user.id },
|
|
{ uploaderId: null },
|
|
],
|
|
},
|
|
select: { id: true, coverUrl: true, seriesId: true }
|
|
})
|
|
|
|
if (!novel) {
|
|
return NextResponse.json({ error: "Truyện không tồn tại hoặc không đủ quyền" }, { status: 404 })
|
|
}
|
|
|
|
await connectToMongoDB()
|
|
const chapterDeleteResult = await Chapter.deleteMany({ novelId: id })
|
|
|
|
await prisma.novel.delete({
|
|
where: { id },
|
|
})
|
|
|
|
await deleteR2ObjectByUrl(novel.coverUrl).catch(() => { })
|
|
|
|
if (novel.seriesId) {
|
|
const remainingSeriesNovels = await prisma.novel.count({ where: { seriesId: novel.seriesId } })
|
|
if (remainingSeriesNovels === 0) {
|
|
await prisma.series.delete({ where: { id: novel.seriesId } }).catch(() => { })
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
message: "Đã xóa truyện và toàn bộ chương thành công",
|
|
deletedChapters: chapterDeleteResult.deletedCount || 0
|
|
})
|
|
} catch (error) {
|
|
return NextResponse.json({ error: "Failed to delete novel" }, { status: 500 })
|
|
}
|
|
}
|