Why is my ternary operator breaking my login routees? - javascript

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.

Related

React state inside React-context is not updating

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 :)

React problem with router before fetching the user

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>
)
}

Setting state in react useEffect not happening before gets to the route

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>
);
}

React Private Route Redirect Condition Is Ignored

Every time I try to navigate to a page after the token expires, I am not redirected to my landing page. Ideally, I would like to be redirected to my landing page after my token expires. My status variable does update when the token expires but gets ignored when rendering the Redirect component. I think my Private Route may be written wrong.
App.js
import { BrowserRouter as Router, Route, Switch, Redirect } from "react-router-dom";
import LandingPage from './pages/LandingPage';
import ViewAllRecipe from './pages/ViewAllRecipe';
import AddRecipe from './pages/AddRecipe';
import EditRecipe from './pages/EditRecipe';
import SingleRecipe from './pages/SingleRecipe';
import axios from 'axios';
var status;
async function validToken(){
var token = localStorage.getItem('token');
try{
const res = await axios.post('/api/auth/verify',
{token: token},
{headers: {'Content-Type': 'application/json'}},
);
if(res.data.status===200){
status = 200;
}
else{
localStorage.removeItem('token');
status= 401;
}
}
catch(err){
localStorage.removeItem('token');
status=500;
}
}
function PrivateRoute({ children, ...rest }) {
validToken();
return (
<Route
{...rest}
render={() =>
status!==200 ? (
<Redirect
to="/landing"
/>
):(
children
)
}
/>
);
}
function App() {
return (
<Router>
<Switch>
<Route exact path="/landing" component={LandingPage}></Route>
<PrivateRoute exact path="/addRecipe" component={AddRecipe}></PrivateRoute>
<PrivateRoute exact path="/viewRecipes" component={ViewAllRecipe}></PrivateRoute>
<PrivateRoute exact path="/recipe/edit/:editRecipe" component={(props) => <EditRecipe {...props}/>}></PrivateRoute>
<PrivateRoute exact path="/recipe/view/:id" component={(props) => <SingleRecipe {...props}/>}></PrivateRoute>
</Switch>
</Router>
);
}
export default App; ```
Try this:
import React, { useState, useEffect } from 'react';
import {
BrowserRouter as Router,
Route,
Switch,
Redirect,
} from 'react-router-dom';
import LandingPage from './pages/LandingPage';
import ViewAllRecipe from './pages/ViewAllRecipe';
import AddRecipe from './pages/AddRecipe';
import EditRecipe from './pages/EditRecipe';
import SingleRecipe from './pages/SingleRecipe';
import axios from 'axios';
async function getStatus() {
var token = localStorage.getItem('token');
try {
const res = await axios.post(
'/api/auth/verify',
{ token: token },
{ headers: { 'Content-Type': 'application/json' } },
);
if (res.data.status === 200) {
return 200;
} else {
localStorage.removeItem('token');
return 401;
}
} catch (err) {
localStorage.removeItem('token');
return 500;
}
}
function PrivateRoute({ children, ...rest }) {
const [status, setStatus] = useState(null);
useEffect(() => {
(async () => setStatus(await getStatus()))();
}, []);
return (
status && (
<Route
{...rest}
render={() => (status !== 200 ? <Redirect to="/landing" /> : children)}
/>
)
);
}
function App() {
return (
<Router>
<Switch>
<Route exact path="/landing" component={LandingPage}></Route>
<PrivateRoute
exact
path="/addRecipe"
component={AddRecipe}></PrivateRoute>
<PrivateRoute
exact
path="/viewRecipes"
component={ViewAllRecipe}></PrivateRoute>
<PrivateRoute
exact
path="/recipe/edit/:editRecipe"
component={props => <EditRecipe {...props} />}></PrivateRoute>
<PrivateRoute
exact
path="/recipe/view/:id"
component={props => <SingleRecipe {...props} />}></PrivateRoute>
</Switch>
</Router>
);
}
export default App;
The useEffect with [] as the second parameter gets called only on the first render and it runs a function that checks the status and sets the state with that status. If the status is null you don't render anything because the async function hasn't run yet so you don't know the status (you could render a loading instead). When the status is the in the state it triggers a re-render with the status in the state

Handling Refreshtoken with React Router PrivateRoute

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.

Categories

Resources