I have a web application that I've been developing for a little over a year and some change. The frontend is react w/ react-router-dom 5.2 to handle navigation, a service worker, to handle caching, installing, and webpush notifications, and then the backend is a Javalin application, which exists on top of Jetty.
I am using the context API to store some session details. When you navigate to my application, if you are not already logged in, then you won't have your information stored in that context yet, so you will be redirected to /login which will begin that process. The LoginLogout component simply redirects to an external authserver that handles the authentication workflow before redirecting back to another endpoint.
Here's the detail:
Here's my ProtectedRoute code
import { Redirect, Route, useLocation } from "react-router-dom";
import PropTypes from "prop-types";
import React, { useContext, useEffect, useState } from "react";
import { AuthContext } from "../Contexts/AuthProvider";
import LoadingComponent from "components/Loading/LoadingComponent";
import { server } from "variables/sitevars";
export const ProtectedRoute = ({ component: Component, ...rest }) => {
const { session, setSession } = useContext(AuthContext);
const [isLoading, setLoading] = useState(true);
const [isError, setError] = useState(false);
const cPath = useLocation().pathname;
//Create a checkAgainTime
const getCAT = (currTime, expireTime) => {
return new Date(
Date.now() + (new Date(expireTime) - new Date(currTime)) * 0.95
);
};
//See if it's time to check with the server about our session
const isCheckAgainTime = (checkTime) => {
if (checkTime === undefined) {
return true;
} else {
return Date.now() >= checkTime;
}
};
useEffect(() => {
let isMounted = true;
let changed = false;
if (isMounted) {
(async () => {
let sesh = session;
try {
//If first run, or previously not logged in, or past checkAgain
if (!sesh.isLoggedIn || isCheckAgainTime(sesh.checkAgainTime)) {
//Do fetch
const response = await fetch(`${server}/auth/check`);
if (response.ok) {
const parsed = await response.json();
//Set Login Status
if (!sesh.isLoggedIn && parsed.isLoggedIn) {
sesh.isLoggedIn = parsed.isLoggedIn;
sesh.webuser = parsed.webuser;
sesh.perms = parsed.perms;
if (sesh.checkAgainTime === undefined) {
//Set checkAgainTime if none already set
sesh.checkAgainTime = getCAT(
parsed.currTime,
parsed.expireTime
);
}
changed = true;
}
if (sesh.isLoggedIn && !parsed.isLoggedIn) {
sesh.isLoggedIn = false;
sesh.checkAgainTime = undefined;
sesh.webuser = undefined;
sesh.perms = undefined;
changed = true;
}
} else {
setError(true);
}
}
if (changed) {
setSession(sesh);
}
} catch (error) {
setError(true);
}
setLoading(false);
})();
}
return function cleanup() {
isMounted = false;
};
}, []);
if (isLoading) {
return <LoadingComponent isLoading={isLoading} />;
}
if (session.isLoggedIn && !isError) {
return (
<Route
{...rest}
render={(props) => {
return <Component {...props} />;
}}
/>
);
}
if (!session.isLoggedIn && !isError) {
return <Redirect to="/login" />;
}
if (isError) {
return <Redirect to="/offline" />;
}
return null;
};
ProtectedRoute.propTypes = {
component: PropTypes.any.isRequired,
exact: PropTypes.bool,
path: PropTypes.string.isRequired,
};
Here's the use of the Authprovider. I also went ahead and give login/logout a different endpoint:
export default function App() {
return (
<BrowserRouter>
<Switch>
<Suspense fallback={<LoadingComponent />}>
<Route path="/login" exact component={InOutRedirect} />
<Route path="/logout" exact component={InOutRedirect} />
<Route path="/auth/forbidden" component={AuthPage} />
<Route path="/auth/error" component={ServerErrorPage} />
<Route path="/offline" component={OfflinePage} />
<AuthProvider>
<ProtectedRoute path="/admin" component={AdminLayout} />
</AuthProvider>
</Suspense>
</Switch>
</BrowserRouter>
);
}
And this is the AuthProvider itself:
import React, { createContext, useState } from "react";
import PropTypes from "prop-types";
export const AuthContext = createContext(null);
import { defaultProfilePic } from "../../views/Users/UserVarsAndFuncs/UserVarsAndFuncs";
const AuthProvider = (props) => {
const [session, setSesh] = useState({
isLoggedIn: undefined,
checkAgainTime: undefined,
webuser: {
IDX: undefined,
LastName: "",
FirstName: "",
EmailAddress: "",
ProfilePic: defaultProfilePic,
},
perms: {
IDX: undefined,
Name: "",
Descr: "",
},
});
const setSession = (newSession) => {
setSesh(newSession);
};
return (
<AuthContext.Provider value={{ session, setSession }}>
{props.children}
</AuthContext.Provider>
);
};
export default AuthProvider;
AuthProvider.propTypes = {
children: PropTypes.any,
};
Update: Because it was asked for, here is my login/logout component, with the changes suggested (separated from the ProtectedRoute dependency)
import React, { useEffect, useState } from "react";
import { Redirect, useLocation } from "react-router-dom";
//Components
import LoadingComponent from "components/Loading/LoadingComponent";
import { server } from "variables/sitevars";
//Component Specific Vars
export default function InOutRedirect() {
const rPath = useLocation().pathname;
const [isError, setError] = useState(false);
const [isLoading, setLoading] = useState(true);
useEffect(() => {
let isMounted = true;
if (isMounted) {
(async () => {
try {
//Do fetch
const response = await fetch(`${server}/auth/server/data`);
if (response.ok) {
const parsed = await response.json();
if (rPath === "/login") {
window.location.assign(`${parsed.LoginURL}`);
} else if (rPath === "/logout") {
window.location.assign(`${parsed.LogoutURL}`);
}
}
} catch (error) {
setError(true);
}
})();
setLoading(false);
}
return function cleanup() {
isMounted = false;
};
}, []);
if (isLoading) {
return <LoadingComponent />;
}
if (isError) {
return <Redirect to="/offline" />;
}
}
How can I track down this issue?
UPDATE: I have done further troubleshooting and am now convinced that something is wrong with how I'm using context and that the service worker does not actually play a role in this issue. I've updated the post to reflect this.
UPDATE 2: I have done further simplification. The issue is assuredly that the context is not updating via setSession either prior to the page rendering the redirect component and redirecting back to login, or altogether.
UPDATE 3: I believe I found the issue, not positive but I think it's resolved. The bounty already being offered, if someone can explain why this happened, it's yours.
I think your issue is that you're trying to redirect to a server-side route. I ran into the same issue before. React-router is picking up the path and trying to serve it to you on the client side where there is no route available and it's using the default route which is causing the loop.
To resolve, create a route to mimic the server-side route, then redirect on the client side with react-router once the server-side workflow is completed.
EDIT: The suspense tag should be outside your switch iirc. And I would include a default route pointing to a 404 or similar.
The issue seems to be with your unordered conditions. What if you have not logged in but has error? There'll be no default render for this and will cause the application halt. When user tries to login the state is touched and and upon error, this won't match any render. When you put return null, it will first render that and after a while it will match to the correct condition and return that. So, you could order your conditions like:
if (isLoading) {
// return ...
}
if (isError) {
// return ...
}
if (session.isLoggedIn) {
// return ...
}
return <Redirect to="/login" />;
Here, we're first checking if there is any error, and if it is so, redirect to error page. Otherwise, route to the logged in component or redirect to the login page.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With