clouddatabasereal-time

SERIES: Let's build SupaAuth #4

Redirect on Sign-In and Sign-Out

Aftab Alam

Aftab AlamApril 20, 2021

9 min read–––

Aftab Alam

You're on part 4 of the 6-part series Let's build SupaAuth where You'll build an authentication system using Supabase and Next.js

4

Redirect on Sign-in and Sign-out

In the previous part of this series, we pulled all of the authentication-related state, types, and functions out of the page component, and shared them through an app-wide accessible AuthContext. Tapping into this context is as simple as using an useAuth hook. The structure we've created will help us manage, extend and scale the authentication capabilities of the application efficiently.

Without further ado, let's explore the missing part of the authentication journey we've charted so far. In this part -

  • We'll create a protected /profile page
  • We'll only allow the signed-in users to view this page
  • We'll allow the signed-in user's to logout from this page
  • As soon as the user's logs out, we'll redirect them to the login page

Since, this all starts with first having something that we should protect, let's build something our registered users should come for.

Create a Profile Page

Let's create a very simple profile page that won't do more than what's necessary to

  • show the logged-in user's information
  • give an option to logout the user

Place the following code in a profile.tsx file under the /pages directory.

profile.tsx
import Link from 'next/link'
import Layout from '~/components/Layout'

const ProfilePage = ({}) => {
    return (
        <Layout useBackdrop={false}>
            <div className="h-screen flex flex-col justify-center items-center relative">
                <h2 className="text-3xl my-4">Howdie, Explorer!</h2>
                <small className="mb-2">You've landed on a protected page. Please <Link href="/">log in</Link> to view the page's full content </small>
                <div>
                    <button className="border bg-gray-500 border-gray-600 text-white px-3 py-2 rounded w-full text-center transition duration-150 shadow-lg">Sign Out</button>
                </div>
            </div>
        </Layout>
    )
}

export default ProfilePage

Once there, it should look somewhat like

Since, the /profile and / route could be navigated from several different places, let's create a file src/config.ts and place the following code

config.ts
export const ROUTE_HOME = '/profile'
export const ROUTE_AUTH = '/'

Let your known user's in

We've successfully managed to sign up and log in our users in the previous articles, but we're not doing anything on the successful log-ins yet. We'll do the rest of the orchestration now.

Know, when your user's do anything Auth

Sign in, Sign up, etc. are very important, and connected sets of events your users produce while navigating in an application. Supabase like many of the solutions in the Auth Space (Amazon Cognito, Firebase, etc.) has a concept of an authentication event hub, built around these events. Different user actions in the system like sign in, sign up, password recovery, etc. generate an event in the UI thread that you could listen to and act accordingly. Whenever a user performs any of these events

  • Sign In
  • Sign Out
  • User Updates
  • Password Recovery

The UI is notified of an AuthChangeEvent. Listners/Subsrcriber functions can listen to these events by associating themselves through an supabase.auth.onAuthStateChange(async(event, session) => { ... }) where

  • The event parameter is of type AuthChangeEvent and takes the following values - 'SIGNED_IN' | 'SIGNED_OUT' | 'USER_UPDATED' | 'PASSWORD_RECOVERY'

  • The session is of type Session that Supabase creates post a successful login, and it could be checked to validate the state of the user's authenticity and decide whether it should be okay to let a user access some resource or not.

In a conventional SPA, transitions are not full-page reloads, and views are created and destroyed as a user navigates, thus, observing for auth change events through one single hub is a more reliable and manageable way to make decisions based on the user's state.

Act, as your App loads

To go with the plan of this article

  • We'll check a user's state on page reload and perform redirection to an internal page(/profile) if authenticated.
  • We'll attach a watcher method to redirect the user to the login page / on signing out

Implementing the above scenarios will ensure that a logged-in user is always redirected to the inner /profile page and not the sign-in page until exited.

As this behavior can only be attached once a page is loaded we'll use a useEffect hook. In the process, we'll also create few more context-internal states that could be used by any component to easily identify the user's current state.

Open AuthContext.tsx and add the following code inside the AuthProvider (where you've defined the states previously)

AuthContext.tsx
// ... previous code unchanged
const [ user, setUser ] = useState<User>(null)
const [ userLoading, setUserLoading ] = useState(true)
const [ loggedIn, setLoggedin ] = useState(false)
// ... previous code unchanged

Although user object should be enough here, we're creating a boolean loggedIn state for checking logged-in condition a bit more simply.

As the page component is done loading, we'll immediately

  • Set the user object(if found)
  • Mark loggedIn truthy, and
  • Redirect the user to the /profile page.

userLoading could help us show an indicator while the user object is not done loading.

useEffect(() => {
    const user = supabase.auth.user() // returns the `user` if there's an active session avaialable

    if (user) {
        setUser(user)
        setUserLoading(false)
        setLoggedin(true)
        Router.push(ROUTE_HOME) // Your users will automatically land on the `/profile` page on page load
    }

}, [])

Act, as your user's sign-in

Similarly, when there's an AuthChange event trigger, based on the availability of the user object (user login scenario), we'll populate the user object and immediately redirect the user to the /profile page

AuthContext.tsx
useEffect(() => {
    const user = supabase.auth.user()

    setUserLoading(false)
    if (user) {
        setUser(user)
        setLoggedin(true)
        Router.push(ROUTE_HOME)
    }

    const { data: authListener } = supabase.auth.onAuthStateChange(
        async (event, session) => {
        const user = session?.user! ?? null
        setUserLoading(false)
        if (user) {
            setUser(user)
            setLoggedin(true)
            Router.push(ROUTE_HOME) // Your users will automatically be redirected to the `/profile` page on logging in
        }
    })

    return () => {
        authListener.unsubscribe() // We'll simply unsubscribe from listening to the events when the user navigates away from our App.
    }
}, [])

Let the signed-in user's out!

Since we're not allowing for signOut's anywhere till now, let's add a signOut function for our users to sign out. Sign-out can simply be invoked with a supabase.auth.signOut() call.

Let's add a method within the AuthProvider where previous signIn and signUp functions are placed.

AuthContext.tsx
const signOut = async () => await supabase.auth.signOut()

Act, as your user's sign-out

As we populated the user object and redirect them to the /profile page on signIn, we'll have to flush the state holding the user object and immediately send the logged-in user to the login / page on the sign-out.

Add the following code in the else condition of the onAuthStateChange listener

setUser(null) // nullify the user object
Router.push(ROUTE_AUTH) // redirect to the home page

Your useEffect hook should eventually look like

useEffect(() => {
    const user = supabase.auth.user()

    setUserLoading(false)
    if (user) {
        setUser(user)
        setLoggedin(true)
        Router.push(ROUTE_HOME)
    }

    const { data: authListener } = supabase.auth.onAuthStateChange(
        async (event, session) => {
        const user = session?.user! ?? null
        setUserLoading(false)
        if (user) {
            setUser(user)
            setLoggedin(true)
            Router.push(ROUTE_HOME) // Your users will automatically be redirected to the `/profile` page on logging in
        } else { // new
            setUser(null) // new: nullify the user object
            Router.push(ROUTE_AUTH) // new: redirect to the home page
        }
    })

    return () => {
        authListener.unsubscribe() // We'll simply unsubscribe from listening to the events when the user navigates away from our App.
    }
}, [])

Share 'em all!

You now have all the methods for signUp, signIn, signOut, etc. as well as the sufficient orchestration needed to maintain your user's session, and their login state. All we need now is to tell what the AuthProvider context will be able to share and update the AuthProvider accordingly.

AuthContext.tsx
// ... previous code unchanged
// Import the User type from supabase library
import { User } from '@supabase/supabase-js'
// ... previous code unchanged

// Augment the Props with new share-able values/functions
export type AuthContextProps = {
    user: User, // new
    signUp: (payload: SupabaseAuthPayload) => void,
    signIn: (payload: SupabaseAuthPayload) => void,
    signOut: () => void, // new
    loggedIn: boolean, // new
    loading: boolean, // new
    userLoading: boolean //new
}

// ... previous code unchanged

Update the AuthContext.Provider with the newly created states and functions

// ... preceeding code unchanged
return (<AuthContext.Provider value={{
                user, // new
                signUp,
                signIn,
                signOut, // new
                loggedIn, // new
                loading, // new
                userLoading // // new
            }}>
            {children}
        </AuthContext.Provider>
    )
// ... succeeding code unchanged

Your AuthContext.Provider now holds all the essential states and functions the application components could ever need to tap into.

Glue `em together!

With AuthContext.Provider providing all the necessary states and functions, let's bring our /profile page to life.

import Link from 'next/link'
import { useAuth } from '~/lib/auth' // pull the `useAuth` hook
import Layout from '~/components/Layout'
import { SpinnerFullPage } from '~/components/Spinner'

const ProfilePage = ({}) => {

    // the absolutely essential methods we'll need from AuthContext
    const {
        user, // The logged-in user object
        loading, // loading state
        signOut // and a method to let the logged-in user sign out
    } = useAuth()

    if(loading) {
        return <SpinnerFullPage/>
    }

    return (
        <Layout useBackdrop={false}>
            <div className="h-screen flex flex-col justify-center items-center relative">
                <h2 className="text-3xl my-4">Howdie, { user && user.email ? user.email : 'Explorer' }!</h2>
                {!user && <small className="mb-2">You've landed on a protected page. Please <Link href="/">log in</Link> to view the page's full content </small>}
                {user && <div>
                    <button onClick={signOut} className="border bg-gray-500 border-gray-600 text-white px-3 py-2 rounded w-full text-center transition duration-150 shadow-lg">Sign Out</button>
                </div> }
            </div>
        </Layout>
    )
}

export default ProfilePage

See it in action

Open the login / page, provide the details, and press login. You should see yourself getting correctly redirected to the protected profile page with user information populated.

Try to reload the page. Everything should still work.

Try to signOut and re-visit the /profile page directly, and you should see something like

You could find all of the final changes(with few enhancements) here, and in a fix here

Takeaways/What's Next?

In this article, we've used the logged-in user's context to

  • redirect them directly to their /profile page both when the page loads, as well as when they log in.
  • determine what to show, and what not to show

We've also provided a way to log out, and redirected the user to the login / page.

In the next article, we'll see how we can take the security aspects of the protected pages even further by not letting anonymous users even access the protected pages.

Thank you for being till the end 🙌 . If you enjoyed this article, or learned something new, support me by clicking the share button below to reach more people and/or give me a follow on Twitter to see some other tips, articles, and things I learn and share there.