Initial reader-api backend extracted from reader
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function GET(
|
||||
req: Request,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await context.params
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
try {
|
||||
const novel = await prisma.novel.findFirst({
|
||||
where: session.user.role === "ADMIN"
|
||||
? { id }
|
||||
: {
|
||||
id,
|
||||
OR: [
|
||||
{ uploaderId: session.user.id },
|
||||
{ uploaderId: null },
|
||||
],
|
||||
},
|
||||
include: {
|
||||
series: true,
|
||||
genres: {
|
||||
include: {
|
||||
genre: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
return NextResponse.json({ error: "Novel not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(novel)
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Failed to fetch novel details" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: novelId } = await params
|
||||
|
||||
try {
|
||||
const novel = await prisma.novel.findUnique({
|
||||
where: { id: novelId },
|
||||
select: { trashWords: true, uploaderId: true }
|
||||
})
|
||||
|
||||
if (!novel) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
if (session.user.role !== "ADMIN" && novel.uploaderId !== session.user.id) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ trashWords: novel.trashWords })
|
||||
} catch (error) {
|
||||
console.error("GET Trash Words Error:", error)
|
||||
return NextResponse.json({ error: "Lỗi Server" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const { id: novelId } = await params
|
||||
|
||||
try {
|
||||
const novel = await prisma.novel.findUnique({
|
||||
where: { id: novelId },
|
||||
select: { id: true, uploaderId: true }
|
||||
})
|
||||
|
||||
if (!novel) return NextResponse.json({ error: "Not found" }, { status: 404 })
|
||||
|
||||
if (session.user.role !== "ADMIN" && novel.uploaderId !== session.user.id) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { trashWords } = body
|
||||
|
||||
if (!Array.isArray(trashWords)) {
|
||||
return NextResponse.json({ error: "Mảng từ rác không hợp lệ" }, { status: 400 })
|
||||
}
|
||||
|
||||
const updated = await prisma.novel.update({
|
||||
where: { id: novelId },
|
||||
data: { trashWords }
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, trashWords: updated.trashWords })
|
||||
} catch (error) {
|
||||
console.error("PUT Trash Words Error:", error)
|
||||
return NextResponse.json({ error: "Lỗi Server" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import connectToMongoDB from "@/lib/mongoose"
|
||||
import { Chapter } from "@/lib/models/chapter"
|
||||
import { deleteR2ObjectByUrl } from "@/lib/r2"
|
||||
|
||||
function normalizeIds(value: any): string[] {
|
||||
if (!Array.isArray(value)) return []
|
||||
return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim())
|
||||
}
|
||||
|
||||
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 body = await req.json()
|
||||
const action = typeof body?.action === "string" ? body.action : ""
|
||||
const ids = normalizeIds(body?.ids)
|
||||
|
||||
if (ids.length === 0) {
|
||||
return NextResponse.json({ error: "Danh sách truyện trống" }, { status: 400 })
|
||||
}
|
||||
|
||||
const accessibleNovels = await prisma.novel.findMany({
|
||||
where: session.user.role === "ADMIN"
|
||||
? { id: { in: ids } }
|
||||
: {
|
||||
id: { in: ids },
|
||||
OR: [
|
||||
{ uploaderId: session.user.id },
|
||||
{ uploaderId: null },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
coverUrl: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (accessibleNovels.length === 0) {
|
||||
return NextResponse.json({ error: "Không có truyện hợp lệ để thao tác" }, { status: 404 })
|
||||
}
|
||||
|
||||
const accessibleIds = accessibleNovels.map((novel) => novel.id)
|
||||
|
||||
if (action === "delete") {
|
||||
await connectToMongoDB()
|
||||
await Chapter.deleteMany({ novelId: { $in: accessibleIds } })
|
||||
|
||||
await prisma.novel.deleteMany({
|
||||
where: { id: { in: accessibleIds } },
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
accessibleNovels.map((novel) => deleteR2ObjectByUrl(novel.coverUrl).catch(() => {}))
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true, deletedCount: accessibleIds.length })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "Chỉ hỗ trợ xóa hàng loạt" }, { status: 400 })
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Bulk operation failed" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
|
||||
type MissingKey = "author" | "cover" | "description" | "genres"
|
||||
|
||||
const ALL_MISSING_KEYS: MissingKey[] = ["author", "cover", "description", "genres"]
|
||||
|
||||
function getScopeWhere(session: { user: { role: string; id: string } }) {
|
||||
if (session.user.role === "ADMIN") {
|
||||
return {}
|
||||
}
|
||||
|
||||
return {
|
||||
OR: [
|
||||
{ uploaderId: session.user.id },
|
||||
{ uploaderId: null },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function parseMissingKeys(raw: string | null): MissingKey[] {
|
||||
if (!raw || !raw.trim()) return ALL_MISSING_KEYS
|
||||
|
||||
const parsed = raw
|
||||
.split(",")
|
||||
.map((item) => item.trim().toLowerCase())
|
||||
.filter((item): item is MissingKey => ALL_MISSING_KEYS.includes(item as MissingKey))
|
||||
|
||||
if (parsed.length === 0) return ALL_MISSING_KEYS
|
||||
return Array.from(new Set(parsed))
|
||||
}
|
||||
|
||||
function buildMissingWhereForKey(key: MissingKey) {
|
||||
switch (key) {
|
||||
case "author":
|
||||
return { authorName: { equals: "" } }
|
||||
case "cover":
|
||||
return {
|
||||
OR: [
|
||||
{ coverUrl: null },
|
||||
{ coverUrl: { equals: "" } },
|
||||
],
|
||||
}
|
||||
case "description":
|
||||
return { description: { equals: "" } }
|
||||
case "genres":
|
||||
return { genres: { none: {} } }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function computeMissingStatus(novel: {
|
||||
authorName: string
|
||||
coverUrl: string | null
|
||||
description: string
|
||||
genres: Array<{ genre: { id: string; name: string } }>
|
||||
}) {
|
||||
const authorMissing = novel.authorName.trim().length === 0
|
||||
const coverMissing = !novel.coverUrl || novel.coverUrl.trim().length === 0
|
||||
const descriptionMissing = novel.description.trim().length === 0
|
||||
const genresMissing = novel.genres.length === 0
|
||||
|
||||
return {
|
||||
author: authorMissing,
|
||||
cover: coverMissing,
|
||||
description: descriptionMissing,
|
||||
genres: genresMissing,
|
||||
}
|
||||
}
|
||||
|
||||
function hasSelectedMissing(missingStatus: Record<MissingKey, boolean>, selected: MissingKey[]) {
|
||||
return selected.some((key) => missingStatus[key])
|
||||
}
|
||||
|
||||
export async function GET(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 q = (url.searchParams.get("q") || "").trim()
|
||||
const selectedMissing = parseMissingKeys(url.searchParams.get("missing"))
|
||||
|
||||
const andWhere: any[] = [getScopeWhere(session)]
|
||||
|
||||
if (q) {
|
||||
andWhere.push({
|
||||
OR: [
|
||||
{ title: { contains: q, mode: "insensitive" } },
|
||||
{ slug: { contains: q, mode: "insensitive" } },
|
||||
{ authorName: { contains: q, mode: "insensitive" } },
|
||||
{ series: { name: { contains: q, mode: "insensitive" } } },
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
if (selectedMissing.length > 0) {
|
||||
andWhere.push({
|
||||
OR: selectedMissing.map((key) => buildMissingWhereForKey(key)),
|
||||
})
|
||||
}
|
||||
|
||||
const novels = await (prisma as any).novel.findMany({
|
||||
where: { AND: andWhere },
|
||||
orderBy: [{ updatedAt: "desc" }],
|
||||
take: 600,
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
authorName: true,
|
||||
coverUrl: true,
|
||||
description: true,
|
||||
totalChapters: true,
|
||||
updatedAt: true,
|
||||
series: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
genres: {
|
||||
select: {
|
||||
genre: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const items = novels
|
||||
.map((novel: any) => {
|
||||
const missing = computeMissingStatus(novel)
|
||||
|
||||
return {
|
||||
id: novel.id,
|
||||
title: novel.title,
|
||||
slug: novel.slug,
|
||||
authorName: novel.authorName,
|
||||
coverUrl: novel.coverUrl,
|
||||
description: novel.description,
|
||||
totalChapters: novel.totalChapters,
|
||||
updatedAt: novel.updatedAt,
|
||||
series: novel.series,
|
||||
genres: novel.genres.map((item: any) => item.genre),
|
||||
missing,
|
||||
}
|
||||
})
|
||||
.filter((item: any) => hasSelectedMissing(item.missing, selectedMissing))
|
||||
|
||||
return NextResponse.json({
|
||||
items,
|
||||
total: items.length,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch novels with missing fields", error)
|
||||
return NextResponse.json({ error: "Failed to fetch missing-field novels" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(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 body = await req.json()
|
||||
const updates = Array.isArray(body?.updates) ? body.updates : []
|
||||
|
||||
if (updates.length === 0) {
|
||||
return NextResponse.json({ error: "Thiếu danh sách cập nhật" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (updates.length > 200) {
|
||||
return NextResponse.json({ error: "Chỉ hỗ trợ tối đa 200 bản ghi mỗi lần" }, { status: 400 })
|
||||
}
|
||||
|
||||
const ids = updates
|
||||
.map((item: any) => (typeof item?.id === "string" ? item.id : ""))
|
||||
.filter(Boolean)
|
||||
|
||||
if (ids.length === 0) {
|
||||
return NextResponse.json({ error: "Danh sách ID không hợp lệ" }, { status: 400 })
|
||||
}
|
||||
|
||||
const allowedRows = await (prisma as any).novel.findMany({
|
||||
where: {
|
||||
AND: [
|
||||
getScopeWhere(session),
|
||||
{ id: { in: ids } },
|
||||
],
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
|
||||
const allowedSet = new Set(allowedRows.map((row: any) => row.id))
|
||||
|
||||
let updatedCount = 0
|
||||
let skippedCount = 0
|
||||
const failures: Array<{ id: string; error: string }> = []
|
||||
|
||||
for (const raw of updates) {
|
||||
const id = typeof raw?.id === "string" ? raw.id : ""
|
||||
if (!id) {
|
||||
skippedCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (!allowedSet.has(id)) {
|
||||
failures.push({ id, error: "Không có quyền cập nhật truyện này" })
|
||||
continue
|
||||
}
|
||||
|
||||
const data: Record<string, any> = {}
|
||||
|
||||
if (typeof raw.authorName === "string") {
|
||||
data.authorName = raw.authorName.trim()
|
||||
}
|
||||
|
||||
if (typeof raw.coverUrl === "string") {
|
||||
const normalizedCover = raw.coverUrl.trim()
|
||||
data.coverUrl = normalizedCover.length > 0 ? normalizedCover : null
|
||||
} else if (raw.coverUrl === null) {
|
||||
data.coverUrl = null
|
||||
}
|
||||
|
||||
if (typeof raw.description === "string") {
|
||||
data.description = raw.description.trim()
|
||||
}
|
||||
|
||||
const hasGenreUpdate = Array.isArray(raw.genreIds)
|
||||
const genreIds: string[] = hasGenreUpdate
|
||||
? Array.from(new Set((raw.genreIds as unknown[]).filter((item): item is string => typeof item === "string" && item.trim().length > 0)))
|
||||
: []
|
||||
|
||||
if (Object.keys(data).length === 0 && !hasGenreUpdate) {
|
||||
skippedCount += 1
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
if (Object.keys(data).length > 0) {
|
||||
await (tx as any).novel.update({
|
||||
where: { id },
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
if (hasGenreUpdate) {
|
||||
await (tx as any).novelGenre.deleteMany({ where: { novelId: id } })
|
||||
|
||||
if (genreIds.length > 0) {
|
||||
await (tx as any).novelGenre.createMany({
|
||||
data: genreIds.map((genreId) => ({ novelId: id, genreId })),
|
||||
skipDuplicates: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
updatedCount += 1
|
||||
} catch (error: any) {
|
||||
failures.push({ id, error: error?.message || "Cập nhật thất bại" })
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
updatedCount,
|
||||
skippedCount,
|
||||
failureCount: failures.length,
|
||||
failures,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to patch missing-field novels", error)
|
||||
return NextResponse.json({ error: "Failed to update novels" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
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
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: "Thiếu ID truyện" }, { status: 400 })
|
||||
}
|
||||
|
||||
const hasField = (field: string) => Object.prototype.hasOwnProperty.call(data, field)
|
||||
|
||||
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 sharedData: Record<string, unknown> = {}
|
||||
if (hasField("originalTitle")) sharedData.originalTitle = originalTitle
|
||||
if (hasField("authorName")) sharedData.authorName = authorName
|
||||
if (hasField("originalAuthorName")) sharedData.originalAuthorName = originalAuthorName
|
||||
if (hasField("description")) sharedData.description = description
|
||||
if (hasField("status")) sharedData.status = status
|
||||
|
||||
const ownData: Record<string, unknown> = {}
|
||||
if (hasField("title")) ownData.title = title
|
||||
if (hasField("coverUrl")) ownData.coverUrl = coverUrl
|
||||
if (session.user.role === "MOD") ownData.uploaderId = session.user.id
|
||||
|
||||
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.
|
||||
if (Object.keys(sharedData).length > 0) {
|
||||
await tx.novel.updateMany({
|
||||
where: { id: { in: seriesNovelIds } },
|
||||
data: sharedData,
|
||||
})
|
||||
}
|
||||
|
||||
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.
|
||||
if (Object.keys(ownData).length === 0) {
|
||||
return tx.novel.findUnique({ where: { id } })
|
||||
}
|
||||
|
||||
return tx.novel.update({
|
||||
where: { id },
|
||||
data: ownData,
|
||||
})
|
||||
})
|
||||
|
||||
return NextResponse.json(updatedNovel)
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
seriesId: fixedSeriesId,
|
||||
...(session.user.role === "MOD" && { uploaderId: session.user.id }),
|
||||
}
|
||||
|
||||
if (hasField("title")) updateData.title = title
|
||||
if (hasField("originalTitle")) updateData.originalTitle = originalTitle
|
||||
if (hasField("authorName")) updateData.authorName = authorName
|
||||
if (hasField("originalAuthorName")) updateData.originalAuthorName = originalAuthorName
|
||||
if (hasField("description")) updateData.description = description
|
||||
if (hasField("coverUrl")) updateData.coverUrl = coverUrl
|
||||
if (hasField("status")) updateData.status = status
|
||||
|
||||
const updatedNovel = await prisma.novel.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...updateData,
|
||||
...(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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user