I have MERN application which uses http only cookies with jwt as an authentication. I made a provider in React to hold the user information. There are also protected routes on the front-end. On the backend I have a route which returns the current user if a cookie is present else I set the user to null. When the app starts, refreshes or the user changes I make this call. Everything works as expected.
The problem appears when I refresh the page or use the url bar to manually navigate to a certain page. Because in both cases the app refreshes there is a short amount of time where a user is not present until the fetch finishes. As a result I am always redirected to the homepage instead of the page I refreshed on or typed. I tried setting a 'Loading' state but it only works for the App Component.
Example: I am on '/details/1234' I click refresh and I am sent back to '/'.
Is there any way to fix this behaviour? Maybe some changes in the router or forcing React to not do anything until that fetch is finished? Thanks in advance.
Here is the code for my provider:
import { createContext, useEffect, useState } from 'react';
export const AuthContext = createContext();
function AuthContextProvider(props) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function initialUser() {
try {
const response = await fetch(`/api/users/profile`, {
headers: {
"Content-Type": "application/json"
},
method: "GET",
credentials: 'include'
});
setLoading(false);
const userData = await response.json();
return userData.message ? setUser(null) : setUser(userData)
} catch (error) {
console.log(error);
}
}
if (!user) {
initialUser();
}
}, [user]);
return (
<AuthContext.Provider value={{ user, loading, setUser }}>
{props.children}
</AuthContext.Provider>
)
}
export default AuthContextProvider
Here is the code for the App Component. The App Component is wrapped by the Provider and the Router in index.js
import { useContext } from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import './App.css';
import { AuthContext } from './context/AuthContext';
import Navigation from './components/Navigation/Navigation';
import Footer from './components/Footer/Footer';
import Homescreen from './components/HomeScreen/HomeScreen';
import LandingScreen from './components/LandingScreen/LandingScreen';
import SignScreen from './components/SignScreen/SignScreen';
import Search from './components/Search/Search';
import Genres from './components/Genres/Genres';
import Details from './components/Details/Details';
import Watch from './components/Watch/Watch';
import Profile from './components/Profile/Profile';
function App() {
const { user, loading } = useContext(AuthContext);
return (
!loading
? <div className="App">
<Navigation user={user} />
<Switch>
<Route exact path="/" render={() => (!user ? <LandingScreen /> : <Homescreen />)} />
<Route path="/sign-in" render={() => (!user ? <SignScreen /> : <Redirect to="/" />)} />
<Route path="/sign-up" render={() => (!user ? <SignScreen /> : <Redirect to="/" />)} />
<Route path="/profile" render={() => (!user ? <Redirect to="/sign-in" /> : <Profile />)} />
<Route path="/search" render={() => (!user ? <Redirect to="/sign-in" /> : <Search />)} />
<Route path="/movies/:genre" render={() => (!user ? <Redirect to="/sign-in" /> : <Genres />)} />
<Route path="/details/:id" render={() => (!user ? <Redirect to="/sign-in" /> : <Details />)} />
<Route path="/watch/:id" render={() => (!user ? <Redirect to="/sign-in" /> : <Watch />)} />
</Switch>
<Footer />
</div>
: null
);
}
export default App;
You are updating the loading state to false before you update the user state. You should update the user data first then update the loading state. When you update the loading state before the asynchronous JSON response handling there will be at least 1 render cycle between them, allowing the router to render the routes with the still undetermined user state. I suggest using the finally block of the try/catch so that no matter what, when the fetch processing completes the loading state is updated.
function AuthContextProvider(props) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function initialUser() {
try {
const response = await fetch(`/api/users/profile`, {
headers: {
"Content-Type": "application/json"
},
method: "GET",
credentials: 'include'
});
const userData = await response.json();
userData.message ? setUser(null) : setUser(userData)
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}
if (!user) {
initialUser();
}
}, [user]);
return (
<AuthContext.Provider value={{ user, loading, setUser }}>
{props.children}
</AuthContext.Provider>
)
}
Related
Hello guys so I have the following problem:
I have a login form after user successfully provide the right info for signing in, I store the user object and access token in the AuthContext
I protect the home route using the context
Problem => the react state inside the context is not being updated
-[Edit] Solution => I found the solution and it was only changing the following code:
const {setAuth} = useAuth();
to the code:
const {setAuth} = useAuth({});
-[Edit 2] => Because I am a beginner I also discovered that navigation between components with anchor tag () or window location cause the lose of state data so I should use Link from react router dom to avoid re-rendering
AuthProvider.js
import { createContext, useState } from "react";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
App.js
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
{/* public routes */}
<Route path="auth" element={<AuthLayout />}>
<Route path="login" element={<Login />} />
<Route path="register" element={<Register />} />
<Route path="unauthorized" element={<Unauthorized />} />
</Route>
{/* we want to protect the following routes */}
{/* RequireAuth having outlet => return child only if context auth has user object */}
<Route element={<RequireAuth />}>
<Route path="home" element={<Home />} />
</Route>
{/* catch all */}
</Route>
</Routes>
);
}
export default App;
RequireAuth.js [ Problem is here, the auth is always empty]
const RequireAuth = () => {
const { auth } = useAuth();
const location = useLocation();
return auth?.userObject ? (
// we used from location and replace so we want to save the previous location of the visited page by user so he is able to go back to it
<Outlet />
) : (
<Navigate to="/auth/login" state={{ from: location }} replace />
);
};
export default RequireAuth;
Login.js [Here I update the state]
const handleFormSubmission = async (data,e) => {
e.preventDefault();
try {
const response = await axios.post(
ApiConstants.LOGIN_ENDPOINT,
JSON.stringify({
Email: email,
Password: password,
}),
{
headers: ApiConstants.CONTENT_TYPE_POST_REQUEST,
}
);
//const roles = ;
const userObject = response?.data;
const accessToken = response?.data?.token;
setAuth({ userObject, password, accessToken });
console.log(userObject);
console.log(accessToken);
console.log(password);
message.success("You are successfully logged in");
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./context/AuthProvider";
import "./styles/ant-design/antd.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<Routes>
<Route path="/*" element={<App />} />
</Routes>
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);
Did you import setAuth correctly?
Did you call setAuth({}) inside handleFormSubmission function?
I have gone through the code and there are few suggestions which will make code work.
import { createContext, useState } from "react";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
function updateAuth(authVal) {
console.log("update auth called with value", authVal);
setAuth(authVal);
}
return (
<AuthContext.Provider value={{ auth, updateAuth }}>
{children}
</AuthContext.Provider>
);
};
export default AuthContext;
Also we need few changes in the consumer code which looks like below
const { updateAuth } = useContext(AuthContext);
const handleFormSubmission = async (e) => {
e.preventDefault();
try {
const response = await axios.get("https://catfact.ninja/breeds?
limit=1");
//const roles = ;
const userObject = response?.data;
console.log("cats api data response is ", userObject);
updateAuth(userObject);
//setAuth({ userObject, password, accessToken });
} catch (err) {
console.error("some error");
}
};
I have made sample application on codesandbox for the same as well. Please find url as https://codesandbox.io/s/funny-scott-n7tmxs?file=/src/App.js:203-689
I hope this help and have a wonderful day :)
I'm practicing fundamentals by creating a basic authentication using PERN. My user auth routes are working on the front- and back-end. Logging in and out with JWT functionality works, but when I try to set up a ternary operator that will remember the user's authorization status even when you click to another page, it breaks everything. Here's the code for my client entry point.
import React, { Fragment, useState, useEffect } from "react";
import './App.css';
import { BrowserRouter as Router, Switch, Route, Redirect } from "react-router-dom";
//components
import Dashboard from "./components/Dashboard";
import Login from "./components/Login";
import Register from "./components/Register";
function App() {
async function isAuth() {
try {
const response = await fetch("http://localhost:4000/api/v1/auth/verify/", {
method: "GET",
headers: { jwt_token : localStorage.token }
});
const parseRes = await response.json();
//parseRes === true ? setIsAuthenticated(true) :
//setIsAuthenticated(false);
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
isAuth()
})
const [isAuthenticated, setIsAuthenticated] = useState(false);
const setAuth = boolean => {
setIsAuthenticated(boolean);
};
return (
<Fragment>
<Router>
<div className="container">
<Switch>
<Route
exact
path="/login"
render={props =>
!isAuthenticated ? (
<Login {...props} setAuth={setAuth} />
) : (
<Redirect to="/dashboard" />
)
}
/>
<Route
exact
path="/register"
render={props =>
!isAuthenticated ? (
<Register {...props} setAuth={setAuth} />
) : (
<Redirect to="/dashboard" />
)
}
/>
<Route
exact
path="/dashboard"
render={props =>
isAuthenticated ? (
<Dashboard {...props} setAuth={setAuth} />
) : (
<Redirect to="/login" />
)
}
/>
</Switch>
</div>
</Router>
</Fragment>
);
}
export default App;
and this is the code for my back-end dashboard routes, where I think the miscommunication may be happening? Any help would be greatly appreciated.
const router = require("express").Router();
const db = require("../db");
const authorization = require('../middleware/authorization');
router.get("/", authorization, async(req, res) => {
try {
// req.user comes from authorization, contains jwt payload
const user = await db.query("SELECT user_name, user_email FROM users WHERE user_id = $1", [req.user]);
res.json(user.rows[0]);
} catch (err) {
console.err(err.message)
res.status(500).json("Server Error");
}
});
module.exports = router;
Sorry about the confusion! I realized I didn't leave enough information, but I also traced down the source of the issue almost immediately after posting. It was a simple issue with the token variable being mislabeled.
I need to make a router system that distinguishes between authenticated and non-authenticated users.
What I want to achieve with this is that authed users can see their dashboard and personal details, but unauthed users can only see the landing page, the login form and the register form.
I currently have context management so I know whether a user is logged in or not. I just need to figure out all the redirects and routing related things.
My code is making all sorts of "strange" things. I don't really know where I have messed it up.
AppContent.js
This is where all the routing happens.
AppLayout is just a layout wrapper with a header and a footer that must be visible only when logged in.
const AppContent = () => (
<Router>
<Switch>
<PublicRoute component={() => (<h1>Landing page</h1>)} path='/' exact />
<PublicRoute component={LoginPage} path='/login' exact />
<PublicRoute component={() => (<h1>Register page</h1>)} path='/register' exact />
<PrivateRoute component={Home} path='/dashboard' exact />
<PrivateRoute component={Demo} path='/demo' exact />
<PrivateRoute component={Profile} path='/profile' exact />
<PublicRoute component={() => (<h1>not found</h1>)} path='*' />
<PrivateRoute component={() => (<h1>not found</h1>)} path='*' />
</Switch>
</Router>
);
PublicRoute.js
const PublicRoute = ({ component: Component, ...rest }) => {
const { user, isLoading } = useContext(AuthContext);
if (isLoading) {
return (
<h1>Loading...</h1>
);
}
return (
<Route {...rest} render={(props) => (user ? <Redirect to='/dashboard' /> : <Component {...props} />)} />
);
};
PrivateRoute.js
const PrivateRoute = ({ component: Component, ...rest }) => {
const { user, isLoading } = useContext(AuthContext);
if (isLoading) {
return (
<h1>Loading...</h1>
);
}
return (
<Route
{...rest}
render={(props) => (user ? (
<AppLayout>
<Component {...props} />
</AppLayout>
) : <Redirect to='/login' />)}
/>
);
};
AuthStore.js
This is where I manage my auth context.
const AuthStore = ({ children }) => {
const [token, setToken] = useState();
const [user, setUser] = useState();
const [isLoading, setIsLoading] = useState(false);
const setUserToken = async (userToken) => {
localStorage.setItem('token', userToken);
setToken(userToken);
try {
const decodedToken = jwtDecode(userToken);
const currentUserData = await AuthService.getUserData(decodedToken.username);
setUser(currentUserData);
} catch (error) {
console.log(error.name);
}
};
useEffect(() => {
setIsLoading(true);
const localToken = localStorage.getItem('token');
if (localToken) {
setUserToken(localToken);
}
setIsLoading(false);
}, []);
return (
<AuthContext.Provider value={{
user, token, setUserToken, setUser, isLoading,
}}
>
{children}
</AuthContext.Provider>
);
};
Thank you so much.
EDIT
What I am really looking for:
When Unauthorized:
When searching for /dashboard, redirect to /login ✅
When searching for non existing route, redirect to /login ✅
When searching for another unauthorized route like /register, redirect to it fine ✅
When Authorized:
When searching for /login, redirect to /dashboard ✅
When searching for non existing route, redirect to a 404 page, preferably one of the two last routes, the ones that look like <PrivateRoute component={() => (<h1>not found</h1>)} path='*' ✅
When searching for another authorized route like /profile, redirect to it fine ❌ ➡️ It is currently reloading the routes, showing the login page and after half a second it shows /dashboard, but not /profile
According to React Router documentation, direct children of <Switch> component can only be <Route> or <Redirect> components from react-router-dom package.
All children of a <Switch> should be <Route> or <Redirect> elements. Only the first child to match the current location will be rendered.
Your <AppLayout /> component is the problem. Remove it from <Switch>and move it inside your auth pages.
edit
your auth provider is going trough async flow anytime you type in a new address in the browser and press enter, meaning every time you will have a state in your app where user is not logged in, then the components that are consuming this state are reacting to your change. also in the effect you should await setUserToken because loading is set to false before that async function resolves.
the last thing that is not working on your list is because you coded it like that ;), your public route redirects all calls it receives to the /dashboard if there is user, regardless if it should be a 404 route or existing route. i would suggest different approach. create a config where you declare if route is protected or not, then have only one AuthRoute component that will take all the props of the route, and based on this new flag and state of user in the context, do the work
const routes = [{ name: 'home', path: '/', exact: true, isProtected: false, component: Home }, { name: 'protected', path: '/dashboard', exact: true, isProtected: true, component: Dashboard }];
const AuthRoute = ({ name, path, exact, isProtected, component: Component }) => {
const { user } = useContext(userContext);
if (isProtected && !user) { // redirect to login }
if (!isProtected || (isProtected && user)) { // render route with props }
}
///...
<Switch>
{routes.map(route => <AuthRoute {...route} />
</Switch>
I was facing the same problems just 3 to 4 days back when a very kind person who is just amazing, helped me out on the reactiflux server of discord.
You might be trying to go to one protected route and that would work but the other protected route will not work right??
All you have to do is create a Higher Order Component (HOC) in a separate file which you can maybe call protectedRoute.js and inside this file:
import React, { useState, useEffect } from "react";
import { Redirect } from "react-router-dom";
export function protectedRoute(Component) {
return function ComponentCheck(props) {
const [check, setCheck] = useState(false);
const [currentUser, setCurrentUser] = useState(false);
useEffect(() => {
const user = false;
setCurrentUser(user);
setCheck(true);
}, []);
const RedirectToLogin = <Redirect to="/" />;
return check && (currentUser ? <Component {...props} /> : RedirectToLogin);
};
}
In the above file the '/' route in my case is the login page but instead of const RedirectToLogin = <Redirect to='/' /> you can say const RedirectToLogin = <Redirect to='/login' />
and then you can restrict a route like for example I want the /users route to be accessible only if user === true. So for that i can in the users.js file say:
import { protectedRoute } from "./protectedRoute";
const Users = () => {
return <h2>Users</h2>;
};
export default protectedRoute(Users);
and then I also want the /dashboard route to be protected so:
import { protectedRoute } from "./protectedRoute";
const Dashboard = () => {
return <h2>Dashboard</h2>;
};
export default protectedRoute(Dashboard);
but I dont want the /home route to be protected so:
const Home = () => {
return <h2>Home</h2>;
};
export default Home;
app.js
const [isLoggedIn, setIsLoggedIn] = useState(false);
useEffect(() => {
fetch('http://localhost:5000/usersystem/user/isloggedin', {
method: 'GET',
credentials: 'include',
mode: 'cors'
}).then(response => response.json())
.then(response => {
setIsLoggedIn(response.success);
})
}, []);
return (
<div className="App">
<AppContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
<Routes />
</AppContext.Provider>
</div>
);
}
routes.js
export default function Routes() {
return (
<Switch>
<ProtectedRoute exact path="/">
<Dashboard />
</ProtectedRoute>
<Route exact path="/login">
<Login />
</Route>
<Route>
<Notfound />
</Route>
</Switch>
);
}
protectedroute.jsx
import React from "react";
import { Route, Redirect, useLocation } from "react-router-dom";
import { useAppContext } from "../libs/ContextLib";
export default function ProtectedRoute({ children, ...rest }) {
const { pathname, search } = useLocation();
const { isLoggedIn } = useAppContext();
return (
<Route {...rest}>
{isLoggedIn ? (
children
) : (
<Redirect to={
`/login?redirect=${pathname}${search}`
} />
)}
</Route>
);
}
I have a node express app on port 5000, which handles user authentication with passport and cookie session. It works all good, the session cookies gets to the browser, and can read it and can send it back to authenticate the user.
However, in react, when i refresh the browser the user gets redirected to the login page instead of the dashboard, even with the session cookies in the browser.
It feels like the setIsLoggedIn(response.success) in app.js, wont update isLoggedIn before it gets to the ProtectedRoute and redirects to login.
Im into this for 4 hours now, and couldnt solve it. Can someone please suggest me a way to do this, or tell me what is the problem, so when i refresh the page it wont redirect to login? Thanks.
Your component is rendered once when you create the component.
Your setIsLoggedIn will update the state and re-render it.
You could put in a Loader to wait for the data to come before displaying the page you want.
E.g:
function MyComponent () {
const [isLoggedIn, setIsLoggedIn] = useState(null);
useEffect(() => {
fetch('http://localhost:5000/usersystem/user/isloggedin', {
method: 'GET',
credentials: 'include',
mode: 'cors'
}).then(response => response.json())
.then((response, err) => {
if(err) setIsLoggedIn(false); // if login failure
setIsLoggedIn(response.success);
})
}, []);
if(isLoggedIn === null) return (<div>Loading</div>);
// this is only rendered once isLoggedIn is not null
return (
<div className="App">
<AppContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
<Routes />
</AppContext.Provider>
</div>
);
}
I am using React-Router V4 and I want to hand over an 'authenticated' prop to decide wether to send the user to a login page or to the requested page:
PrivateRoute.js
import React from "react";
import {
Route,
Redirect,
} from "react-router-dom";
const PrivateRoute = ({ component: Component, authenticated, ...rest }) => (
<Route
{...rest}
render={props =>
authenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
)
export default PrivateRoute;
My App.js looks like this:
class App extends Component {
is_authenticated() {
const token = localStorage.getItem('access_token');
//if token is not expired send on the way
if (token && jwt_decode(token).exp > Date.now() / 1000) {
return true;
} else {
// if token is expired try to refresh
const refresh_token = localStorage.getItem('refresh_token');
if(refresh_token) {
axios.post(config.refresh_url,{},{
headers: {"Authorization": `Bearer ${refresh_token}`},
"crossdomain": true,
mode: 'no-cors',}).then(
response => {
const access_token = response.data.access_token;
localStorage.setItem('access_token', access_token)
return access_token ? true : false
}
)
}
}
return false
}
render() {
const client = this.client;
return (
<Router>
<div>
<Route exact path="/login" component={Login} />
<PrivateRoute exact path="/" component={Home} authenticated={this.is_authenticated()} />
</div>
</Router>
);
}
}
export default App;
Since the Axios Call is async the component renders before the call is finished.
How can I make the render wait for the token to be refreshed?
I'd keep isAuthenticated in the App components state. Then, you can use setState in the isAuthenticated call to cause a re-render on the result. This will also be important when the tokens expire.