protected route not rendering protected componet after authentication - reactjs

So I have a react app and I want to redirect users to a dashboard after authentication. I am using react-router-dom. The surprising thing is that after all setup and I try to access the protected route without authentication it redirects me back to the home page which works well also, when I console log to check if user verification function works. it returns authenticated user which means that is also working well. But when I sign in as a user react is supposed to render the dashboard but unfortunately it doesn't. I am so confused at the moment. Please some assistance would be highly appreciated. Thanks
ProtectedRoute.js
import React from 'react';
import PropTypes from 'prop-types';
import {Route,Redirect} from 'react-router-dom';
const ProtectedRoute = ({isAuth:isLoggedin,component:Component,...rest})=>{
console.log(Component,isLoggedin)
return (
<Route
{...rest}
render={(props)=>{
if(isLoggedin){
return <Component />
}else{
return(
<Redirect to={{
pathname:'/',
state: {from: props.location}
}}
/>
)
}
}
}
/>
)
}
export default ProtectedRoute;
ProtectedRoute.propTypes={
isAuth: PropTypes.bool.isRequired
}
App.js
const App = ()=>{
const [isClicked, setIsClicked]= useState(false);
const [username,setUsername] = useState()
const [pass,setPass] = useState();
const [isLoggedIn,setIsLoggedIn] = useState(false);
const [authUser,setAuthUser] = useState({})
const disableScroll =()=>{
document.body.style.overflow = 'hidden';
document.querySelector('html').scrollTop = window.scrollY;
}
const enableScroll=()=>{
document.body.style.overflow = null;
}
const handleLogin = () =>{
setIsClicked(!isClicked);
if(!isClicked){disableScroll()}
else if(isClicked){enableScroll()}
}
const handleLogout = ()=>{
setIsLoggedIn(!isLoggedIn)
{<Redirect to={{
pathname: "/",
state: { from: props.location }
}}
/>}
console.log(isLoggedIn)
}
const getUser = ()=>{
Users.forEach((user)=>{
if(user.username === username)return user
})
}
const login=()=>{
const curUser = Users.filter((el)=>{
if(el.username === username) return el
})
if(curUser[0].username === username && curUser[0].pass.toString() === pass){
setAuthUser(curUser[0]);
setIsLoggedIn(!isLoggedIn)
enableScroll();
}else{alert('invalid username or password')}
}
return(
<LoginContext.Provider value={{isClicked,setIsClicked,handleLogin,setUsername,
setPass,login,isLoggedIn,authUser,setAuthUser,handleLogout}}>
<BrowserRouter>
<Switch>
<Route exact path='/' component={HomePage}>
<HomePage/>
</Route>
<ProtectedRoute exact path='/Dashboard' isAuth={isLoggedIn} component={Dashboard}/>
</Switch>
</BrowserRouter>
</LoginContext.Provider>
);
}
export default App;

Related

How can I check if login is successful, then display the protected routes in react hooks

After successful login how can I see/access Dashboard, CreatedLink components mentioned inside the protected route. Could someone please advise how do I check
loginEmail is available in localStorage then display the above components else display login. Do I need another component for navigation to achieve this ?
App.js
import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom';
import Dashboard from "./components/dashboard";
import CreateLink from "./components/createLink";
import Nominate from "./components/nominate";
import Login from "./components/login";
import { ProtectedRoute } from "./components/protectedRoute";
import ErrorPage from "./components/errorPage";
function App() {
return (
<Router>
<div>
<div className="navbar-nav">
</div>
<Switch>
<ProtectedRoute exact path='/dashboard' component={Dashboard} />
<ProtectedRoute exact path='/createLink' component={CreateLink} />
<Route exact path='/' component={Login} />
<Route path='/nominate/:token' component={Nominate} />
<Route path='/errorPage' component={ErrorPage} />
</Switch>
</div>
</Router>
);
}
export default App;
login.js
import React, { useRef, useEffect, useState } from "react";
import { useGoogleLogin } from 'react-google-login';
import { refreshToken } from '../utils/refreshToken';
const clientId ="client_id_here";
const Login = () => {
const [userEmail, setUserEmail] = useState("");
const onSuccess = (res) =>{
console.log("Login successfully",res.profileObj);
const email = res.profileObj.email;
setUserEmail(email);
window.localStorage.setItem("loginEmail", email);
refreshToken(res);
}
const onFailure = (res) => {
console.log('Login failed: res:', res);
alert(
`Failed to login !`
);
};
const {signIn} = useGoogleLogin ({
onSuccess,
onFailure,
clientId,
isSignedIn: true,
accessType: 'offline',
})
return (
<div className="App">
<h1>Login</h1>
<div className="inputForm">
<button onClick={signIn}>
<img src="images/google.png" className="loginG"/>
<span className="loginText">Sign in</span>
</button>
</div>
</div>
)
}
export default Login;
protectedRoute.js
import React from "react";
import { Route, Redirect } from "react-router-dom";
export const ProtectedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) => {
if (localStorage.getItem("loginEmail")) {
return <Component {...props} />;
} else {
return (
<>
<Redirect
to={{
pathname: "/",
state: {
from: props.location,
},
}}
/>
</>
);
}
}}
/>
);
};
Sandboxlink
https://codesandbox.io/s/gracious-forest-4csz3?file=/src/App.js
If I understand your question correctly, you want to be routed back to the protected route originally being accessed before getting bounced to the login route.
The protected route passed a referrer, i.e. from value in route state, you just need to access this value and imperatively redirect back to the original route.
login
Access the location.state and history objects to get the referrer value and redirect to it. This destructures from from location.state object and provides a fallback to the "/dashboard" route if the referrer is undefined (i.e. user navigated directly to "/login").
import { useHistory, useLocation } from 'react-router-dom';
const Login = () => {
const history = useHistory();
const { state: { from = "/dashboard" } = {} } = useLocation();
...
const onSuccess = (res) => {
console.log("Login successfully", res.profileObj);
const email = res.profileObj.email;
setUserEmail(email);
window.localStorage.setItem("loginEmail", email);
// refreshToken(res);
history.replace(from);
};
I also think a small refactor of your PrivateRoute component will allow you to first check localStorage when landing on a protected route.
export const ProtectedRoute = (props) => {
return localStorage.getItem("loginEmail") ? (
<Route {...props} />
) : (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
};

Login redirect rendering twice

I'm using reach router for my routes. I was able to protect the dashboard and expose the login page and redirect if the user is not logged in but now if I enter a url it will do a quick redirecto to login and then to home instead of the page actually entered.
I noticed because of the useEffect to fetch the user the component renders twice: 1 without user (redirects to login) the other one with user and redirects to home.
Routes file
const AdminRoutes = () => {
return (
<Router>
<MainLayout path="/admin">
<HomePage path="/" />
<CarTransfer path="/cartransfers">
<CarTranserList path="/list" />
<CarTransferCreate path="/new" />
</CarTransfer>
<User path="/users">
<UserList path="/list" />
<UserCreate path="/new" />
</User>
</MainLayout>
<LoginPage path="/login" />
</Router>
);
};
Layout file
import { useState, useEffect } from "react";
import { Redirect, useNavigate } from "#reach/router";
import { Layout } from "antd";
import SiderMenu from "./SiderMenu";
import LayoutBanner from "./LayoutBanner";
import { useSelector, useDispatch } from "react-redux";
import {
userSelector,
fetchUserBytoken,
clearState,
} from "../../features/authSlice";
const { Content } = Layout;
const MainLayout = ({ children }) => {
const user = useSelector(userSelector);
const [collapsed, setCollapsed] = useState(false);
const navigate = useNavigate();
const dispatch = useDispatch();
const { isFetching, isError } = useSelector(userSelector);
useEffect(() => {
dispatch(
fetchUserBytoken({
token: localStorage.getItem("token"),
id: localStorage.getItem("id"),
})
);
}, []);
useEffect(() => {
if (isError) {
dispatch(clearState());
navigate("/login");
}
}, [isError]);
const handleOnCollapse = () => {
setCollapsed((prevState) => !prevState);
};
if (isFetching) {
return <div>Loading</div>;
}
if (user.id === "") {
return <Redirect noThrow to="/login" />;
}
return (
<Layout>
<SiderMenu collapsed={collapsed} handleOnCollapse={handleOnCollapse} />
<Layout>
<LayoutBanner
collapsed={collapsed}
handleOnCollapse={handleOnCollapse}
/>
<Content>{children}</Content>
</Layout>
</Layout>
);
};
export default MainLayout;
The second question would be how to get to the same page you were before the login redirect after login.
Thanks

React PrivateRoute is caught in a Route loop

I have a PrivateRoute component that protects any route requiring a valid login in the app. I have a route called Spec.tsx that is called from App.tsx. PrivateRoute will spit you out on the Login page if you're not logged in, which is great. And Login will send you directly to the Home page if you are logged in. Also great.
Right now, I'm caught in a loop where when I go to Spec, the app thinks I'm not logged in, and sends me to Login, which does think I'm logged in, and so sends me back to Home. currentUser is always calculated the same was, like const {currentUser} = useContext(AuthContext);
When I log the currentUser in PrivateRoute as I navigate to /spec/:id it says null once, and then gives the correct answer 3 more times. I suspect the null causes me to get booted to Login and then currentUser must be assigned correctly as I'm sent back to Home immediately. I don't even ever get to Spec.tsx, nothing I try to log there gets logged. Can anyone point out what I'm doing wrong? Thanks
//In App.tsx
<AuthProvider>
<Router history={history}>
<Navbar />
<Switch>
<PrivateRoute exact path="/" component={Home} />
<PrivateRoute exact path="/spec/:id" render={() => (
<Spec isEdit={true}/>
)}/>
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
</Switch>
</Router>
</AuthProvider>
//PrivateRoute.tsx
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "../Util/Auth";
const PrivateRoute = ({ component: RouteComponent, ...rest }: any) => {
const {currentUser} = useContext(AuthContext);
console.log(currentUser)
return (
<Route
{...rest}
render={(routeProps: any) =>
!!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
);
};
export default PrivateRoute
//Login.tsx
const Login = ({ history }: any) => {
const { currentUser } = useContext(AuthContext);
if (currentUser) {
return <Redirect to="/" />;
}
return (
//My JSX
)
My Auth provider is below, which handles all changes to currentUser
//Auth.tsx
import React, { useEffect, useState } from "react";
import { createNewUser } from "../Models/User";
import app from "./firebase";
const initialUser: any = null;
export const AuthContext = React.createContext(initialUser);
export const AuthProvider: React.FC = ({ children }) => {
const [currentUser, setCurrentUser] = useState<any>(null);
const [currentDBUser, setCurrentDBUser] = useState<any>(null);
const [pending, setPending] = useState<boolean>(true);
const [pending2, setPending2] = useState<boolean>(true);
useEffect(() => {
//Auth user
app.auth().onAuthStateChanged((user) => {
setPending(false)
setCurrentUser(user);
});
//Grab database data for user
async function fetchData() {
if (currentUser && !currentDBUser && pending2) {
const newUser: User = {fbUser: currentUser.uid, email: currentUser.email}
const userDBObject = await createNewUser(newUser);
if (userDBObject) {
setCurrentDBUser(userDBObject);
setPending2(false);
}
}
}
if (pending2) {
fetchData();
}
}, [currentUser, currentDBUser, pending2]);
if (pending) {
return <>Loading...</>
}
return (
<AuthContext.Provider value= {{ currentUser, currentDBUser }}>
{ children }
</AuthContext.Provider>
);
};

React-Redux page refresh redirects to login even though state exists

I am building a backend admin app, and it will not stay on the correct route/page when refreshing the browser.
When the user logs in, it redirects to /dashboard, updates the state with user info and access token, sets a refresh token on the server in a httponly cookie.
When the access token expires and a route gets a 401 it calls check refresh token and gets a new access token and updates state. All of this works great except on refresh.
I believe it may have something to do with the HashRouter maybe? Not sure but I'm pulling out my hair because I'm sure it's something small I'm missing.
When visiting the page I've created a PrivateRoute for all traffic other than /login.
App.js
import React, {Component} from 'react';
import {HashRouter, Route, Switch} from 'react-router-dom';
import PrivateRoute from "./components/PrivateRoute";
import './scss/style.scss';
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
)
// Pages
const Login = React.lazy(() => import('./views/pages/login/Login'));
// Containers
const TheLayout = React.lazy(() => import('./containers/TheLayout'));
class App extends Component {
render() {
return (
<HashRouter>
<React.Suspense fallback={loading}>
<Switch>
<Route exact path="/login" name="Login" render={props => <Login {...props}/>}/>
<PrivateRoute path="/" name="Home" render={props => <TheLayout {...props}/>}/>
</Switch>
</React.Suspense>
</HashRouter>
);
}
}
export default App;
In PrivateRoute is where I put the initial check whether there is the token and isLoggedIn state, if not it dispatches my refresh token function to update the state, which also functions properly, but still redirects to /login.
PrivateRoute.js
import React from 'react';
import {Route, Redirect} from 'react-router-dom';
import {useDispatch, useSelector} from "react-redux";
import axios from "axios";
import {eraseToken, getRefreshedToken} from "../store/auth";
import store from "../store";
axios.defaults.withCredentials = true;
axios.interceptors.request.use(req => {
console.log(req)
const token = store.getState().auth.user.token;
if (token) {
req.headers.Authorization = 'Bearer ' + token;
} else {
//req.headers.Authorization = null;
}
return req;
});
axios.interceptors.response.use(res => {
return res;
}, async (error) => {
if (401 === error.response.status) {
if ('/refresh' !== error.response.config.url) {
await store.dispatch(getRefreshedToken());
} else {
store.dispatch(eraseToken());
}
} else {
return Promise.reject(error);
}
}
);
const PrivateRoute = ({render: Component, ...rest}) => {
const dispatch = useDispatch();
let token = useSelector(state => state.auth.user.token);
let isLoggedIn = useSelector(state => state.auth.isLoggedIn);
console.log(token, isLoggedIn);
if (!isLoggedIn || !token) {
dispatch(getRefreshedToken());
}
// check again after state update
token = useSelector(state => state.auth.user.token);
isLoggedIn = useSelector(state => state.auth.isLoggedIn);
return (
<Route {...rest} render={props => (
(token && isLoggedIn)
? <Component {...props} />
: <Redirect to={{pathname: '/login', state: {from: props.location}}}/>
)}/>
)
}
export default PrivateRoute;
This is where I'm confused. Once I log in I get redirected fine to /dashboard because token etc exists in state, however if I refresh the page while at /dashboard it calls dispatch(getRefreshedToken()); which works fine, and updates the state with the correct values, which should then just render the component (I assumed) instead of redirecting back to /login.
If I manually change the url and replace /login with /dashboard it views the dashboard fine still as the state exists properly.
The PrivateRoute renders a component called TheLayout, which renders a component called TheContent which maps routes to views to display.
routes.js
const routes = [
{ path: '/', exact: true, name: 'Home' },
{ path: '/dashboard', name: 'Dashboard', component: Dashboard },
{ path: '/users', name: 'Users', component: Users }
]
export default routes;
TheContent.js
import React, {Suspense} from 'react';
import {Redirect, Route, Switch} from 'react-router-dom';
import {CContainer, CFade} from '#coreui/react';
// routes config
import routes from '../routes';
const loading = (
<div className="pt-3 text-center">
<div className="sk-spinner sk-spinner-pulse"></div>
</div>
)
const TheContent = () => {
return (
<main className="c-main">
<CContainer fluid>
<Suspense fallback={loading}>
<Switch>
{routes.map((route, idx) => {
return route.component && (
<Route
key={idx}
path={route.path}
exact={route.exact}
name={route.name}
render={props => (
<CFade>
<route.component {...props} />
</CFade>
)}/>
)
})}
<Redirect from="/" to="/dashboard"/>
</Switch>
</Suspense>
</CContainer>
</main>
)
}
export default React.memo(TheContent);
Not sure if that might have something to do with it?
UPDATE
I realized that the code was not waiting on dispatch to finish updating the state, so I changed the PrivateRoute to instead use useEffect with dependencies, and got it working how I needed.
However, trying to understand useEffect better, it's firing my refresh and calling the server multiple times once my cookie expires. Is there any way to stop this from happening? Here is my updated Private Route.
const PrivateRoute = ({render: Component, ...rest}) => {
const dispatch = useDispatch();
const history = useHistory();
let token = useSelector(state => state.auth.user.token);
let isLoggedIn = useSelector(state => state.auth.isLoggedIn);
let isRefreshingToken = useSelector(state => state.auth.isRefreshingToken);
useEffect(() => {
if (!isLoggedIn && !token && !isRefreshingToken) {
(async () => {
await dispatch(getRefreshedToken())
.catch(err => {
history.push('/login');
});
})();
}
}, [token, isLoggedIn, isRefreshingToken, dispatch, history])
return (
<Route {...rest} render={props => (
<Component {...props} />
)}/>
)
}

React Router - Login redirect to a private route goes to landing page instead

I have authentication up and running, but I am trying to get a redirect to the (private route) application page after a successful sign in. Instead a user is redirected to the landing page, in which they have to navigate to the application page manually.
My route is wrapped as follows
<Router>
<div className="app">
<Route exact path="/" component={Landing} />
<PrivateRoute exact path="/app" component={AppHome} />
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={SignUp} />
</div>
</Router>
Where the login component checks and redirects via
const { currentUser } = useContext(AuthContext);
if (currentUser) {
console.log(currentUser)
return <Redirect to="/app" />
}
Full login component
import React, { useCallback, useContext } from 'react'
import { BrowserRouter as Router, Link } from 'react-router-dom'
import { withRouter, Redirect } from 'react-router'
import FBase from '../firebase.js'
import { AuthContext } from '../auth/Auth'
const Login = ({ history }) => {
const handleLogin = useCallback(
async event => {
event.preventDefault()
const { email, password } = event.target.elements;
try {
await FBase
.auth()
.signInWithEmailAndPassword(email.value, password.value);
history.push('/')
} catch (error) {
alert(error)
}
},
[history]
);
const { currentUser } = useContext(AuthContext);
if (currentUser) {
console.log(currentUser)
return <Redirect to="/app" />
}
return (
///UI - Form calls handleLogin
)
}
export default withRouter(Login)
Console is clearly logging the user after a successful sign in, so this may be a lifecycle issue.
The private route itself (the redirect in here works)
const PrivateRoute = ({ component: RouteComponent, ...rest }) => {
const { currentUser } = useContext(AuthContext)
return (
<Route
{...rest}
render = {routeProps => !!currentUser ? (
<RouteComponent {...routeProps} />
) : (
<Redirect to={"/login"} />
)
}
/>
)
}
After further testing, it seems the redirect just doesn't work at all, as I've tried redirecting to signup too.
You could use useHistory hook to handle redirection, it's easier and you can directly implement it in your async/await function.
import {useHistory} from "react-router-dom"
const history = useHistory()
async function foo(){
const res = await stuff
history.push("/app")
}
Also, don't write exact path everywhere in your route. This is only necessary for the main root "/".

Resources