Skip to content

Commit

Permalink
feat: Use context provider to ensure 1 onauthstatechange listener exi…
Browse files Browse the repository at this point in the history
…st for the whole app
  • Loading branch information
weaponsforge committed Mar 15, 2023
1 parent 654c3b5 commit eb91ce2
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 128 deletions.
52 changes: 28 additions & 24 deletions client/src/common/auth/protectedpage/index.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
import { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { useRouter } from 'next/router'
import { useAuth } from '@/lib/hooks/useAuth'

import LoadingCover from '@/common/layout/loadingcover'
import WithAuth from '@/common/auth/withauth'

/**
* HOC that listens for the Firebase user auth info from the global "user" redux states set by the useAuth() hook.
* Displays a loading cover page while waiting for the remote authentication info to settle in.
* Displays and returns the contents (children) of a tree if there is a signed-in user from the remote auth.
* Displays and returns the Component parameter if there is user data from the remote auth.
* Redirect to the /login page if there is no user info after the remote auth settles in.
* @param {Component} children
* @returns {Component}
* @param {Component} Component
* @returns {Component} (Component parameter or a Loading placeholder component)
* Usage: export default ProtectedPage(MyComponent)
*/
function ProtectedPage ({ children }) {
const router = useRouter()
const { authLoading, authUser } = useSelector(state => state.user)
function ProtectedPage (Component) {
function AuthListener (props) {
const router = useRouter()
const { authLoading, authError, authUser } = useAuth()

useEffect(() => {
if (!authLoading && !authUser) {
router.push('/login')
}
}, [authUser, authLoading, router])
useEffect(() => {
if (!authLoading && !authUser) {
router.push('/login')
}
}, [authUser, authLoading, router])

const ItemComponent = () => {
if (authLoading) {
return <LoadingCover />
} else if (authUser) {
return <div>
{children}
</div>
} else {
return <LoadingCover />
const ItemComponent = () => {
if (authLoading) {
return <LoadingCover />
} else if (authUser) {
return <Component {...props} />
} else {
return <LoadingCover authError={authError} />
}
}

return (<ItemComponent />)
}

return (<ItemComponent />)
return AuthListener
}

export default WithAuth(ProtectedPage)
export default ProtectedPage
71 changes: 0 additions & 71 deletions client/src/common/auth/withauth/index.js

This file was deleted.

11 changes: 5 additions & 6 deletions client/src/common/layout/header/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// REACT
import { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { authSignOut } from '@/lib/store/users/userThunk'
import { useDispatch } from 'react-redux'

// NEXT
import Link from 'next/link'
Expand All @@ -28,10 +27,10 @@ import HowToRegIcon from '@mui/icons-material/HowToReg'
// LIB
import { Avalon } from '@/lib/mui/theme'
import { useSyncLocalStorage } from '@/lib/hooks/useSync'
import { useAuth } from '@/lib/hooks/useAuth'

// VARIABLES
const pages = ['about']
// const settings = ['Profile', 'Account', 'Dashboard', 'Logout']
const settings = [
{
name: 'Profile',
Expand All @@ -56,7 +55,7 @@ function Header() {
const [anchorElNav, setAnchorElNav] = useState(null)
const [anchorElUser, setAnchorElUser] = useState(null)
const [activeTheme, setActiveTheme] = useSyncLocalStorage('activeTheme')
const authUser = useSelector(state => state.user.authUser)
const { authUser, authSignOut } = useAuth()
const dispatch = useDispatch()
const router = useRouter()

Expand Down Expand Up @@ -203,9 +202,9 @@ function Header() {
onClose={handleCloseUserMenu}
>
{settings.map((setting, id) => {
return (setting === 'Logout')
return (setting.name === 'Logout')
? <MenuItem key={id} onClick={() => dispatch(authSignOut())}>
<Typography textAlign="center">{setting}</Typography>
<Typography textAlign="center">{setting.name}</Typography>
</MenuItem>
: <MenuItem key={id} onClick={() => handleClickNavMenu(setting.route)}>
<Typography textAlign="center">{setting.name}</Typography>
Expand Down
8 changes: 7 additions & 1 deletion client/src/common/layout/loadingcover/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Page from '@/common/layout/page'
import TransparentBox from '@/common/ui/transparentbox'
import { useSyncLocalStorage } from '@/lib/hooks/useSync'

function LoadingCover () {
function LoadingCover ({ authError }) {
const [activeTheme] = useSyncLocalStorage('activeTheme')

return (
Expand All @@ -33,6 +33,12 @@ function LoadingCover () {
color={(activeTheme === 'light') ? 'dark' : 'primary'}
/>
</span>

{authError &&
<Typography variant='label' color='error'>
{authError}
</Typography>
}
</TransparentBox>
</Box>
</Page>
Expand Down
81 changes: 81 additions & 0 deletions client/src/lib/hooks/useAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { useEffect, createContext, useContext } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { authErrorReceived, authReceived } from '@/store/users/userSlice'
import { authSignOut } from '@/lib/store/users/userThunk'
import { auth, onAuthStateChanged } from '@/lib/utils/firebase/config'

/**
* Context provider that listens for the Firebase user auth info using the Firebase onAuthStateChanged() method.
* Sets the global Firebase user auth details in the user { authUser, authLoading, authError, authStatus } redux store.
* Also returns the user { authUser, authLoading, authError, authStatus } redux data for convenience.
* @returns {Object} authUser - partial, relevant signed-in Firebase user data
* @returns {Bool} authLoading - Firebase auth status is being fetched from Firebase (from intial page load or during sign-out)
* @returns {String} authError - Firebase authentication error
* @returns {String} authStatus - Descriptive Auth status info. One of USER_STATES
* Usage: const { authUser, authLoading, authError, authStatus } = useAuth()
*/

const AuthContext = createContext()

export function AuthProvider ({ children }) {
const authUser = useFirebaseAuth()
return <AuthContext.Provider value={authUser}>{children}</AuthContext.Provider>
}

export const useAuth = () => {
return useContext(AuthContext)
}

function useFirebaseAuth () {
const { authUser, authLoading, authStatus, authError } = useSelector(state => state.user)
const dispatch = useDispatch()

useEffect(() => {
const handleFirebaseUser = async (firebaseUser) => {
if (firebaseUser) {
// Check if user is emailVerified
if (!firebaseUser?.emailVerified ?? false) {
dispatch(authSignOut('Email not verified. Please verify your email first.'))
return
}

try {
// Retrieve the custom claims information
const { claims } = await firebaseUser.getIdTokenResult()

if (claims.account_level) {
// Get the firebase auth items of interest
dispatch(authReceived({
uid: firebaseUser.uid,
email: firebaseUser.email,
name: firebaseUser.displayName,
accessToken: firebaseUser.accessToken,
emailVerified: firebaseUser.emailVerified,
account_level: claims.account_level
}))
} else {
// User did not sign-up using the custom process
dispatch(authSignOut('Missing custom claims'))
}
} catch (err) {
dispatch(authErrorReceived(err?.response?.data ?? err.message))
dispatch(authReceived(null))
}
} else {
// No user is signed-in
dispatch(authReceived(null))
}
}

const unsubscribe = onAuthStateChanged(auth, handleFirebaseUser)
return () => unsubscribe()
}, [dispatch])

return {
authUser,
authLoading,
authStatus,
authError,
authSignOut
}
}
1 change: 1 addition & 0 deletions client/src/lib/store/users/userThunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export const authSignOut = createAsyncThunk('auth/signout', async(errorMessage =
return thunkAPI.rejectWithValue(err?.response?.data ?? err.message)
}
})

5 changes: 4 additions & 1 deletion client/src/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '@/styles/globals.css'
// Redux
import { Provider } from 'react-redux'
import { store } from '@/store/store'
import { AuthProvider } from '@/lib/hooks/useAuth'

// MUI
import createEmotionCache from '@/lib/mui/createEmotionCache'
Expand All @@ -31,7 +32,9 @@ export default function MyApp(props) {
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Provider store={store}>
<Component {...pageProps} />
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</Provider>
</ThemeProvider>
</CacheProvider>
Expand Down
3 changes: 1 addition & 2 deletions client/src/pages/account/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { usePromise, RequestStatus } from '@/lib/hooks/usePromise'

import AccountComponent from '@/components/account'
import messages from './messages'
import WithAuth from '@/common/auth/withauth'

const defaultState = {
loading: true,
Expand Down Expand Up @@ -90,4 +89,4 @@ function Account () {
)
}

export default WithAuth(Account)
export default Account
8 changes: 2 additions & 6 deletions client/src/pages/dashboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ import ProtectedPage from '@/common/auth/protectedpage'
import DashboardComponent from '@/components/dashboard'

function Dashboard () {
return (
<ProtectedPage>
<DashboardComponent />
</ProtectedPage>
)
return (<DashboardComponent />)
}

export default Dashboard
export default ProtectedPage(Dashboard)
3 changes: 1 addition & 2 deletions client/src/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import HomeComponent from '@/components/home'
import WithAuth from '@/common/auth/withauth'
import { useSyncLocalStorage } from '@/lib/hooks/useSync'
import { getRandomJoke } from '@/lib/services/random'
import { useEffect, useState } from 'react'
Expand Down Expand Up @@ -37,4 +36,4 @@ function Index() {
)
}

export default WithAuth(Index)
export default Index
Loading

0 comments on commit eb91ce2

Please sign in to comment.