SERIES: Let's build SupaAuth #4
Redirect on Sign-In and Sign-Out
9 min read • –––
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
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.
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
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 typeAuthChangeEvent
and takes the following values -'SIGNED_IN' | 'SIGNED_OUT' | 'USER_UPDATED' | 'PASSWORD_RECOVERY'
The
session
is of typeSession
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)
// ... 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 booleanloggedIn
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
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.
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.
// ... 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, please take a second to tweet this article or share on LinkedIn so others can discover it. Or add to the discussion on Twitter.