Search code examples
reactjsauthenticationnext.jsnext-auth

Check isAdmin in Next 13 on the Client Side with Client Component?


I am using Lucia Auth for authentication.

This function returns back the session object:

app/auth/lucia.ts

export const getPageSession = cache(async () => {
    const authRequest = auth.handleRequest("GET", context)
    const session = await authRequest.validate()

    return session
})

app/dashboard/admin.tsx

"use client"

import React from "react"
import { redirect } from "next/navigation"
import NextImg from "next/image"
import ky, { KyResponse } from "ky"

import { getPageSession } from "@/app/auth/lucia"
import { BASE_URL } from "@/app/lib/constants"

const Admin = async () => {
    const [admin, setAdmin] = React.useState(false)

    React.useEffect(() => {
        const fetchAdmin = async () => {
            const res: KyResponse & { isAdmin: boolean } = await ky.get(`${BASE_URL}/api/admin`).json()
            setAdmin(res.isAdmin)
        }
        fetchAdmin()
    }, [])

    const session = await getPageSession()

    if (!session && !admin) redirect("/login")

    return (
        <div className="isolate bg-slate-900 text-white">
            ...
        </div>
    )
}

export default Admin

The first check is to check if session exists because only logged-in users can access the page. It works completely fine.

And the second check is to check if its an admin because only admin can access the page. This doesn't work.

I get this error:

You're importing a component that needs next/headers. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component.

The getPageSession is a server-side check and the React.useEffect is a client call to the api that returns a json with a boolean like { isAdmin: false } or { isAdmin: true }

How do I perform both checks without getting an error? I get too many errors regarding using Server Things in Client Component & the likes.

Ideally, I'd like a common function called isAdmin that returns if someone is an admin or not. And if they are not, then redirect. If they are, then show the page.

How do I do it?


Solution

  • I found a solution. Not sure if its the best way to do it but here it is.

    I created a Client Component to check if someone is an admin:

    components/dashboard/AdminComponent.tsx

    This solution doesn't work. I bet I am using useEffect wrong somehow which can be fixed but the alternate solution below works fine.

    "use client"
    
    import React from "react"
    import { redirect } from "next/navigation"
    import ky, { KyResponse } from "ky"
    
    import { BASE_URL } from "@/app/lib/constants"
    
    export const AdminComponent = () => {
        const [admin, setAdmin] = React.useState(false)
    
        React.useEffect(() => {
            const fetchAdmin = async () => {
                const res: KyResponse & { isAdmin: boolean } = await ky.get(`${BASE_URL}/api/admin`).json()
                setAdmin(res.isAdmin)
            }
            fetchAdmin()
        }, [])
    
        if (!admin) redirect("/dashboard")
    
        return null
    }
    

    Then, I imported the Client Component into a Server Component.

    pages/admin.tsx

    import React from "react"
    import { redirect } from "next/navigation"
    
    import { getPageSession } from "@/app/auth/lucia"
    import { AdminComponent } from "@/app/components/dashboard/AdminComponent"
    
    const Admin = async () => {
        const session = await getPageSession()
    
        if (!session) redirect("/login")
    
        return (
            <div className="isolate bg-slate-900 text-white">
                ...
            </div>
        )
    }
    
    export default Admin
    

    I also rewrote AdminComponent as:

    "use client"
    
    import React from "react"
    import { redirect } from "next/navigation"
    import ky, { KyResponse } from "ky"
    
    import { BASE_URL } from "@/app/lib/constants"
    
    const fetchAdmin = React.cache(async () => {
        const res: KyResponse & { isAdmin: boolean } = await ky.get(`${BASE_URL}/api/admin`).json()
        return res.isAdmin
    })
    
    export const AdminComponent = () => {
        const admin = React.use(fetchAdmin())
    
        if (!admin) redirect("/dashboard")
    
        return null
    }
    

    This looks much cleaner but not a common pattern as of now.

    Would love to know if there is a more simpler or cleaner solution.

    Edit:

    Found a much better solution.

    components/dashboard/AdminComponent.tsx

    "use client"
    
    import React from "react"
    import { redirect } from "next/navigation"
    
    import { useAdminStatus } from "@/app/hooks/useAdminStatus"
    
    // eslint-disable-next-line react/display-name
    export const AdminComponent = React.memo(() => {
        const { data, error, isLoading } = useAdminStatus()
    
        if (isLoading) return null
        if (error) return <span className="text-slate-500">Failed to load `AdminComponent`</span>
    
        if (!data.isAdmin) redirect("/dashboard")
    
        return null
    })
    

    hooks/useAdminStatus.tsx

    import useSWRImmutable from "swr/immutable"
    
    const fetcher = (...args: [any]) => fetch(...args).then((res) => res.json())
    
    export const useAdminStatus = () => {
        const { data, error, isLoading } = useSWRImmutable(`/api/admin`, fetcher)
    
        return {
            data,
            error,
            isLoading,
        }
    }