Refactor API integration and data fetching for genre, novel, and chapter pages

- Replace Prisma database calls with API fetches from the reader API in GenreDetailPage, GenresPage, SearchPage, ChapterReaderPage, and NovelDetailPage.
- Introduce new utility functions for API requests in server-api.ts, including error handling.
- Update authentication flow in auth.ts to sync Google login with the reader API.
- Modify NextAuth session and JWT types to include additional user information.
- Clean up unused imports and code related to Prisma and MongoDB connections.
- Adjust the configuration in next.config.mjs to remove unnecessary API routes.
This commit is contained in:
2026-03-30 13:54:51 +07:00
parent f9bb247ff1
commit 41aca718c9
12 changed files with 515 additions and 749 deletions
+47 -15
View File
@@ -1,10 +1,20 @@
import { NextAuthOptions } from "next-auth"
import GoogleProvider from "next-auth/providers/google"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "./prisma"
const readerApiOrigin = (process.env.READER_API_ORIGIN || "http://localhost:8000").replace(/\/+$/, "")
type MobileLoginResponse = {
accessToken: string
user: {
id: string
email?: string | null
name?: string | null
image?: string | null
role?: string | null
}
}
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma) as any, // ép kiểu vì type mismatch nhỏ
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "demo-id",
@@ -12,24 +22,46 @@ export const authOptions: NextAuthOptions = {
}),
],
session: {
// Để giữ NextAuth dùng JWT thay vì lưu phiên vào DB nếu thích, nhưng khi dùng PrismaAdapter, mặc định nó dùng DB strategy.
// strategy: "jwt",
strategy: "jwt",
},
callbacks: {
async session({ session, user }) {
if (session.user) {
// Lấy role từ DB gán vào session
const dbUser = await prisma.user.findUnique({
where: { email: session.user.email as string },
select: { role: true, id: true },
})
async jwt({ token, account }) {
if (account?.provider === "google" && account.id_token) {
try {
const response = await fetch(`${readerApiOrigin}/api/auth/mobile-login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ googleIdToken: account.id_token }),
})
session.user.id = dbUser?.id || user.id
session.user.role = dbUser?.role || "USER"
if (response.ok) {
const data = (await response.json()) as MobileLoginResponse
token.sub = data.user.id
;(token as any).id = data.user.id
;(token as any).role = data.user.role || "USER"
;(token as any).name = data.user.name || token.name || null
;(token as any).email = data.user.email || token.email || null
;(token as any).picture = data.user.image || (token as any).picture || null
;(token as any).accessToken = data.accessToken
}
} catch (error) {
console.error("Failed to sync Google login with reader-api", error)
}
}
return token
},
async session({ session, token }) {
if (session.user) {
session.user.id = String((token as any).id || token.sub || "")
session.user.role = String((token as any).role || "USER")
session.user.name = ((token as any).name ?? token.name ?? session.user.name ?? null) as string | null
session.user.email = ((token as any).email ?? token.email ?? session.user.email ?? null) as string | null
session.user.image = ((token as any).picture ?? (token as any).image ?? session.user.image ?? null) as string | null
;(session as any).accessToken = (token as any).accessToken || null
}
return session
},
},
// Tuân thủ bảo mật NextAuth
secret: process.env.NEXTAUTH_SECRET,
}
+48
View File
@@ -0,0 +1,48 @@
const apiBaseUrl = (process.env.READER_API_ORIGIN || "http://localhost:8000").replace(/\/+$/, "")
export class ReaderApiError extends Error {
status: number
constructor(status: number, message: string) {
super(message)
this.status = status
}
}
function buildApiUrl(path: string) {
return `${apiBaseUrl}${path.startsWith("/") ? path : `/${path}`}`
}
export async function readerApiFetch<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(buildApiUrl(path), {
...init,
cache: init?.cache ?? "no-store",
headers: {
Accept: "application/json",
...(init?.headers || {}),
},
})
if (!response.ok) {
let detail = response.statusText
try {
const data = await response.json()
detail = data?.detail || data?.error || detail
} catch {}
throw new ReaderApiError(response.status, `Reader API error ${response.status}: ${detail}`)
}
return response.json() as Promise<T>
}
export async function readerApiFetchNullable<T>(path: string, init?: RequestInit): Promise<T | null> {
try {
return await readerApiFetch<T>(path, init)
} catch (error) {
if (error instanceof ReaderApiError && error.status === 404) {
return null
}
throw error
}
}