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;
Related
I would like to refresh the current page (home) when the user tries to go back via browser, after logged in.
What's the best way to solve this? Any suggestions?
I was trying to do something like this inside index.tsx:
if (id) {
const rollback = history.goBack();
if (rollback) {
history.push('/');
}
}
Obs: In this case, '/' is my home page, and i can't apply the logic above because "An expression of type 'void' cannot be tested for truthiness".
Sorry for anything i'm still new at react and trying to learn.
Don't know if i could do something inside my router, here it is anyway:
import React, { Suspense, lazy } from 'react';
import { Route, Router, Switch } from 'react-router-dom';
import history from '../utils/history';
import LoadingPage from '../components/organisms/LoadingPage';
const DownloadExams = lazy(() => import('../pages/private/DownloadExams'));
const Home = lazy(() => import('../pages/private/Home'));
const ProfileSelector = lazy(() => import('../pages/private/ProfileSelector'));
const AppRoutes = () => {
return (
<Router history={history}>
<Suspense fallback={<LoadingPage />}>
<Switch>
<Route exact path={'/'} component={Home} />
<Route exact path={'/baixar-exames'} component={DownloadExams} />
<Route exact path={'/profile'} component={ProfileSelector} />
</Switch>
</Suspense>
</Router>
);
};
export default AppRoutes;
Any suggestions?
Thanks!
User logIn time you can store a token or flag and store it in localStorage. After that, you can check if the user login so redirects to the page. You can also create some HOC for the same.
Example :
import React from "react";
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({ component: Component, ...rest }) => {
const isLoggedIn = localStorage.getItem("token");
return (
<Route
{...rest}
render={(props) => {
if (isLoggedIn) {
return <Component {...props} />;
} else {
return (
<Redirect
to={{
pathname: "/",
state: {
from: props.location,
},
}}
/>
);
}
}}
/>
);
};
Example Usage :
<ProtectedRoute path="/home" exact component={Home} />
This will redirect the user to /home after login.
i'm working on a MERNG app, and i have a problem loging out the user.
So, i have a route, that will take you to a page where the url has /profile/userId, the userID is given by the localStorage since i'm taking that value from there, and, when i logout i remove the token from the localStorage, so, if there's no values, it should take you login page.
Now, the problem is that, when i login, and get to the /profile/userId page, and i refresh the page with f5, if i logout, it won't let me do it, the redirecting doesn't work, it only works if i don't refresh the page, and that's a big issue for my app, and it's actually weird.
Maybe it's a problem with my code, i don't know that well how to use Redirect, so if you can help me with this, you're the greatest !
So this is the code
Routes with the redirect
import React from "react";
import "./App.css";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect
} from "react-router-dom";
import Auth from "./website/Auth";
import SocialMedia from "./website/SocialMedia";
import SingleUser from "./website/SingleUser";
function App() {
const logIn = JSON.parse(localStorage.getItem("token"));
return (
<Router>
<Switch>
<Route exact path="/">
{logIn ? <Redirect to={`/profile/${logIn.data.id}`} /> : <Auth />}
</Route>
<Route exact path="/socialmedia" component={SocialMedia} />
<Route exact path="/profile/:id" component={SingleUser} />
</Switch>
</Router>
);
}
export default App;
To logout
import React from "react";
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
const SingleUser = () => {
const history = useHistory();
const dispatch = useDispatch();
const userData = JSON.parse(localStorage.getItem("token"));
const logout = () => {
dispatch({ type: "LOGOUT" });
if (!userData) {
history.push("/");
}
};
return <div onClick={logout}>Single User</div>;
};
export default SingleUser;
My reducer where i store the token in the localStorage and remove it with logout action
const reducer = (state = { authData: null }, action) => {
switch (action.type) {
case "LOGIN":
localStorage.setItem("token", JSON.stringify({ data: action.payload }));
return {
...state,
authData: action.payload
};
case "LOGOUT":
localStorage.removeItem("token");
return {
...state,
authData: null
};
default:
return state;
}
};
export default reducer;
This is simply because you are rendering the Auth component only in the "/" route.
So if you are on a different route, you are skipping the "/" route and the user route will be shown. There are different ways on how to handle it:
Always redirect
<Switch>
<Route exact path="/">
<Auth />
</Route>
{!logIn && <Route path="*"><Redirect to={`/profile/${logIn.data.id}`} /></Route>} // This route will not always be rendered if the user should login and is not on the "/" page
<Route exact path="/socialmedia" component={SocialMedia} />
<Route exact path="/profile/:id" component={SingleUser} />
</Switch>
Redirect manually for every route
const SingleUser = () => {
const history = useHistory();
const dispatch = useDispatch();
const userData = JSON.parse(localStorage.getItem("token"));
if(!userData) histroy.push("/")
...
Early return
function App() {
const logIn = JSON.parse(localStorage.getItem("token"));
if(!login) return <Auth />
return (
<Router>
<Switch>
<Route exact path="/">
<Redirect to={`/profile/${logIn.data.id}`} />
</Route>
<Route exact path="/socialmedia" component={SocialMedia} />
<Route exact path="/profile/:id" component={SingleUser} />
</Switch>
</Router>
);
}
Additionally, userData will always be defined on logout, since you are not accessing a new lcoalState but the current one, before it got removed. It still lives in your varibale.
if (!userData) { // Remove the if
history.push("/");
}
Another quite easy solution for you might be just doing this:
<Route exact path="/" render={() => (logIn ? <SingleUser /> : <Auth />)} />
This way on every rerender it will check if there is a user and if not it will redirect you directly to the Auth Component.
Hi mike from the past !
Your code it's okay, but, when you click logout, the variable on the App.js component is still the same, so, you won't get the results you're looking for, what you have to do, it's to reload the website, so te App.js component will upload the variable and will recognize that the value of variable has changed, so you will go to login/register again!
just change your code for this one, so far everything work as expected
import React from "react";
import { useHistory } from "react-router-dom";
import { useDispatch } from "react-redux";
const SingleUser = () => {
const history = useHistory();
const dispatch = useDispatch();
const logout = () => {
dispatch({ type: "LOGOUT" });
history.push("/");
history.go("/");
};
return <div onClick={logout}>Single User</div>;
};
export default SingleUser;
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>
)
}
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'm trying to implement some security into my app and ended up creating this code:
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
const PrivateRoute = ({
component: Component,
auth: { isAuthenticated, loading },
...rest
}) => (
<Route
{...rest}
render={props =>
!isAuthenticated && !loading ? (
<Redirect to='/auth/login' />
) : (
<Component {...props} />
)
}
/>
);
PrivateRoute.propTypes = {
auth: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
auth: state.auth
});
export default connect(mapStateToProps)(PrivateRoute);
auth comes from my state management that looks exactly like this when viewed from the Redux Devtools installed on my Chrome browser; here it is:
isAuthenticated and loading are usually true when a user is loggedIn; that works just fine. The problem I'm having is that my PrivateRoute does not redirect to the auth/login page when no one is loggedIn. Does anyone has any idea on how to fix this?. This is an example of one of my routes that need the PrivateRoute component:
<PrivateRoute exact path='/edit-basics' component={EditBasics} />
The route above is a page to edit the current loggedIn user info only available to him/her. I'm still accessing to it without being loggedIn.
So, you're probably getting errors from having the && operator and two expressions inside of the ternary operation that you're passing to the render method of Route.
You'll have to find a different way to validate if it's still loading.
In JSX if you pair true && expression it evaluates to expression – so basically you're returning !loading as a component to render.
Read more: https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator
const PrivateRoute = ({
component: Component,
auth: { isAuthenticated, loading },
...rest
}) => (
<Route
{...rest}
render={props =>
// TWO OPERATIONS WITH && WILL CAUSE ERROR
!isAuthenticated && !loading ? (
<Redirect to='/auth/login' />
) : (
<Component {...props} />
)
}
/>
);
Also,
React Router's authors recommend constructing this kind of private route with child components instead of passing the component to render as a prop.
Read more: https://reacttraining.com/react-router/web/example/auth-workflow
function PrivateRoute({ auth, children, ...rest }) {
return (
<Route
{...rest}
render={() =>
!auth.isAuthenticated ? (
<Redirect
to={{
pathname: "/auth/login",
state: { from: location }
}}
/>
) : (
children
)
}
/>
);
}
And to call that route:
<PrivateRoute path="/protected" auth={auth} >
<ProtectedPage customProp="some-prop" />
</PrivateRoute>
It looks like you are not passing down the auth prop to the PrivateRoute. Try adding it.
<PrivateRoute exact path='/edit-basics' component={EditBasics} auth={this.props.auth}/>
maybe something like this
const PrivateRoute = ({ component,
auth: { isAuthenticated, loading },
...options }) => {
let history = useHistory();
useLayoutEffect(() => {
if ( !isAuthenticated && !loading) history.push('/auth/login')
}, [])
return <Route {...options} component={component} />;
};