Initial reader-api backend extracted from reader

This commit is contained in:
2026-03-24 13:55:10 +07:00
parent 56f8f5ccfc
commit 24f070d14e
69 changed files with 12167 additions and 1 deletions
+53
View File
@@ -0,0 +1,53 @@
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 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 {
await connectToMongoDB()
// console.log("Fetching chapter with ID:", id)
const chapter = await Chapter.findById(id)
if (!chapter) {
// console.log("Chapter not found in DB")
return NextResponse.json({ error: "Chapter not found" }, { status: 404 })
}
// 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: 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 })
}
return NextResponse.json(chapter)
} catch (error) {
console.error("GET Chapter error:", error)
return NextResponse.json({ error: "Failed to fetch chapter details" }, { status: 500 })
}
}
+63
View File
@@ -0,0 +1,63 @@
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"
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 { novelId, fromNumber, toNumber } = data
if (!novelId || typeof fromNumber !== "number" || typeof toNumber !== "number") {
return NextResponse.json({ error: "Dữ liệu không hợp lệ" }, { status: 400 })
}
if (fromNumber > toNumber) {
return NextResponse.json({ error: "Chương bắt đầu không được lớn hơn chương kết thúc" }, { status: 400 })
}
// Xác minh truyện thuộc về Mod này (hoặc Admin)
const novel = await prisma.novel.findUnique({
where: { id: novelId }
})
if (!novel) {
return NextResponse.json({ error: "Truyện không tồn tại" }, { status: 404 })
}
if (session.user.role !== "ADMIN" && novel.uploaderId !== session.user.id) {
return NextResponse.json({ error: "Bạn không có quyền thao tác trên truyện này" }, { status: 403 })
}
await connectToMongoDB()
// Xóa các chương trong khoảng
const deleteResult = await Chapter.deleteMany({
novelId,
number: { $gte: fromNumber, $lte: toNumber }
})
// Cập nhật lại số lượng chương
const totalChapters = await Chapter.countDocuments({ novelId })
await prisma.novel.update({
where: { id: novelId },
data: { totalChapters },
})
return NextResponse.json({
success: true,
deletedCount: deleteResult.deletedCount,
totalChapters
})
} catch (error: any) {
console.error("Bulk Delete Chapters Error:", error)
return NextResponse.json({ error: "Lỗi hệ thống khi xóa chương: " + error.message }, { status: 500 })
}
}
+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 })
}
}
+77
View File
@@ -0,0 +1,77 @@
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 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 body = await req.json()
const { novelId, updates } = body
if (!novelId || !updates || !Array.isArray(updates)) {
return NextResponse.json({ error: "Tham số không hợp lệ" }, { status: 400 })
}
const novel = await prisma.novel.findUnique({
where: { id: novelId },
select: { id: true, uploaderId: true }
})
if (!novel) {
return NextResponse.json({ error: "Không tìm thấy truyện" }, { status: 404 })
}
if (session.user.role !== "ADMIN" && novel.uploaderId !== session.user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
}
const validUpdates = updates.filter((update: any) =>
update &&
typeof update.id === "string" &&
typeof update.number === "number" &&
typeof update.title === "string"
)
if (validUpdates.length === 0) {
return NextResponse.json({ message: "Không có thay đổi nào" }, { status: 200 })
}
await connectToMongoDB()
// Prepare bulk operations for mongoose
const bulkOps = validUpdates.map((update: any) => ({
updateOne: {
filter: { _id: update.id, novelId: novelId },
update: {
$set: {
number: update.number,
title: update.title
}
}
}
}))
if (bulkOps.length === 0) {
return NextResponse.json({ message: "Không có thay đổi nào" }, { status: 200 })
}
const result = await Chapter.bulkWrite(bulkOps)
return NextResponse.json({
message: "Cập nhật thành công",
matchedCount: result.matchedCount,
modifiedCount: result.modifiedCount
}, { status: 200 })
} catch (error: any) {
console.error("Bulk optimize error:", error)
return NextResponse.json({ error: "Lỗi cập nhật hàng loạt", details: error.message }, { status: 500 })
}
}
+189
View File
@@ -0,0 +1,189 @@
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"
function toNullableNumber(value: any): number | null {
if (value === null || value === undefined || value === "") return null
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : null
}
export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const novelId = searchParams.get("novelId")
const page = parseInt(searchParams.get("page") || "1")
const limit = parseInt(searchParams.get("limit") || "20")
if (!novelId) {
return NextResponse.json({ error: "novelId is required" }, { status: 400 })
}
try {
await connectToMongoDB()
const skip = (page - 1) * limit
const [chapters, totalChapters] = await Promise.all([
Chapter.find({ novelId })
.sort({ number: 1 })
.skip(skip)
.limit(limit)
.select("-content"),
Chapter.countDocuments({ novelId })
])
return NextResponse.json({
chapters,
totalChapters,
totalPages: Math.ceil(totalChapters / limit),
currentPage: page
})
} catch (error) {
console.error("GET Chapter Error:", error)
return NextResponse.json({ error: "Failed to fetch chapters" }, { 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 { novelId, number, title, content, volumeNumber, volumeTitle, volumeChapterNumber } = data
// Xác minh truyện thuộc về Mod này
const novel = await prisma.novel.findFirst({
where: { id: novelId, uploaderId: session.user.id },
})
if (!novel) {
return NextResponse.json({ error: "Truyện không tồn tại hoặc không đủ quyền" }, { status: 403 })
}
await connectToMongoDB()
// Kiểm tra chương đã tồn tại
const existingChapter = await Chapter.findOne({ novelId, number })
if (existingChapter) {
return NextResponse.json({ error: "Chương này đã tồn tại" }, { status: 400 })
}
const newChapter = await Chapter.create({
novelId,
number,
volumeNumber: toNullableNumber(volumeNumber),
volumeTitle: typeof volumeTitle === "string" && volumeTitle.trim().length > 0 ? volumeTitle.trim() : null,
volumeChapterNumber: toNullableNumber(volumeChapterNumber),
title,
content,
})
// Cập nhật số chương trong table PostgreSQL, tự động đếm lại
const totalChapters = await Chapter.countDocuments({ novelId })
await prisma.novel.update({
where: { id: novelId },
data: { totalChapters },
})
return NextResponse.json(newChapter, { status: 201 })
} catch (error) {
console.error("POST Chapter Error:", error)
return NextResponse.json({ error: "Failed to create chapter" }, { 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, novelId, number, title, content, volumeNumber, volumeTitle, volumeChapterNumber } = data
// Xác minh truyện thuộc về Mod này
const novel = await prisma.novel.findFirst({
where: { id: novelId, uploaderId: session.user.id },
})
if (!novel) {
return NextResponse.json({ error: "Truyện không tồn tại hoặc không đủ quyền" }, { status: 403 })
}
await connectToMongoDB()
const updatedChapter = await Chapter.findOneAndUpdate(
{ _id: id, novelId },
{
number,
title,
content,
volumeNumber: toNullableNumber(volumeNumber),
volumeTitle: typeof volumeTitle === "string" && volumeTitle.trim().length > 0 ? volumeTitle.trim() : null,
volumeChapterNumber: toNullableNumber(volumeChapterNumber),
},
{ new: true }
)
if (!updatedChapter) {
return NextResponse.json({ error: "Không tìm thấy chương" }, { status: 404 })
}
return NextResponse.json(updatedChapter)
} catch (error) {
console.error("PUT Chapter Error:", error)
return NextResponse.json({ error: "Failed to update chapter" }, { 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")
const novelId = url.searchParams.get("novelId")
if (!id || !novelId) {
return NextResponse.json({ error: "Thiếu ID chương hoặc ID truyện" }, { status: 400 })
}
// Xác minh truyện thuộc về Mod này
const novel = await prisma.novel.findFirst({
where: { id: novelId, uploaderId: session.user.id },
})
if (!novel) {
return NextResponse.json({ error: "Truyện không tồn tại hoặc không đủ quyền" }, { status: 403 })
}
await connectToMongoDB()
const deletedChapter = await Chapter.findOneAndDelete({ _id: id, novelId })
if (!deletedChapter) {
return NextResponse.json({ error: "Không tìm thấy chương" }, { status: 404 })
}
// Cập nhật lại số lượng chương trong Postgres
const totalChapters = await Chapter.countDocuments({ novelId })
await prisma.novel.update({
where: { id: novelId },
data: { totalChapters },
})
return NextResponse.json({ message: "Đã xóa chương thành công" })
} catch (error) {
console.error("DELETE Chapter Error:", error)
return NextResponse.json({ error: "Failed to delete chapter" }, { status: 500 })
}
}