Jon
Jon

Reputation: 77

How can I password protect every page in NextJS and Supabase, using the Supabase default auth helpers and UI?

I'm trying to add user authentication to every page in my NextJS project (pages, not app.) This tutorial was very helpful (and is exactly what I want to do) - https://alexsidorenko.com/blog/next-js-protected-routes/ - but I'm having trouble integrating Supabase's default auth UI and capabilities into that model (https://supabase.com/docs/guides/auth/auth-helpers/nextjs).

My basic goal is to move authentication branching into _app.tsx, rather than on each page:


// _app.tsx

import { useEffect, useState } from "react";
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider, useUser, useSession, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
import { AppProps } from 'next/app'
import { UserContext } from "@components/user"

function MyApp({Component, pageProps}: AppProps<{ initialSession: Session }>) {
  const [supabase] = useState(() => createBrowserSupabaseClient())
  const session = useSession()
  const user = useUser()

  console.log("session:" + session);
  console.log("user:" + user);


useEffect(() => {
    if (
    pageProps.protected
  ) {
    return <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />    
  }
}, [])

  return (
    <SessionContextProvider supabaseClient={supabase} session={session} initialSession={pageProps.initialSession}>
      <Component {...pageProps} />
    </SessionContextProvider>
  )
}
export default MyApp

A page I want to protect (for example, the index page) looks like this:

// index.tsx

import Account from "@components/account";

const Home = () => {
  return (
    <div>
          <Account session={session} />
    </div>
  )
}

export async function getStaticProps(context) {
  return {
    props: {
      protected: true,
    },
  }
}

export default Home

And then the Account component that's included on the index page is the Supabase out of the box profile panel, although it could be any content:

// @components/account.tsx

import { useState, useEffect } from 'react'

import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react'
import { Database } from '@utils/database.types'
type Profiles = Database['public']['Tables']['profiles']['Row']

export default function Account({ session }: { session: Session }) {
  const supabase = useSupabaseClient<Database>()
  const user = useUser()
  const [loading, setLoading] = useState(true)
  const [username, setUsername] = useState<Profiles['username']>(null)

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

  async function getProfile() {
    try {
      setLoading(true)
      if (!user) throw new Error('No user')

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

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

      if (data) {
        setUsername(data.username)
      }
    } catch (error) {
      alert('Error loading user data!')
      console.log(error)
    } finally {
      setLoading(false)
    }
  }

  async function updateProfile({
    username,    
  }: {
    username: Profiles['username']    
  }) {
    try {
      setLoading(true)
      if (!user) throw new Error('No user')

      const updates = {
        id: user.id,
        username,
        updated_at: new Date().toISOString(),
      }

      let { error } = await supabase.from('profiles').upsert(updates)
      if (error) throw error
      alert('Profile updated!')
    } catch (error) {
      alert('Error updating the data!')
      console.log(error)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" type="text" value={session.user.email} disabled />
      </div>
      <div>
        <label htmlFor="username">Username</label>
        <input id="username" type="text" value={username || ''} onChange={(e) => setUsername(e.target.value)} />
      </div>      
      <div>
        <button onClick={() => updateProfile({ username })} disabled={loading} >
          {loading ? 'Loading ...' : 'Update'}
        </button>
      </div>
      <div>
        <button onClick={() => supabase.auth.signOut()}>
          Sign Out
        </button>
      </div>
    </div>
  )
}

I think I have a fundamental misunderstanding of the relationship between protected routes and Supabase's use of session and user.

Any help would be very much appreciated.

Upvotes: 0

Views: 1418

Answers (1)

thorwebdev
thorwebdev

Reputation: 1152

I'd recommend using Next.js middleware for this: https://supabase.com/docs/guides/auth/auth-helpers/nextjs#auth-with-nextjs-middleware

import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(req: NextRequest) {
  // We need to create a response and hand it to the supabase client to be able to modify the response headers.
  const res = NextResponse.next()
  // Create authenticated Supabase Client.
  const supabase = createMiddlewareSupabaseClient({ req, res })
  // Check if we have a session
  const {
    data: { session },
  } = await supabase.auth.getSession()

  // Check auth condition
  if (session?.user.email?.endsWith('@gmail.com')) {
    // Authentication successful, forward request to protected route.
    return res
  }

  // Auth condition not met, redirect to home page.
  const redirectUrl = req.nextUrl.clone()
  redirectUrl.pathname = '/'
  redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
  return NextResponse.redirect(redirectUrl)
}

export const config = {
  matcher: '/middleware-protected/:path*',
}

Upvotes: 1

Related Questions