Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-03-10 16:37:55 +07:00
parent 75ed8e233b
commit 8908395867
45 changed files with 2528 additions and 365 deletions
+12 -5
View File
@@ -26,15 +26,22 @@ export async function GET(
return NextResponse.json({ error: "Chapter not found" }, { status: 404 })
}
// Verify the moderator owns the related novel
// Verify the moderator owns the related novel (or is an ADMIN)
let novelQuery: any = { id: chapter.novelId }
if (session.user.role !== "ADMIN") {
novelQuery.uploaderId = session.user.id
}
const novel = await prisma.novel.findFirst({
where: {
id: chapter.novelId,
uploaderId: session.user.id
}
where: novelQuery
})
if (!novel) {
console.log("Novel not found or unauthorized:", {
chapterNovelId: chapter.novelId,
userId: session.user.id,
role: session.user.role
})
return NextResponse.json({ error: "Unauthorized access to this chapter" }, { status: 403 })
}
+121
View File
@@ -0,0 +1,121 @@
import { NextResponse } from "next/server"
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import connectToMongoDB from "@/lib/mongoose"
import { Chapter } from "@/lib/models/chapter"
import { prisma } from "@/lib/prisma"
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 { novelId, action = "replace", findText, replaceText, matchCase = false, trashWords = "", preview = false } = body
if (!novelId) {
return NextResponse.json({ error: "novelId is required" }, { status: 400 })
}
// Verify that the novel belongs to the uploader
let novelQuery: any = { id: novelId }
if (session.user.role !== "ADMIN") {
novelQuery.uploaderId = session.user.id
}
const novel = await prisma.novel.findFirst({
where: novelQuery,
})
if (!novel) {
return NextResponse.json({ error: "Truyện không tồn tại hoặc không đủ quyền" }, { status: 403 })
}
await connectToMongoDB()
let patterns: { regex: RegExp, replaceWith: string }[] = []
if (action === "replace") {
if (!findText) return NextResponse.json({ error: "findText is required for replace action" }, { status: 400 })
const flags = matchCase ? "g" : "gi"
const safeFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
patterns.push({ regex: new RegExp(safeFindText, flags), replaceWith: replaceText || "" })
} else if (action === "trash") {
let words: string[] = []
if (Array.isArray(trashWords)) {
words = trashWords
} else if (typeof trashWords === "string") {
words = trashWords.split(',').map((w: string) => w.trim()).filter((w: string) => w.length > 0)
}
if (words.length === 0) return NextResponse.json({ error: "No valid words provided" }, { status: 400 })
words.forEach((word: string) => {
const safeWord = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
patterns.push({ regex: new RegExp(safeWord, 'gi'), replaceWith: "" })
})
} else {
return NextResponse.json({ error: "Invalid action" }, { status: 400 })
}
// Find all chapters for the novel
const chapters = await Chapter.find({ novelId }).sort({ number: 1 })
let updatedCount = 0
let previewResults: any[] = []
for (const chap of chapters) {
let originalContent = chap.content || ""
let newContent = originalContent
let modified = false
patterns.forEach(({ regex, replaceWith }) => {
if (regex.test(newContent)) {
modified = true
newContent = newContent.replace(regex, replaceWith)
}
})
if (modified) {
if (preview && previewResults.length < 5) { // Limit previews to 5 chapters to save payload size
// Capture a small text snippet from the first pattern match
let snippet = ""
if (patterns.length > 0) {
const match = patterns[0].regex.exec(originalContent)
if (match) {
const matchIndex = match.index
const start = Math.max(0, matchIndex - 30)
const end = Math.min(originalContent.length, matchIndex + match[0].length + 30)
snippet = "..." + originalContent.substring(start, end).replace(/\n/g, ' ') + "..."
}
}
previewResults.push({
chapterId: chap._id,
number: chap.number,
title: chap.title,
snippet
})
}
if (!preview) {
chap.content = newContent
await chap.save()
}
updatedCount++
}
}
return NextResponse.json({
message: preview ? "Preview generated" : "Success",
updatedChapters: updatedCount,
previews: previewResults
}, { status: 200 })
} catch (error) {
console.error("Global Replace Error:", error)
return NextResponse.json({ error: "Failed to perform global replacement" }, { 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 })
}
}
+8 -2
View File
@@ -29,16 +29,19 @@ export async function POST(req: Request) {
try {
const data = await req.json()
const { title, authorName, description, genreIds = [] } = data
const { title, originalTitle, authorName, originalAuthorName, description, coverUrl, genreIds = [] } = data
// Tạo slug từ title
const slug = slugify(title)
const newNovel = await prisma.novel.create({
data: {
title,
originalTitle,
slug: slug,
authorName,
originalAuthorName,
description,
coverUrl,
uploaderId: session.user.id,
genres: {
create: genreIds.map((id: string) => ({
@@ -61,15 +64,18 @@ export async function PUT(req: Request) {
try {
const data = await req.json()
const { id, title, authorName, description, status, genreIds } = data
const { id, title, originalTitle, authorName, originalAuthorName, description, coverUrl, 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,
originalTitle,
authorName,
originalAuthorName,
description,
coverUrl,
status,
// Replace all existing genres if genreIds is provided
...(genreIds !== undefined && {
+48
View File
@@ -0,0 +1,48 @@
import { NextResponse } from "next/server"
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { writeFile } from "fs/promises"
import path from "path"
import fs from "fs"
export async function POST(req: Request) {
try {
const session = await getServerSession(authOptions)
if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const formData = await req.formData()
const file = formData.get("file") as File | null
if (!file) {
return NextResponse.json({ error: "No file uploaded" }, { status: 400 })
}
if (!file.type.startsWith("image/")) {
return NextResponse.json({ error: "Only image files are allowed" }, { status: 400 })
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9)
const ext = path.extname(file.name) || ".jpg"
const filename = `cover-${uniqueSuffix}${ext}`
const uploadDir = path.join(process.cwd(), "public", "uploads", "covers")
// Ensure directory exists
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true })
}
const filepath = path.join(uploadDir, filename)
await writeFile(filepath, buffer)
return NextResponse.json({ url: `/uploads/covers/${filename}` })
} catch (error: any) {
console.error("Cover upload error:", error)
return NextResponse.json({ error: error.message || "Failed to upload cover" }, { status: 500 })
}
}
+49
View File
@@ -0,0 +1,49 @@
import { NextResponse } from "next/server"
import connectToMongoDB from "@/lib/mongoose"
import { Chapter } from "@/lib/models/chapter"
export async function GET(
req: Request,
{ params }: { params: Promise<{ id: string }> } // `id` is the `novel.id`
) {
try {
const { id: novelId } = await params
const { searchParams } = new URL(req.url)
const page = parseInt(searchParams.get("page") || "1", 10)
const limit = parseInt(searchParams.get("limit") || "100", 10)
await connectToMongoDB()
const skip = (page - 1) * limit
const [chapters, totalChapters] = await Promise.all([
Chapter.find({ novelId })
.sort({ number: 1 })
.skip(skip)
.limit(limit)
.select("number title createdAt") // don't return content
.lean(),
Chapter.countDocuments({ novelId })
])
return NextResponse.json({
chapters: chapters.map(c => ({
id: c._id.toString(),
number: c.number,
title: c.title,
createdAt: (c.createdAt as Date).toISOString()
})),
totalChapters,
totalPages: Math.ceil(totalChapters / limit),
currentPage: page
})
} catch (error: any) {
console.error("Fetch novel chapters error:", error)
return NextResponse.json(
{ error: "Không thể lấy danh sách chương" },
{ status: 500 }
)
}
}
+47 -2
View File
@@ -78,6 +78,39 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Missing chapter info" }, { status: 400 })
}
// Lấy bookmark cũ (nếu có)
const existingBookmark = await prisma.bookmark.findUnique({
where: {
userId_novelId: {
userId: session.user.id,
novelId,
}
}
})
let newReadChapters: number[] = []
let newHasCountedView = false
let shouldIncrementNovelView = false
if (existingBookmark) {
newReadChapters = existingBookmark.readChapters || []
newHasCountedView = existingBookmark.hasCountedView
// Nếu chương này chưa đọc, thêm vào mảng
if (!newReadChapters.includes(lastChapterNumber)) {
newReadChapters.push(lastChapterNumber)
}
// Nếu đọc đủ 5 chương và chưa từng đếm view
if (newReadChapters.length >= 5 && !newHasCountedView) {
newHasCountedView = true
shouldIncrementNovelView = true
}
} else {
newReadChapters = [lastChapterNumber]
// Chưa đủ 5 chương ngay từ lần đầu tạo
}
const bookmark = await prisma.bookmark.upsert({
where: {
userId_novelId: {
@@ -87,15 +120,27 @@ export async function POST(req: Request) {
},
update: {
lastChapterId,
lastChapterNumber
lastChapterNumber,
readChapters: newReadChapters,
hasCountedView: newHasCountedView
},
create: {
userId: session.user.id,
novelId,
lastChapterId,
lastChapterNumber
lastChapterNumber,
readChapters: newReadChapters,
hasCountedView: newHasCountedView
}
})
if (shouldIncrementNovelView) {
await prisma.novel.update({
where: { id: novelId },
data: { views: { increment: 1 } }
})
}
return NextResponse.json({ status: "updated", bookmark })
}