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
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;