Refactor authentication system: replace NextAuth with custom login/logout/session handling, improve cookie management, and enhance error handling
Build and Push Reader Image / docker (push) Successful in 39s
Build and Push Reader Image / docker (push) Successful in 39s
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
+18
-5
@@ -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}
|
||||
>
|
||||
<GoogleIcon className="h-5 w-5" />
|
||||
{"Dang nhap bang Google"}
|
||||
{isSubmitting ? "Dang xu ly..." : "Dang nhap bang Google"}
|
||||
</Button>
|
||||
|
||||
<div className="mt-4 text-center text-xs text-muted-foreground">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 <ChapterClient />
|
||||
}
|
||||
|
||||
@@ -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 <RecommendationClient />
|
||||
}
|
||||
|
||||
+2
-9
@@ -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 (
|
||||
<div className="flex min-h-[calc(100vh-3.5rem)] bg-muted/20">
|
||||
|
||||
+6
-6
@@ -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 (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Xin chào, {session?.user.name}</h1>
|
||||
<h1 className="text-2xl font-bold mb-4">Xin chào, {sessionUser?.name || "Moderator"}</h1>
|
||||
<p className="text-muted-foreground mb-6">
|
||||
Chào mừng bạn đến với trang quản trị dành cho Moderator.
|
||||
</p>
|
||||
|
||||
@@ -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 <SeriesClient />
|
||||
}
|
||||
|
||||
@@ -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 <GenreClient />
|
||||
}
|
||||
|
||||
@@ -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 <MissingFieldsClient />
|
||||
}
|
||||
|
||||
@@ -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 <NovelClient />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user