diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
index b6149fb..9304253 100644
--- a/app/api/auth/[...nextauth]/route.ts
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -1,6 +1,21 @@
-import NextAuth from "next-auth"
-import { authOptions } from "@/lib/auth"
+import { NextResponse } from "next/server"
-const handler = NextAuth(authOptions)
+export const runtime = "nodejs"
+export const dynamic = "force-dynamic"
-export { handler as GET, handler as POST }
+function disabled() {
+ return NextResponse.json(
+ {
+ detail: "Legacy NextAuth route is disabled. Use /api/auth/login, /api/auth/session, /api/auth/logout.",
+ },
+ { status: 410 },
+ )
+}
+
+export async function GET() {
+ return disabled()
+}
+
+export async function POST() {
+ return disabled()
+}
diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts
new file mode 100644
index 0000000..9307fa9
--- /dev/null
+++ b/app/api/auth/login/route.ts
@@ -0,0 +1,71 @@
+import { NextRequest, NextResponse } from "next/server"
+import { AUTH_COOKIE_MAX_AGE_SECONDS, AUTH_COOKIE_NAME } from "@/lib/auth-cookie"
+
+export const runtime = "nodejs"
+export const dynamic = "force-dynamic"
+
+const readerApiOrigin = (process.env.READER_API_ORIGIN || "http://localhost:8000").replace(/\/+$/, "")
+
+type MobileLoginResponse = {
+ accessToken: string
+ expiresIn?: number
+ user: {
+ id: string
+ email?: string | null
+ name?: string | null
+ image?: string | null
+ role?: string | null
+ }
+}
+
+export async function POST(req: NextRequest) {
+ try {
+ const body = await req.json()
+ const googleIdToken = String(body?.googleIdToken || "").trim()
+
+ if (!googleIdToken) {
+ return NextResponse.json({ detail: "googleIdToken is required" }, { status: 400 })
+ }
+
+ const upstream = await fetch(`${readerApiOrigin}/api/auth/mobile-login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ googleIdToken }),
+ cache: "no-store",
+ signal: AbortSignal.timeout(10000),
+ })
+
+ if (!upstream.ok) {
+ const message = await upstream.text()
+ return NextResponse.json({ detail: message || "Authentication failed" }, { status: upstream.status })
+ }
+
+ const data = (await upstream.json()) as MobileLoginResponse
+
+ const response = NextResponse.json(
+ {
+ user: {
+ id: data.user.id,
+ email: data.user.email || null,
+ name: data.user.name || null,
+ image: data.user.image || null,
+ role: data.user.role || "USER",
+ },
+ },
+ { status: 200 },
+ )
+
+ response.cookies.set(AUTH_COOKIE_NAME, data.accessToken, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/",
+ maxAge: data.expiresIn || AUTH_COOKIE_MAX_AGE_SECONDS,
+ })
+
+ return response
+ } catch (error) {
+ console.error("/api/auth/login failed", error)
+ return NextResponse.json({ detail: "Internal Server Error" }, { status: 500 })
+ }
+}
diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts
new file mode 100644
index 0000000..856481f
--- /dev/null
+++ b/app/api/auth/logout/route.ts
@@ -0,0 +1,17 @@
+import { NextResponse } from "next/server"
+import { AUTH_COOKIE_NAME } from "@/lib/auth-cookie"
+
+export const runtime = "nodejs"
+export const dynamic = "force-dynamic"
+
+export async function POST() {
+ const response = NextResponse.json({ success: true }, { status: 200 })
+ response.cookies.set(AUTH_COOKIE_NAME, "", {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/",
+ maxAge: 0,
+ })
+ return response
+}
diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts
new file mode 100644
index 0000000..6e609bf
--- /dev/null
+++ b/app/api/auth/session/route.ts
@@ -0,0 +1,34 @@
+import { NextRequest, NextResponse } from "next/server"
+import { AUTH_COOKIE_NAME } from "@/lib/auth-cookie"
+
+export const runtime = "nodejs"
+export const dynamic = "force-dynamic"
+
+const readerApiOrigin = (process.env.READER_API_ORIGIN || "http://localhost:8000").replace(/\/+$/, "")
+
+export async function GET(req: NextRequest) {
+ const accessToken = req.cookies.get(AUTH_COOKIE_NAME)?.value || ""
+ if (!accessToken) {
+ return NextResponse.json({ user: null }, { status: 200 })
+ }
+
+ try {
+ const upstream = await fetch(`${readerApiOrigin}/api/auth/session`, {
+ method: "GET",
+ headers: { authorization: `Bearer ${accessToken}` },
+ cache: "no-store",
+ signal: AbortSignal.timeout(5000),
+ })
+
+ if (!upstream.ok) {
+ return NextResponse.json({ user: null }, { status: 200 })
+ }
+
+ const data = await upstream.json()
+ const user = data?.user || null
+ return NextResponse.json({ user }, { status: 200 })
+ } catch (error) {
+ console.error("/api/auth/session failed", error)
+ return NextResponse.json({ user: null }, { status: 200 })
+ }
+}
diff --git a/app/api/mod/[...path]/route.ts b/app/api/mod/[...path]/route.ts
index f0c6486..1cf290b 100644
--- a/app/api/mod/[...path]/route.ts
+++ b/app/api/mod/[...path]/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"
-import { getToken } from "next-auth/jwt"
+import { AUTH_COOKIE_NAME } from "@/lib/auth-cookie"
export const runtime = "nodejs"
export const dynamic = "force-dynamic"
@@ -7,8 +7,7 @@ export const dynamic = "force-dynamic"
const readerApiOrigin = (process.env.READER_API_ORIGIN || "http://localhost:8000").replace(/\/+$/, "")
async function proxyToReaderApi(req: NextRequest, path: string[]) {
- const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
- const accessToken = typeof (token as any)?.accessToken === "string" ? (token as any).accessToken : null
+ const accessToken = req.cookies.get(AUTH_COOKIE_NAME)?.value || null
const url = new URL(req.url)
const query = url.search || ""
@@ -16,6 +15,7 @@ async function proxyToReaderApi(req: NextRequest, path: string[]) {
const headers = new Headers(req.headers)
headers.delete("host")
+ headers.delete("cookie")
if (accessToken) {
headers.set("authorization", `Bearer ${accessToken}`)
}
diff --git a/app/api/user/[...path]/route.ts b/app/api/user/[...path]/route.ts
index 7d8adac..b345855 100644
--- a/app/api/user/[...path]/route.ts
+++ b/app/api/user/[...path]/route.ts
@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from "next/server"
-import { getToken } from "next-auth/jwt"
+import { AUTH_COOKIE_NAME } from "@/lib/auth-cookie"
export const runtime = "nodejs"
export const dynamic = "force-dynamic"
@@ -7,8 +7,7 @@ export const dynamic = "force-dynamic"
const readerApiOrigin = (process.env.READER_API_ORIGIN || "http://localhost:8000").replace(/\/+$/, "")
async function proxyToReaderApi(req: NextRequest, path: string[]) {
- const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET })
- const accessToken = typeof (token as any)?.accessToken === "string" ? (token as any).accessToken : null
+ const accessToken = req.cookies.get(AUTH_COOKIE_NAME)?.value || null
const url = new URL(req.url)
const query = url.search || ""
@@ -16,6 +15,7 @@ async function proxyToReaderApi(req: NextRequest, path: string[]) {
const headers = new Headers(req.headers)
headers.delete("host")
+ headers.delete("cookie")
if (accessToken) {
headers.set("authorization", `Bearer ${accessToken}`)
}
diff --git a/app/dang-nhap/page.tsx b/app/dang-nhap/page.tsx
index 07861b0..5caa937 100644
--- a/app/dang-nhap/page.tsx
+++ b/app/dang-nhap/page.tsx
@@ -1,11 +1,12 @@
"use client"
-import { useEffect } from "react"
+import { useEffect, useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { BookOpen } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useAuth } from "@/lib/auth-context"
+import { toast } from "sonner"
function GoogleIcon({ className }: { className?: string }) {
return (
@@ -21,6 +22,7 @@ function GoogleIcon({ className }: { className?: string }) {
export default function LoginPage() {
const router = useRouter()
const { user, loginWithGoogle } = useAuth()
+ const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => {
if (user) {
@@ -28,9 +30,19 @@ export default function LoginPage() {
}
}, [user, router])
- const handleGoogleLogin = () => {
- loginWithGoogle()
- router.push("/")
+ const handleGoogleLogin = async () => {
+ if (isSubmitting) return
+
+ setIsSubmitting(true)
+ try {
+ await loginWithGoogle()
+ router.push("/")
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Đăng nhập thất bại"
+ toast.error(message)
+ } finally {
+ setIsSubmitting(false)
+ }
}
return (
@@ -52,9 +64,10 @@ export default function LoginPage() {
variant="outline"
className="flex h-12 w-full items-center justify-center gap-3 text-sm font-medium"
onClick={handleGoogleLogin}
+ disabled={isSubmitting}
>
- {"Dang nhap bang Google"}
+ {isSubmitting ? "Dang xu ly..." : "Dang nhap bang Google"}
diff --git a/app/mod/ai-tool/page.tsx b/app/mod/ai-tool/page.tsx
index 7e0f6c2..f9ac816 100644
--- a/app/mod/ai-tool/page.tsx
+++ b/app/mod/ai-tool/page.tsx
@@ -6,6 +6,8 @@ import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { toast } from "sonner"
+// Trang này được đặt dưới /mod layout đã kiểm soát quyền ở server side.
+
type NovelRow = {
id: string
title: string
diff --git a/app/mod/chuong/[id]/page.tsx b/app/mod/chuong/[id]/page.tsx
index 6a61cb4..deeb8fb 100644
--- a/app/mod/chuong/[id]/page.tsx
+++ b/app/mod/chuong/[id]/page.tsx
@@ -1,13 +1,8 @@
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/lib/auth"
-import { redirect } from "next/navigation"
+import { requireModSessionUser } from "@/lib/server-auth"
import { EditorClient } from "./editor-client"
export default async function ModEditChapterPage({ params }: { params: Promise<{ id: string }> }) {
- const session = await getServerSession(authOptions)
- if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
- redirect("/")
- }
+ await requireModSessionUser()
const resolvedParams = await params
diff --git a/app/mod/chuong/page.tsx b/app/mod/chuong/page.tsx
index 6baa4c5..dd40ddc 100644
--- a/app/mod/chuong/page.tsx
+++ b/app/mod/chuong/page.tsx
@@ -1,13 +1,8 @@
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/lib/auth"
-import { redirect } from "next/navigation"
+import { requireModSessionUser } from "@/lib/server-auth"
import { ChapterClient } from "./chapter-client"
export default async function ModChuongPage() {
- const session = await getServerSession(authOptions)
- if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
- redirect("/")
- }
+ await requireModSessionUser()
return
}
diff --git a/app/mod/de-cu/page.tsx b/app/mod/de-cu/page.tsx
index 1ce66da..f5dcf7f 100644
--- a/app/mod/de-cu/page.tsx
+++ b/app/mod/de-cu/page.tsx
@@ -1,13 +1,8 @@
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/lib/auth"
-import { redirect } from "next/navigation"
+import { requireModSessionUser } from "@/lib/server-auth"
import { RecommendationClient } from "./recommendation-client"
export default async function ModRecommendationPage() {
- const session = await getServerSession(authOptions)
- if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
- redirect("/")
- }
+ await requireModSessionUser()
return
}
diff --git a/app/mod/layout.tsx b/app/mod/layout.tsx
index 4acb502..e822949 100644
--- a/app/mod/layout.tsx
+++ b/app/mod/layout.tsx
@@ -1,6 +1,4 @@
-import { redirect } from "next/navigation"
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/lib/auth"
+import { requireModSessionUser } from "@/lib/server-auth"
import { CollapsibleSidebar } from "./collapsible-sidebar"
export default async function ModLayout({
@@ -8,12 +6,7 @@ export default async function ModLayout({
}: {
children: React.ReactNode
}) {
- const session = await getServerSession(authOptions)
-
- // Kiểm tra quyền
- if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
- redirect("/") // Không đủ quyền, đưa về trang chủ
- }
+ await requireModSessionUser()
return (
diff --git a/app/mod/page.tsx b/app/mod/page.tsx
index e1fdc22..5cb3d84 100644
--- a/app/mod/page.tsx
+++ b/app/mod/page.tsx
@@ -1,13 +1,13 @@
-import { getServerSession } from "next-auth"
-import { authOptions } from "@/lib/auth"
import Link from "next/link"
import { Sparkles } from "lucide-react"
import { cookies } from "next/headers"
+import { AUTH_COOKIE_NAME } from "@/lib/auth-cookie"
+import { requireModSessionUser } from "@/lib/server-auth"
const readerApiOrigin = (process.env.READER_API_ORIGIN || "http://localhost:8000").replace(/\/+$/, "")
export default async function ModDashboardPage() {
- const session = await getServerSession(authOptions)
+ const sessionUser = await requireModSessionUser()
let novelCount = 0
let totalViews = 0
@@ -15,10 +15,10 @@ export default async function ModDashboardPage() {
let seriesCount = 0
try {
- const cookieHeader = (await cookies()).toString()
+ const accessToken = (await cookies()).get(AUTH_COOKIE_NAME)?.value || ""
const res = await fetch(`${readerApiOrigin}/api/mod/overview`, {
cache: "no-store",
- headers: cookieHeader ? { cookie: cookieHeader } : undefined,
+ headers: accessToken ? { authorization: `Bearer ${accessToken}` } : undefined,
})
if (res.ok) {
const data = await res.json()
@@ -33,7 +33,7 @@ export default async function ModDashboardPage() {
return (
-
Xin chào, {session?.user.name}
+
Xin chào, {sessionUser?.name || "Moderator"}
Chào mừng bạn đến với trang quản trị dành cho Moderator.
diff --git a/app/mod/series/page.tsx b/app/mod/series/page.tsx
index 5c46ed5..cfe17db 100644
--- a/app/mod/series/page.tsx
+++ b/app/mod/series/page.tsx
@@ -1,13 +1,8 @@
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/lib/auth"
-import { redirect } from "next/navigation"
+import { requireModSessionUser } from "@/lib/server-auth"
import { SeriesClient } from "./series-client"
export default async function ModSeriesPage() {
- const session = await getServerSession(authOptions)
- if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
- redirect("/")
- }
+ await requireModSessionUser()
return
}
diff --git a/app/mod/the-loai/page.tsx b/app/mod/the-loai/page.tsx
index 2372953..dff3625 100644
--- a/app/mod/the-loai/page.tsx
+++ b/app/mod/the-loai/page.tsx
@@ -1,13 +1,8 @@
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/lib/auth"
-import { redirect } from "next/navigation"
+import { requireModSessionUser } from "@/lib/server-auth"
import { GenreClient } from "./genre-client"
export default async function ModTheLoaiPage() {
- const session = await getServerSession(authOptions)
- if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
- redirect("/")
- }
+ await requireModSessionUser()
return
}
diff --git a/app/mod/thieu-thong-tin/page.tsx b/app/mod/thieu-thong-tin/page.tsx
index ddeccf1..17f61f0 100644
--- a/app/mod/thieu-thong-tin/page.tsx
+++ b/app/mod/thieu-thong-tin/page.tsx
@@ -1,14 +1,8 @@
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/lib/auth"
-import { redirect } from "next/navigation"
+import { requireModSessionUser } from "@/lib/server-auth"
import { MissingFieldsClient } from "./missing-fields-client"
export default async function ModMissingFieldsPage() {
- const session = await getServerSession(authOptions)
-
- if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
- redirect("/")
- }
+ await requireModSessionUser()
return
}
diff --git a/app/mod/truyen/page.tsx b/app/mod/truyen/page.tsx
index 652358c..4f3a24d 100644
--- a/app/mod/truyen/page.tsx
+++ b/app/mod/truyen/page.tsx
@@ -1,13 +1,8 @@
-import { getServerSession } from "next-auth/next"
-import { authOptions } from "@/lib/auth"
-import { redirect } from "next/navigation"
+import { requireModSessionUser } from "@/lib/server-auth"
import { NovelClient } from "./novel-client"
export default async function ModTruyenPage() {
- const session = await getServerSession(authOptions)
- if (!session || (session.user.role !== "MOD" && session.user.role !== "ADMIN")) {
- redirect("/")
- }
+ await requireModSessionUser()
return
}
diff --git a/components/header.tsx b/components/header.tsx
index eb69e7d..0a5798f 100644
--- a/components/header.tsx
+++ b/components/header.tsx
@@ -206,7 +206,7 @@ export function Header() {
-
+ void logout()} className="flex items-center gap-2 text-destructive">
Đăng Xuất
@@ -304,7 +304,7 @@ export function Header() {
Loại tài khoản: {roleLabel(user.role)}
-