I have following bit of code where <AuthContext>
provide authentication objects and methods, <ProtectedRoute>
component which guards the authentication required routes.
But the problem is when I login and refresh inside authenticated page, user
object fetched from the useAuth
hook returns null
, but if I use next/link
it works fine and user object is preserved.
AuthContext.tsx
const AuthContext = createContext<any>({})
export const useAuth = () => useContext(AuthContext)
type Props = {
children: React.ReactNode
}
const LOGIN = gql`...`
export const AuthContextProvider = ({ children }: Props) => {
const [user, setUser] = useState<object | null>(null)
const [loginUser, { data, loading, error }] = useMutation(LOGIN)
const router = useRouter()
useEffect(() => {
// in case of first login set user and token
// push user to route he belongs
if (data != null) {
if (data.authenticate.jwt !== null) {
console.log('Setting user with JWT decoding...')
const decoded = jwt_decode(data.authenticate.jwt)
setUser({
role: decoded.role,
id: decoded.id,
})
localStorage.setItem('token', data.authenticate.jwt)
}
}
const token = localStorage.getItem('token')
// if token is present set the user object
if (token !== null) {
console.log('Token present')
const decoded = jwt_decode(token)
// console.log(user)
console.log(`Decoded token : ${JSON.stringify(decoded)}`)
setUser({
role: decoded.role,
id: decoded.id,
})
} else {
setUser(null)
}
}, [data])
const login = async (username: string, password: string) => {
loginUser({
variables: {
username,
password,
},
})
}
const logOut = async () => {
console.log('Logging out ...')
setUser(null)
data = null
localStorage.removeItem('token')
}
// if (error) return <p>Submission error! ${error.message}</p>
return (
<AuthContext.Provider value={{ user, login, logOut }}>
{error || loading ? null : children}
</AuthContext.Provider>
)
}
ProtectedRoute.tsx
const PUBLIC_PATHS = ['/']
const ADMIN_ROUTES = [...]
const SUPERVISOR_ROUTES = [...]
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user } = useAuth()
const router = useRouter()
const [authorized, setAuthorized] = useState(false)
useEffect(() => {
console.log(`Current User : ${JSON.stringify(user)}`)
const isAdminRoute = ADMIN_ROUTES.includes(router.pathname)
const isSupervisorRoute = SUPERVISOR_ROUTES.includes(router.pathname)
// if an token is present send user to
// authorized route
if (user !== null) {
// @ts-ignore
if (user.role === 1 && isAdminRoute) {
setAuthorized(true)
console.log(`Pushing to ${router.pathname}`)
router.push(router.pathname)
// @ts-ignore
} else if (user.role === 2 && isSupervisorRoute) {
setAuthorized(true)
console.log(`Pushing to ${router.pathname}`)
router.push(router.pathname)
} else {
console.log(`Invalid role! user: ${JSON.stringify(user)}`)
}
} else {
setAuthorized(false)
console.log('Sending you to login page')
// "/" have the login page
router.push('/')
}
}, [user]) // router is avoided from deps to avoid infinite loop
return <>{authorized && children}</>
}
_app.tsx
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
const noAuthRequired = ['/']
function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page)
const router = useRouter()
return (
<ApolloProvider client={CLIENT}>
<AuthContextProvider>
{getLayout(
noAuthRequired.includes(router.pathname) ? (
<Component {...pageProps} />
) : (
<ProtectedRoute>
<Component {...pageProps} />
</ProtectedRoute>
),
)}
</AuthContextProvider>
</ApolloProvider>
)
}
export default App
Current possible workaround is to read JWT in the ProtectedRoute
to get user information.
I there anyway to preserve user
object on page refresh?