typescriptnext.jssupabasesupabase-js

Issue with Supabase Auth + Next.js setup using pages directory


I am currently working on a project that involves setting up Supabase Auth with Next.js. I have followed the documentation provided by Supabase (https://supabase.com/docs/guides/auth/auth-helpers/nextjs-pages) to integrate the authentication functionality into my project.

However, I faced difficulties while converting the source files from the app directory to the pages directory. I merged my pages directory with someone else's setup from the app directory because I am more comfortable coding in the pages directory.

To provide more context and details about my issue, I have documented the steps and code changes in my GitHub repository: https://github.com/mashwishi/NextJS-Supabase-AuthHelper/. You can find the specific problem in the readme file.

My main goal with this project is to create a project pages directory with Supabase Auth and database integration using Next.js and React.js. This setup should allow users to log in with their email and password or via OAuth/Social Auth using the "@supabase/auth-ui-react" library.

Additionally, I am looking for guidance on implementing middleware for protected pages, ensuring that only authenticated users can access certain pages.

Note: Everything is working without the _app.tsx file but i want to have this because i need this.

_app.tsx

import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
import { useState } from 'react'
import { AppProps } from 'next/app'

function MyApp({
  Component,
  pageProps,
}: AppProps<{
  initialSession: Session
}>) {
  // Create a new supabase browser client on every first render.
  const [supabaseClient] = useState(() => createPagesBrowserClient())

  return (
    <SessionContextProvider
      supabaseClient={supabaseClient}
      initialSession={pageProps.initialSession}
    >
      <Component {...pageProps} />
    </SessionContextProvider>
  )
}

middleware.ts

import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'

import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
    const res = NextResponse.next()
    const supabase = createMiddlewareClient({ req, res })

    const {
        data: { user },
    } = await supabase.auth.getUser()

    // if user is signed in and the current path is / redirect the user to /account
    if (user && req.nextUrl.pathname === '/login') {
        return NextResponse.redirect(new URL('/account', req.url))
    }

    // if user is not signed in and the current path is not / redirect the user to /
    if (!user && req.nextUrl.pathname !== '/login') {
        return NextResponse.redirect(new URL('/login', req.url))
    }

    return res
}

export const config = {
    matcher: ['/', '/account'],
}

./pages/login/index.tsx

import AuthForm from '../../components/auth/auth-form'

export default function Login() {
    return (
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4 h-screen">
            <div className="flex flex-col justify-center">
                <div className="text-center mx-auto md:max-w-2xl">
                    <h1 className="header">Supabase Auth + Storage</h1>
                    <p>
                        Experience our Auth and Storage through a simple profile management example. Create a user profile and
                        upload an avatar image. Fast, simple, secure.
                    </p>
                </div>
            </div>
            <div className="flex flex-col justify-center">
                <div className="mx-auto w-1/2">
                    <AuthForm />
                </div>
            </div>
        </div>
    )
}

./components/auth/auth-form.tsx

'use client'
import { Auth } from '@supabase/auth-ui-react'
import { ThemeSupa } from '@supabase/auth-ui-shared'
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { Database } from '../../types/database.types'

export default function AuthForm() {
    const supabase = createClientComponentClient<Database>()

    return (
        <Auth
            supabaseClient={supabase}
            view="magic_link"
            appearance={{ theme: ThemeSupa }}
            theme="dark"
            showLinks={false}
            providers={['discord']}
            redirectTo="http://localhost:3000/api/auth/callback"
        />
    )
}

./pages/api/auth/callback.ts

import { NextApiHandler } from 'next'
import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'

const handler: NextApiHandler = async (req, res) => {
    const { code } = req.query

    if (code) {
        const supabase = createPagesServerClient({ req, res })
        await supabase.auth.exchangeCodeForSession(String(code))
    }

    res.redirect('/account')
}

export default handler

./pages/account/index.tsx

import { createPagesServerClient } from '@supabase/auth-helpers-nextjs'

export default function Profile({ user }) {
    console.log(user)
    return <div>Hello {user.user_metadata.name}</div>
}

export const getServerSideProps = async (ctx) => {
    // Create authenticated Supabase Client
    const supabase = createPagesServerClient(ctx)
    // Check if we have a session
    const {
        data: { session },
    } = await supabase.auth.getSession()

    if (!session)
        return {
        redirect: {
            destination: '/',
            permanent: false,
        },
        }

    return {
        props: {
        initialSession: session,
        user: session.user,
        },
    }
}

./pages/account/account-form.tsx

'use client'
import { useCallback, useEffect, useState } from 'react'
import { Database } from '../../types/database.types'
import { Session, createClientComponentClient } from '@supabase/auth-helpers-nextjs'

export default function AccountForm({ session }: { session: Session | null }) {
    const supabase = createClientComponentClient<Database>()
    const [loading, setLoading] = useState(true)
    const [fullname, setFullname] = useState<string | null>(null)
    const [username, setUsername] = useState<string | null>(null)
    const [avatar_url, setAvatarUrl] = useState<string | null>(null)
    const user = session?.user

    const getProfile = useCallback(async () => {
        try {
            setLoading(true)

            let { data, error, status } = await supabase
                .from('profiles')
                .select(`full_name, username, avatar_url`)
                .eq('id', user?.id)
                .single()

            if (error && status !== 406) {
                throw error
            }

            if (data) {
                setFullname(data.full_name)
                setUsername(data.username)
                setAvatarUrl(data.avatar_url)
            }
        } catch (error) {
            alert('Error loading user data!')
        } finally {
            setLoading(false)
        }
    }, [user, supabase])

    useEffect(() => {
        getProfile()
    }, [user, getProfile])

    async function updateProfile({
            username,
            avatar_url,
        }: {
        username: string | null
        fullname: string | null
        avatar_url: string | null
    }) {
        try {
            setLoading(true)

            let { error } = await supabase.from('profiles').upsert({
                id: user?.id as string,
                full_name: fullname,
                username,
                avatar_url,
                updated_at: new Date().toISOString(),
            })
            if (error) throw error
            alert('Profile updated!')
        } catch (error) {
            alert('Error updating the data!')
        } finally {
            setLoading(false)
        }
    }

    return (
        <div className="form-widget p-4 rounded-md w-1/2 mx-auto">
            <div className="mb-4">
                <label htmlFor="email" className="text-sm font-medium text-gray-700">Email</label>
                <input
                    id="email"
                    type="text"
                    value={session?.user.email}
                    disabled
                    className="w-full px-3 py-2 mt-1 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-black"
                />
            </div>
            <div className="mb-4">
                <label htmlFor="fullName" className="text-sm font-medium text-gray-700">Full Name</label>
                <input
                    id="fullName"
                    type="text"
                    value={fullname || ''}
                    onChange={(e) => setFullname(e.target.value)}
                    className="w-full px-3 py-2 mt-1 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-black"
                />
            </div>
            <div className="mb-4">
                <label htmlFor="username" className="text-sm font-medium text-gray-700">Username</label>
                <input
                    id="username"
                    type="text"
                    value={username || ''}
                    onChange={(e) => setUsername(e.target.value)}
                    className="w-full px-3 py-2 mt-1 border border-gray-300 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-black"
                />
            </div>

            <div className="mb-4">
                <button
                    className="w-full px-4 py-2 text-white bg-blue-500 rounded-md hover:bg-blue-600"
                    onClick={() => updateProfile({ fullname, username, avatar_url })}
                    disabled={loading}
                >
                    {loading ? 'Loading ...' : 'Update'}
                </button>
            </div>

            <div>
                <form action="/auth/signout" method="post">
                    <button
                        className="w-full px-4 py-2 text-gray-700 bg-gray-300 rounded-md hover:bg-gray-400"
                        type="submit"
                    >
                        Sign out
                    </button>
                </form>
            </div>
        </div>
    );

}

./pages/index.tsx

const Home = () => {
    return (
      <div className="grid grid-cols-1 md:grid-cols-2 gap-4 h-screen">
      <div className="flex flex-col justify-center">
          <div className="text-center mx-auto md:max-w-2xl">
              <h1 className="header">You are home</h1>
          </div>
      </div>
  </div>
    )
  }
  
  export default Home  

enter image description here


Solution

  • Solved, and this is my solution to _app.tsx i have used my old _app.tsx and merge it with the _app.tsx to the supabase page directory _app.tsx

    import "../styles/globals.css";
    import App, { AppProps, AppContext } from 'next/app'
    
    import { createPagesBrowserClient } from '@supabase/auth-helpers-nextjs'
    import { SessionContextProvider, Session } from '@supabase/auth-helpers-react'
    import { useState } from 'react'
    
    function MyApp({ Component, pageProps }: AppProps<{ initialSession: Session }>) {
    
        // Create a new supabase browser client on every first render.
        const [supabaseClient] = useState(() => createPagesBrowserClient())
    
        return (
            <SessionContextProvider
            supabaseClient={supabaseClient}
            initialSession={pageProps.initialSession}
            >
            <Component {...pageProps} />
            </SessionContextProvider>
        )
    
    }
    
    MyApp.getInitialProps = async (appContext: AppContext) => {
        const appProps = await App.getInitialProps(appContext)
        const { router } = appContext;
        const { query } = router;
    
        return {
            pageProps: {
            ...appProps.pageProps,
            },
        }
    }
    
    export default MyApp;