feat: add auth controls to navbar and middleware route protection

This commit is contained in:
2026-04-26 01:31:12 +08:00
parent 8c6e326bad
commit f60b3b3b08
2 changed files with 156 additions and 0 deletions
+127
View File
@@ -9,6 +9,8 @@ import { CircleXIcon, Gamepad2Icon, MenuIcon, XIcon } from "lucide-react"
import { routes } from "@/lib/routes" import { routes } from "@/lib/routes"
import { usePathname, useRouter, useSearchParams } from "next/navigation" import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useDebounce } from "@/lib/hooks/useDebounce" import { useDebounce } from "@/lib/hooks/useDebounce"
import { authClient } from "@/lib/auth-client"
import { LogOut, User } from "lucide-react"
export default function Navbar() { export default function Navbar() {
const pathname = usePathname() const pathname = usePathname()
@@ -24,6 +26,10 @@ export default function Navbar() {
const [forceFocusStyles, setForceFocusStyles] = useState(false) const [forceFocusStyles, setForceFocusStyles] = useState(false)
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const { data: session, isPending: isSessionLoading } =
authClient.useSession()
const [userMenuOpen, setUserMenuOpen] = useState(false)
// Sync search query with URL ?q= param // Sync search query with URL ?q= param
useEffect(() => { useEffect(() => {
const q = searchParams.get("q") || "" const q = searchParams.get("q") || ""
@@ -183,6 +189,70 @@ export default function Navbar() {
))} ))}
</ul> </ul>
{/* Desktop Auth Controls */}
<div className='hidden md:flex items-center gap-3 shrink-0'>
{isSessionLoading ? (
<div className='w-20 h-8 rounded-lg bg-white/[0.03] animate-pulse' />
) : session ? (
<div className='relative'>
<button
onClick={() => setUserMenuOpen(!userMenuOpen)}
className='flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-white/[0.05] transition-colors'
>
{session.user.image ? (
<img
src={session.user.image}
alt=''
className='h-7 w-7 rounded-full'
/>
) : (
<div className='h-7 w-7 rounded-full bg-[#571b8b] flex items-center justify-center'>
<User className='h-3.5 w-3.5 text-[#ebe4f1]' />
</div>
)}
<span className='text-sm text-[#ebe4f1] max-w-[100px] truncate'>
{session.user.name}
</span>
</button>
{userMenuOpen && (
<>
<div
className='fixed inset-0 z-40'
onClick={() => setUserMenuOpen(false)}
/>
<div className='absolute right-0 top-full mt-1 w-48 bg-[#1a1020] border border-white/10 rounded-lg shadow-lg z-50 py-1'>
<button
onClick={async () => {
setUserMenuOpen(false)
await authClient.signOut()
}}
className='w-full flex items-center gap-2 px-3 py-2 text-sm text-[#ebe4f1]/70 hover:text-[#ebe4f1] hover:bg-white/[0.05] transition-colors'
>
<LogOut className='h-4 w-4' />
Sign out
</button>
</div>
</>
)}
</div>
) : (
<>
<Link
href='/login'
className='text-sm font-medium text-[#ebe4f1]/70 hover:text-[#ebe4f1] transition-colors'
>
Sign in
</Link>
<Link
href='/signup'
className='px-3.5 py-1.5 rounded-lg bg-[#eb3779] text-white text-sm font-medium hover:bg-[#eb3779]/90 transition-colors'
>
Sign up
</Link>
</>
)}
</div>
{/* Mobile Hamburger Button */} {/* Mobile Hamburger Button */}
<button <button
onClick={() => setMobileMenuOpen(true)} onClick={() => setMobileMenuOpen(true)}
@@ -242,6 +312,63 @@ export default function Navbar() {
</Link> </Link>
))} ))}
</nav> </nav>
{/* Mobile Auth Controls */}
<div className='mt-auto p-4 border-t border-white/[0.08]'>
{isSessionLoading ? (
<div className='w-full h-10 rounded-lg bg-white/[0.03] animate-pulse' />
) : session ? (
<div className='space-y-3'>
<div className='flex items-center gap-2'>
{session.user.image ? (
<img
src={session.user.image}
alt=''
className='h-8 w-8 rounded-full'
/>
) : (
<div className='h-8 w-8 rounded-full bg-[#571b8b] flex items-center justify-center'>
<User className='h-4 w-4 text-[#ebe4f1]' />
</div>
)}
<div className='min-w-0'>
<p className='text-sm font-medium text-[#ebe4f1] truncate'>
{session.user.name}
</p>
<p className='text-xs text-[#ebe4f1]/50 truncate'>
{session.user.email}
</p>
</div>
</div>
<button
onClick={async () => {
setMobileMenuOpen(false)
await authClient.signOut()
}}
className='w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg border border-white/10 text-sm text-[#ebe4f1]/70 hover:text-[#ebe4f1] hover:bg-white/[0.05] transition-colors'
>
<LogOut className='h-4 w-4' />
Sign out
</button>
</div>
) : (
<div className='space-y-2'>
<Link
href='/login'
onClick={() => setMobileMenuOpen(false)}
className='block w-full text-center px-3 py-2 rounded-lg border border-white/10 text-sm text-[#ebe4f1] hover:bg-white/[0.05] transition-colors'
>
Sign in
</Link>
<Link
href='/signup'
onClick={() => setMobileMenuOpen(false)}
className='block w-full text-center px-3 py-2 rounded-lg bg-[#eb3779] text-white text-sm font-medium hover:bg-[#eb3779]/90 transition-colors'
>
Sign up
</Link>
</div>
)}
</div>
</motion.aside> </motion.aside>
</> </>
)} )}
+29
View File
@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server"
const authRoutes = [
"/login",
"/signup",
"/forgot-password",
"/reset-password",
]
export default async function middleware(req: NextRequest) {
const path = req.nextUrl.pathname
const isAuthRoute = authRoutes.some((route) => path.startsWith(route))
// Check for better-auth session cookie
const hasSession = req.cookies
.getAll()
.some((cookie) => cookie.name.startsWith("better-auth."))
// Redirect authenticated users away from auth pages
if (isAuthRoute && hasSession) {
return NextResponse.redirect(new URL("/", req.url))
}
return NextResponse.next()
}
export const config = {
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
}