My react app has the following routes and contexts:
const App = () =>
<AuthContextProvider>
<IntelContextProvider>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="login" element={<Authentication />} />
<Route path="register" element={<Registration />} />
<Route element={<RequireAuth />}>
<Route path="/" element={<Home />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</IntelContextProvider>
</AuthContextProvider>
Authentication uses an access token stored in memory (AuthContextProvider
) and a refresh token stored in a HttpOnly cookie.
My home path uses a protected route implemented as follows:
export const RequireAuth = () => {
const location = useLocation()
const {auth} = useAuth()
return (auth?.username ?
<Outlet/> :
<Navigate to='/login' state={{from: location}} replace/> )
}
If the user is not authenticated, it is redirected to /login without any issue. However, I need to do the opposite too:
If the user has a valid refresh token, when the page is refreshed or the user requests '/register' or '/login pages', then I want the route to be redirected to Home component again.
I tried to put do the following in the Authentication component:
const Authentication = () => {
const [authenticated, setAuthenticated] = useState(null)
const {values, errors, handleChange, handleSubmit} = useAuthenticationForm(validate)
const silentlySignIn = useSilentSignIn()
useEffect(() => {
const silentlyLogin = async () => {
const isAuthenticated = await silentlySignIn()
if(isAuthenticated) setAuthenticated(true)
}
silentlyLogin()
// eslint-disable-next-line
}, [])
return (
authenticated ? <Navigate to='/'/> :
<main className="authentication">
<form>
...
</form>
</main>
);
}
Here is my AuthContext:
export const AuthContextProvider = ({ children }) => {
const [auth, setAuth] = useState()
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
And Here is my useSignIn hook:
const useSignIn = () => {
const [success, setSuccess] = useState(false)
const [error, setError] = useState()
const { setAuth } = useAuth()
const signIn = async (payload) => {
try {
setError(null)
const { headers: { authorization: token }, data: { uuid } } = await axiosPrivate.post(`/login`, payload)
setAuth({ token, uuid, username: payload.username })
setSuccess(true)
} catch (error) {
console.log(error.message)
if (!error.response) setError('Sistema temporariamente indisponível.')
else if (error.response.status === 401) setError('Usuário ou senha inválidos.')
else setError('Algo deu errado.')
}
}
return [signIn, success, error]
}
Here is my useSilentSignIn (to get a new access token if the refresh token is still valid):
const useSilentSignIn = () => {
const { auth, setAuth } = useAuth()
const silentlySignIn = async () => {
try {
if (auth?.uuid) return false
const response = await axiosPrivate.get('/refresh-token')
const token = response.headers.authorization
const uuid = response.data.uuid
const username = response.data.username
setAuth(prev => ({ ...prev, token, uuid, username }))
return true
} catch (error) {
console.log('Logged out. Please sign in.')
return false
}
}
return silentlySignIn
}
I "solved" the problem, but it first renders the login, then navigates to '/' (due to React component lifecycle). It does not seem like a good solution, it is ugly, and I would need to do the same for '/register' or any similar route.
How to implement something efficient for such a problem?
Github of the project: https://github.com/lucas-ifsp/CTruco-front
Thanks
CodePudding user response:
Your authenticated
state has three possible states (yay JavaScript):
- Authenticated (true)
- Non-authenticated (false)
- Not yet known (null)
You could convert them to string enums for clarity, but for conciseness, this is how you would handle all three cases:
if (authenticated === null) return <Spinner /> // Or some other loading indicator
return (
authenticated ? <Navigate to='/'/> :
<main className="authentication">
<form>
...
</form>
</main>
);
CodePudding user response:
There's no need to add this logic to the Authentication
rendered on the "/login"
path. In this case you create another route protection component that does the inverse of the RequireAuth
component. This is commonly referred to as an "anonymous route" that you only want users that are not authenticated to access.
If the user is authenticated then render a redirect to any non-anonymous path, otherwise render the outlet for the nested route to render its element
into. While the auth status is being checked and still undefined, you can render null or any sort of loading indicator to make the route protection wait until the state value updates.
Example:
export const AnonymousRoute = () => {
const { auth } = useAuth();
if (auth === undefined) {
return null; // or loading indicator/spinner/etc...
}
return auth.username
? <Navigate to='/' replace />
: <Outlet/>;
}
...
<Routes>
<Route element={<Layout />}>
<Route element={<AnonymousRoute />}>
<Route path="login" element={<Authentication />} />
<Route path="register" element={<Registration />} />
</Route>
<Route element={<RequireAuth />}>
<Route path="/" element={<Home />} />
</Route>
<Route path="*" element={<NotFound />} />
</Route>
</Routes>