How to render react routes after auth response? - javascript

When someone arrives to my webpage I first check if the user is authenticated or not. I need to wait for the response from my GET /auth/loggedin request before rendering the routes so that I know if I should redirect to /login or /. I have added conditional rendering to the App.js component, but it always redirects the user to /login even when the user is authenticated.
App.js:
import { useEffect, useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import AuthContext from "./contexts/authContext";
import { apiLoggedIn } from "./api/auth";
import ProtectedRoute from "./components/ProtectedRoute";
import Document from "./pages/Document";
import { Login } from "./pages/Login";
import { Signup } from "./pages/Signup";
import Layout from "./components/Layout";
import PublicRoute from "./components/PublicRoute";
export const App = () => {
const [auth, setAuth] = useState({ isAuth: undefined, user: undefined });
useEffect(() => {
apiLoggedIn()
.then((res) => {
setAuth({ isAuth: true, user: res.data.userData });
})
.catch((err) => {
if (err.response) {
setAuth({ isAuth: false, user: {} });
} else if (err.request) {
setAuth({ isAuth: false, user: {} });
console.log(err.request);
} else {
setAuth({ isAuth: false, user: {} });
console.log(err.message);
}
});
}, []);
return (
<>
{auth.isAuth !== undefined ? (
<AuthContext.Provider value={{ auth, setAuth }}>
<BrowserRouter>
<Routes>
<Route
path="/"
element={
<ProtectedRoute redirectTo="/login">
<Layout />
</ProtectedRoute>
}
>
<Route index element={<Document />} />
</Route>
<Route
path="/signup"
element={
<PublicRoute>
<Signup />
</PublicRoute>
}
/>
<Route
path="/login"
element={
<PublicRoute>
<Login />
</PublicRoute>
}
/>
</Routes>
</BrowserRouter>
</AuthContext.Provider>
) : (
""
)}
</>
);
};
./api/auth.js
import axios from "axios";
export const apiLoggedIn = () => {
return axios.get("/auth/loggedin");
};
./components/ProtectedRoute.js
import { useContext } from "react";
import AuthContext from "../contexts/authContext";
import { Navigate } from "react-router-dom";
const ProtectedRoute = ({ children, redirectTo }) => {
const { isAuth } = useContext(AuthContext);
return isAuth ? children : <Navigate to={redirectTo} />;
};
export default ProtectedRoute;
./components/PublicRoute.js
import { useContext } from "react";
import AuthContext from "../contexts/authContext";
import { Navigate } from "react-router-dom";
const PublicRoute = ({ children }) => {
const { isAuth } = useContext(AuthContext);
return isAuth ? <Navigate to={"/"} /> : children;
};
export default PublicRoute;
./contexts/authContext.js
import { createContext } from "react";
const AuthContext = createContext({
auth: { isAuth: false, user: {} },
setAuth: () => {},
});
export default AuthContext;

The AuthContext value is an object with auth and setAuth properties
<AuthContext.Provider value={{ auth, setAuth }}>
...
</AuthContext.Provider>
But in the route wrapper you are referencing an isAuth property, which will always be undefined, i.e. falsey.
const ProtectedRoute = ({ children, redirectTo }) => {
const { isAuth } = useContext(AuthContext);
return isAuth ? children : <Navigate to={redirectTo} />;
};
This is why the redirect always occurs. To resolve, ensure you reference the same context values throughout the code.
Either specify an isAuth context value:
<AuthContext.Provider value={{ isAuth: auth.isAuth, setAuth }}>
...
</AuthContext.Provider>
Or fix the wrappers:
const ProtectedRoute = ({ children, redirectTo }) => {
const { auth } = useContext(AuthContext);
return auth.isAuth ? children : <Navigate to={redirectTo} />;
};
...
const PublicRoute = ({ children }) => {
const { auth } = useContext(AuthContext);
return auth.isAuth ? <Navigate to={"/"} /> : children;
};

Your context has the following shape :
{
"auth": {
"isAuth": true,
"user": {...},
},
"setAuth": function ...
}
you must update ProtectedRoute to
const ProtectedRoute = ({ children, redirectTo }) => {
const { auth } = useContext(AuthContext);
return auth.isAuth ? children : <Navigate to={redirectTo} />;
};
you must update PublicRoute to
const PublicRoute = ({ children }) => {
const { auth } = useContext(AuthContext);
return auth.isAuth ? <Navigate to={"/"} /> : children;
};

the key name is not passed correctly in auth provider it should be passed as following here is sample sandbox demo : https://codesandbox.io/s/routes-4c5oh?file=/src/App.js
<AuthContext.Provider value={{ isAuth:auth, setAuth }}>
or change the key to auth in each place. in Public and Protected routes like following
const { auth:{isAuth} } = useContext(AuthContext);

Related

Error: Uncaught TypeError: Cannot destructure property '...' of '...' as it is nul

I am trying to destructure the onLogout function from useAuth which returns useContext(AuthContext), and while doing this, I get an error in the console:
Error:
Uncaught TypeError: Cannot destructure property 'onLogout' of 'useAuth(...)' as it is null.
App.js code snippet:
import React, { createContext, useContext } from "react";
import { Navigate, Route, Routes, useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { logout, userLogin, userRegister } from "./features/cv";
import { cookieCutter } from "./utils/cookie";
const AuthContext = createContext(null);
const AuthProvider = ({ children }) => {
const token = cookieCutter("token");
const dispatch = useDispatch();
const navigate = useNavigate();
const handleLogin = (data) => {
dispatch(userLogin(data));
navigate("/table");
};
const handleRegister = (data) => {
dispatch(userRegister(data));
navigate("/table");
};
const handleLogout = () => {
dispatch(logout());
navigate("/login");
};
const value = {
token,
onLogin: handleLogin,
onRegister: handleRegister,
onLogout: handleLogout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
const useAuth = () => {
return useContext(AuthContext);
};
function App() {
const ProtectedRoute = ({ children }) => {
const { token } = useAuth();
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
};
const userString = JSON.parse(cookieCutter("user") ?? "{}");
const { onLogout } = useAuth();
return (
<AuthProvider>
<div>
<Routes>
<Route
path="/"
element={
<ProtectedRoute>
<Create />
</ProtectedRoute>
}
/>
<Route
path="/table"
element={
<ProtectedRoute>
<Table />
</ProtectedRoute>
}
/>
<Route
path="/preview"
element={
<ProtectedRoute>
<Preview />
</ProtectedRoute>
}
/>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Routes>
</div>
</AuthProvider>
)
But when I do the same inside the ProtectedRoute function to get the token it works without any error, don't know why.
App.js ProtectedRoute function:
const ProtectedRoute = ({ children }) => {
const { token } = useAuth();
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
};
You cannot use useAuth() in App, because it's not a child of AuthProvider. For this to work, move AuthProvider call to the index file to have something like this:
<AuthProvider>
<App/>
</AuthProvider>
Also, it's not correct to define a component inside another one. Move ProtectedRoute outside of App:
const ProtectedRoute = ({ children }) => {
const { token } = useAuth();
if (!token) {
return <Navigate to="/login" replace />;
}
return children;
};

Everytime I refresh the page, isAuth goes to false. Its messing up my routing

I have multiple flags that route my user to specific pages depending on the flag. I am storing the token in sessionStorage and using it to authenticate. But whenever I remove redirect my page doesnt route correctly. If I remove redirect and refresh auth goes false and then to true and doesnt route correctly. I dont understand what im missing.
Routes.jsx
import React, { useEffect } from 'react';
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import { useAuth } from '../Contexts/AuthContext';
import GridContainer from '../Components/Grid';
import Navbar from '../Components/Navbar';
import Login from '../Pages/Authentication/Login';
import Signup from '../Pages/Authentication/Signup';
import ForgotPassword from '../Pages/Authentication/ForgotPassword';
import UpdatePassword from '../Pages/Authentication/UpdatePassword';
import ConnectedTellUsAboutYourBusiness from '../Pages/AboutYourBusiness/ConnectedTellUsAboutYourBusiness';
import Portal from '../Pages/Portal/Portal';
import { AdvertisersProvider } from '../Contexts/AdvertisersContext';
import { ProfileProvider } from '../Contexts/UserProfileContext';
import { CampaignsProvider } from '../Contexts/CampaignsContext';
import AccountManagment from '../Pages/Authentication/AccountManagement';
const Routes = () => {
const { currentUser } = useAuth();
const { isAuthenticated, emailIsVerified, accountIsComplete } = { ...currentUser };
let routes;
if (isAuthenticated && emailIsVerified && accountIsComplete) {
routes = (
<ProfileProvider>
<Switch>
<Route exact path="/updatePassword">
<UpdatePassword />
</Route>
<Route path="/portal">
<AdvertisersProvider>
<CampaignsProvider>
<Portal />
</CampaignsProvider>
</AdvertisersProvider>
</Route>
<Redirect to="/portal" />
</Switch>
</ProfileProvider>
);
}
if (
(isAuthenticated && !emailIsVerified && !accountIsComplete) ||
(isAuthenticated && !emailIsVerified && accountIsComplete)
) {
routes = (
<ProfileProvider>
<Switch>
<Route exact path="/accountManagement">
<AccountManagment />
</Route>
<Redirect to="/accountManagement" />
</Switch>
</ProfileProvider>
);
}
if (isAuthenticated && emailIsVerified && !accountIsComplete) {
routes = (
<ProfileProvider>
<Switch>
<Route path="/aboutYourBusiness/form">
<ConnectedTellUsAboutYourBusiness />
</Route>
<Redirect to="/aboutYourBusiness/form" />
</Switch>
</ProfileProvider>
);
}
if (!isAuthenticated) {
routes = (
<Switch>
<Route exact path="/aboutYourBusiness/plan">
<ConnectedTellUsAboutYourBusiness />
</Route>
<Route exact path="/login">
<Login />
</Route>
<Route exact path="/signup">
<Signup />
</Route>
<Route exact path="/forgotPassword">
<ForgotPassword />
</Route>
<Redirect to="/login" />
</Switch>
);
}
return (
<Router>
<GridContainer pageHeight="100%">
<Navbar />
</GridContainer>
{routes}
</Router>
);
};
export default Routes;
authContext.jsx
import React, { useContext, useState, useEffect } from 'react';
import propTypes from 'prop-types';
import usePost from '../Hooks/usePost';
const AuthContext = React.createContext();
export function useAuth() {
return useContext(AuthContext);
}
export function AuthProvider({ children }) {
const { post } = usePost('/login');
const defaultCurrentUser = {
isAuthenticated: false,
emailIsVerified: false,
accountIsComplete: false,
accessToken: null,
tokenExpiry: null,
};
const [currentUser, setCurrentUser] = useState({ ...defaultCurrentUser });
function logout() {
sessionStorage.removeItem('token');
sessionStorage.removeItem('tokenExpiry');
return setCurrentUser({
...defaultCurrentUser,
});
}
const login = async (email, password) => {
const loginResponse = await post(
{
email,
password,
},
'POST'
);
const token = loginResponse.data.bearerToken;
const tokenExpiry = loginResponse.data.tokenExpiresOn;
const isAuthenticated = loginResponse.data.authenticated;
const emailIsVerified = true;
const accountIsComplete = true;
if (isAuthenticated && token) {
// set token to sessionStorage
sessionStorage.setItem('token', token);
sessionStorage.setItem('tokenExpiry', tokenExpiry);
setCurrentUser({
isAuthenticated,
emailIsVerified, // true until we implement
accountIsComplete, // true until we implement
accessToken: token,
tokenExpiry,
});
}
return loginResponse;
};
const token = sessionStorage.getItem('token');
const tokenExpiry = sessionStorage.getItem('tokenExpiry');
const currentDate = new Date().toISOString();
useEffect(() => {
if (token) {
if (currentDate < tokenExpiry) {
// token is valid
setCurrentUser({
...currentUser,
isAuthenticated: true,
emailIsVerified: true,
accountIsComplete: true,
accessToken: token,
tokenExpiry,
});
} else {
// token is expired logout user
logout();
}
}
}, []);
console.log(currentUser, 'current user');
const value = {
currentUser,
setCurrentUser,
logout,
login,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
AuthProvider.propTypes = {
children: propTypes.node.isRequired,
};

How can I redirect a user to login when a certain link is clicked using JWT

I am trying to make it so that when a user clicks on a certain Link (Upload Component), it will redirect them to to the login component and am not sure how to accomplish this task in React. I was directed to another answered question, but it hasn't helped me as I am still confused on what I need to do with my own set up. I understand I need to make my own protected route (maybe), but I saw others accessing useContext and I do not have any file with context. I am using Version 6 in react dom. I am also using React router and redux in my project so I know I need to access the state somehow, just not sure how to wrap my mind around it so if anyone could help, I would appreciate it. The user is being stored in local storage with JWT authentication.
App.js:
import React, {useState, useEffect, useCallback} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {BrowserRouter as Router, Routes, Route} from 'react-router-dom'; //Switch was replaced by Routes in react-router-dom v6
import './App.css';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import Home from './components/pages/Home';
import Uploads from './components/pages/Uploads';
import Account from './components/pages/Account';
import Login from './components/pages/Login';
import LogOut from './components/Logout';
import Register from './components/pages/Register';
import Profile from './components/pages/Profile';
import EditProfile from './components/pages/EditProfile';
import BoardAdmin from './components/pages/BoardAdmin';
import BoardModerator from './components/pages/BoardModerator';
import BoardUser from './components/pages/BoardUser';
import {logout} from './slices/auth';
import EventBus from './common/EventBus';
const App = () => {
const [showModeratorBoard, setShowModeratorBoard] = useState(false);
const [showAdminBoard, setShowAdminBoard] = useState(false);
const {user: currentUser} = useSelector((state) => state.auth);
const dispatch = useDispatch();
useEffect(() => {
if (currentUser) {
setShowModeratorBoard(currentUser.roles.includes('ROLE_MODERATOR'));
setShowAdminBoard(currentUser.roles.includes('ROLE_ADMIN'));
} else {
setShowModeratorBoard(false);
setShowAdminBoard(false);
}
}, [currentUser]);
return (
<>
<Router>
<Navbar />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/create/upload' element={<Uploads />} />
<Route path='/my-account' element={<Account />} />
<Route path='/login' element={<Login />} />
<Route path='/logout' element={<LogOut />} />
<Route path='/register' element={<Register />} />
<Route path='/profile' element={<Profile />} />
<Route path='/profile/edit' element={<EditProfile />} />
<Route path='/user' element={<BoardUser />} />
<Route path='/mod' element={<BoardModerator />} />
<Route path='/admin' element={<BoardAdmin />} />
</Routes>
<Footer />
</Router>
</>
);
};
export default App;
Auth.js:
import {createSlice, createAsyncThunk} from '#reduxjs/toolkit';
import {setMessage} from './messages';
import AuthService from '../services/auth.service';
const user = JSON.parse(localStorage.getItem('user'));
export const register = createAsyncThunk(
'auth/register',
async ({username, email, password}, thunkAPI) => {
try {
const response = await AuthService.register(username, email, password);
thunkAPI.dispatch(setMessage(response.data.message));
return response.data;
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
thunkAPI.dispatch(setMessage(message));
return thunkAPI.rejectWithValue();
}
}
);
export const login = createAsyncThunk(
'auth/login',
async ({username, password}, thunkAPI) => {
try {
const data = await AuthService.login(username, password);
return {user: data};
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
thunkAPI.dispatch(setMessage(message));
return thunkAPI.rejectWithValue();
}
}
);
export const logout = createAsyncThunk('auth/logout', async () => {
await AuthService.logout();
});
const initialState = user
? {isLoggedIn: true, user}
: {isLoggedIn: false, user: null};
const authSlice = createSlice({
name: 'auth',
initialState,
extraReducers: {
[register.fulfilled]: (state, action) => {
state.isLoggedIn = false;
},
[register.rejected]: (state, action) => {
state.isLoggedIn = false;
},
[login.fulfilled]: (state, action) => {
state.isLoggedIn = true;
state.user = action.payload.user;
},
[login.rejected]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
[logout.fulfilled]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
},
});
const {reducer} = authSlice;
export default reducer;
It looks like adding the if statement in my upload file is making it so that a logged in user can access the h1 element that is in the file, but not an unauthenticated user. So its working to an extent, just not redirecting back to login.
Upload.jsx:
import React from 'react';
import {Link} from 'react-router-dom';
import {useSelector} from 'react-redux';
function Uploads() {
const {user: currentUser} = useSelector((state) => state.auth);
if (!currentUser) {
return <Link to='/login' />;
}
return (
<>
<h1 className='page'>UPLOADS</h1>
</>
);
}
export default Uploads;
Rendering the Link doesn't imperatively navigate, use the Navigation component.
Example:
import React, { useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
function Uploads() {
const { user: currentUser } = useSelector((state) => state.auth);
if (!currentUser) {
return <Navigate to='/login' replace />;
}
return (
<>
<h1 className='page'>UPLOADS</h1>
</>
);
}
export default Uploads;
Typically you would protect the route instead. If you wanted to do that:
import { Navigate, Outlet } from 'react-router-dom';
import { useSelector } from 'react-redux';
const PrivateRoutes = () => {
const { user: currentUser } = useSelector((state) => state.auth);
return currentUser
? <Outlet />
: <Navigate to="/login" replace />;
}
...
<Routes>
<Route path='/' element={<Home />} />
<Route element={<PrivateRoutes />}>
<Route path='/create/upload' element={<Uploads />} />
... any other protected routes ...
</Route>
... other unprotected routes ...
</Routes>

Redux resets after dispatching an action

so I've been working on this small cinema app and I'm facing a weird bug where every time I try to log in to access the home page it doesn't redirect and then the redux store gets reset, but when I commented useEffect in the HomePage.js component It worked and also when I tried to conosole.log(action.payload) in another reducer file for the movies I got the payload from the AuthReducer so I don't know what's the problem, is it with my whole redux setup or because I'm using the render method in my ProtecdedRoute.js
AuthSaga.js
function* loginUserSaga({ payload }) {
yield delay(1500);
yield put(LoginUserSuccessAction(payload));
}
export default function* AuthSaga() {
yield takeLatest(actionTypes.LOGIN_USER, loginUserSaga);
}
protectedRoute.js
import React from 'react';
import { useSelector } from 'react-redux';
import { Route, Redirect, useLocation } from 'react-router';
const ProtectedRoute = ({ component: Component, layout: Layout, ...rest }) => {
const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
if (!isLoggedIn) {
return (
<Redirect
to={{
pathname: '/login',
}}
/>
);
}
const renderComponent = () => {
return (
<Layout>
<Component />
</Layout>
);
};
return <Route {...rest} render={renderComponent} />;
};
export default ProtectedRoute;
Routes.js
const Routes = () => {
const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
console.log(isLoggedIn);
return (
<Switch>
<ProtectedRoute path="/" exact component={Home} layout={Layout} />
<AuthRoute path="/login" exact component={Login} layout={AuthLayout} />
</Switch>
);
};
export default Routes;
AuthReducer
const authReducer = (state = initialState, action) => {
const { type, payload } = action;
console.log(payload)
switch (type) {
case actionTypes.LOGIN_USER: {
return {
...state,
loading: true,
};
}
case actionTypes.LOGIN_USER_SUCCESS: {
return {
...state,
userName: payload.userName,
token: `${payload.password}${payload.userName}`,
isLoggedIn: true,
loading: false,
};
}
case actionTypes.LOGIN_USER_FAIL: {
return {
...state,
loading: false,
};
}
case actionTypes.LOGOUT_USER: {
return {
...state,
userName: null,
token: null,
isLoggedIn: false,
loading: false,
};
}
default:
return initialState;
}
};
HomePage.jsx
const Home = () => {
const dispatch = useDispatch();
const upcomingMoviesSelector = useSelector(state => state.upcomingMovies.items)
useEffect(() => {
dispatch(fetchUpcomingMoviesAction());
console.log('')
}, []);
return (
<div>
<MovieSlider items={upcomingMoviesSelector} />
</div>
);
};
export default Home;

Redirect not working on React, using react router dom

Here, is my full code details:
route.js
<AdminRoute path="/dashboard" component={Dashboard} />
And, my admin route:
AdminRoute
import React from 'react'
import { Route } from 'react-router-dom'
import AdminLayout from '../components/layouts/AdminLayout'
const AdminRoute = ({ component: Component, ...rest }) => {
return (
<Route {...rest} render={(props) => (
<AdminLayout>
<Component {...props} />
</AdminLayout>
)} />
)
}
export default AdminRoute
and this is the AdminLayout,
and here it is hitting that console.log correctly, but not redirecting.
AdminLayout/
import React from 'react';
import { Redirect } from "react-router-dom";
import { isAuthenticated } from '../../utils/auth';
const AdminLayout = ({ children, ...rest }) => {
const isLoggedIn = isAuthenticated();
console.log("isLoggedIn >>",isLoggedIn);
if(!isLoggedIn){
//ITS HITTING THIS CONSOLE LOG
//BUT ITS NOT REDIRECTING
console.log('hit');
<Redirect
to={{
pathname: "/login",
state: { from: children.props.location },
}}
/>
}
return (
<div>{children}</div>
);
}
and this is the helper function to determine either the user is logged in or not.
Auth.js
export function isAuthenticated(){
if(localStorage.getItem("_token")){
return true;
}
return;
}
const auth = {
isAuthenticated,
};
export default auth;
The Redirect component needs to be returned in order to be rendered, and, ultimately, trigger a redirect, i.e.
const AdminLayout = ({ children, ...rest }) => {
const isLoggedIn = isAuthenticated();
if (!isLoggedIn){
return <Redirect
to={{
pathname: "/login",
state: { from: children.props.location },
}}
/>
}
return (
<div>{children}</div>
);
}

Categories

Resources