I am trying to create protected routing and have created a component called Protected which fetches the access token from the backend if user exists and displays the protected components if user exists or gets navigated to the Not logged in component. This functionality is not working as expected. For some reason I am getting current user as undefined. Is it because the protected route's Navigate is getting rendered even before access token gets generated and stored in localStorage?
Below is the code
import React, { useEffect, useState } from 'react'
import { Outlet, Navigate } from 'react-router-dom'
import authService from '../services/auth.service';
function Protected() {
const [currentUser, setCurrentUser] = useState(undefined);
console.log(currentUser);
useEffect(() => {
const user = authService.getCurrentUser();
if (user) {
setCurrentUser(user);
}
}, []);
return (
currentUser ? <Outlet/> : <Navigate to='/notloggedin'/>
)
}
export default Protected
authService.js
import axios from "axios";
const API_URL = "http://localhost:3000/auth";
const signup = (email, password) => {
try {
return axios
.post(API_URL + "/signup", {
email,
password,
})
.then((response) => {
//console.log(response.data.errors);
if (response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
}
console.log(response.data.errors?.map((error) => console.log(error.msg)))
return response.data;
});
} catch(error) {
return error;
}
};
const login = (email, password) => {
return axios
.post(API_URL + "/login", {
email,
password,
})
.then((response) => {
if (response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
}
console.log(response.data.errors?.map((error) => console.log(error.msg)))
return response.data;
});
};
const logout = () => {
localStorage.removeItem("user");
};
const getCurrentUser = () => {
return JSON.parse(localStorage.getItem("user"));
};
const authService = {
signup,
login,
logout,
getCurrentUser,
};
export default authService;
App.js
function App() {
return (
<>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/notloggedin" element={<NotLoggedIn/>} />
<Route element={<Protected/>}>
<Route path="/restaurants" element={<Food_Main />} />
<Route path="/:food/:id" element={<Food_Detail />} />
<Route path="/payment" element={<PaymentDetails />} />
<Route path="/thankyou" element={<ThankYou />} />
</Route>
</Routes>
</>
);
}
export default App;
The initial currentUser value is undefined which is also a falsey value and the useEffect hook runs at the end of the render cycle, so on the initial render the Navigate component is returned and the user is navigated off the route you are trying to protect.
Render null or some loading indicator while the auth status is being validated/verified.
Example:
function Protected() {
const [currentUser, setCurrentUser] = useState(undefined);
useEffect(() => {
const user = authService.getCurrentUser();
setCurrentUser(!!user); // <-- sets to defined boolean value
}, []);
if (currentUser === undefined) {
return null; // or loading indicator/spinner/etc...
return currentUser
? <Outlet/>
: <Navigate to='/notloggedin' replace />;
}
Related
I have used react-router-dom for Navigation..but My Problem is without authentication also Dashboard Screen is being visible for mili seconds.
App.js
<Route index path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/Login" element={<Login />} />
ProtectedRoute
const ProtectedRoute = ({ children }) => {
const { user } = useMyContext();
if (!user) {
return <Navigate to="/Login" />;
}
return children;
};
export default ProtectedRoute;
Login.js
onClick..
await login(data.get('email'), data.get('password'));
navigate('/', { replace: true })
Context.js
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password)
}
function logOut() {
return signOut(auth);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
});
return () => {
unsubscribe();
}
}, [])
How can I protect my protected screen from unauthorized access?
The issue is that your ProtectedRoute component doesn't wait for the authentication status to be confirmed. In other words, the default user state masks one of either the authenticated or unauthenticated status.
It should conditionally render a loading indicator while onAuthStateChanged is making the first call to determine the user's authentication status. For the initial user state value use a value that is neither a defined user object in the case of an authenticated user or null in the case of an unauthenticated user. undefined is a good initial value.
Example:
Context
const [user, setUser] = React.useState(); // initially undefined
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
function logOut() {
return signOut(auth);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
});
return unsubscribe;
}, []);
ProtectedRoute
const ProtectedRoute = ({ children }) => {
const { user } = useMyContext();
if (user === undefined) {
return null; // or loading indicator/spinner/etc
}
return user ? children : <Navigate to="/Login" replace />;
};
I am doing a React.js project. I am retrieving dat from the Star Wars API rendering a list of films on the screen and now I am trying to route every film to its own page through react-router-dom. Unfortunately, I am not able to make it work. it crash when I try to routing dynamically.
UPDATE AFTER ANSWER OF REZA
This is the App.js:
import './App.css';
import { Route, Routes } from "react-router-dom";
import Home from './components/Home';
import ItemContainer from './components/ItemContainer';
import Navbar from './components/Navbar';
function App() {
return (
<>
<Navbar />
<Routes>
<Route exact path='/' element={<Home />} />
<Route exact path="/:movieId" element={<ItemContainer />} />
</Routes>
</>
);
}
export default App;
This is the ItemContainer:
import { useEffect, useState } from "react";
import MovieDetail from "../MovieDetail";
import { useParams } from "react-router-dom";
const ShowMovie = (movieId) => {
const [result, setResult] = useState([]);
const fetchData = async () => {
const res = await fetch("https://www.swapi.tech/api/films/");
const json = await res.json();
setResult(json.result);
}
useEffect(() => {
fetchData();
}, []);
return new Promise((res) =>
res(result.find((value) => value.properties.title === movieId)))
}
const ItemContainer = () => {
const [films, setFilms] = useState([]);
const { movieId } = useParams([]);
console.log('params movieId container', movieId)
useEffect(() => {
ShowMovie(movieId).then((value) => {
setFilms(value.properties.title)
})
}, [movieId])
return (
<MovieDetail key={films.properties.title} films={films} />
);
}
export default ItemContainer;
The console.log doesn't give anything.
UPDATE
Also, this is the whole code in sandbox.
ShowMovie is declared like a React component, but used like a utility function. You shouldn't directly invoke React function components. React functions are also to be synchronous, pure functions. ShowMovie is returning a Promise with makes it an asynchronous function.
Convert ShowMovie into a utility function, which will basically call fetch and process the JSON response.
import { useEffect, useState } from "react";
import MovieDetail from "../MovieDetail";
import { useParams } from "react-router-dom";
const showMovie = async (movieId) => {
const res = await fetch("https://www.swapi.tech/api/films/");
const json = await res.json();
const movie = json.result.find((value) => value.properties.episode_id === Number(movieId)));
if (!movie) {
throw new Error("No match found.");
}
return movie;
}
const ItemContainer = () => {
const [films, setFilms] = useState({});
const { movieId } = useParams();
useEffect(() => {
console.log('params movieId container', movieId);
showMovie(movieId)
.then((movie) => {
setFilms(movie);
})
.catch(error => {
// handle error/log it/show message/ignore/etc...
setFilms({}); // maintain state invariant of object
});
}, [movieId]);
return (
<MovieDetail key={films.properties?.title} films={films} />
);
};
export default ItemContainer;
Update
The route path should include movieId, the param you are accessing in ItemContainer. The sub-path "film" should match what you link from in Home. In Home ensure you link to the /"film/...." path.
<Routes>
<Route path="/films" element={<Home />} />
<Route path="/film/:movieId" element={<ItemContainer />} />
<Route path="/" element={<Navigate replace to="/films" />} />
</Routes>
In ItemContainer you should be matching a movie object's episode_id property to the movieId value. Store the entire movie object into state, not just the title.
const showMovie = async (movieId) => {
const res = await fetch("https://www.swapi.tech/api/films/");
const json = await res.json();
const movie = json.result.find((value) => value.properties.episode_id === Number(movieId)));
if (!movie) {
throw new Error("No match found.");
}
return movie;
}
...
useEffect(() => {
console.log("params movieId container", movieId);
showMovie(movieId)
.then((movie) => {
setFilms(movie);
})
.catch((error) => {
// handle error/log it/show message/ignore/etc...
setFilms({}); // maintain state invariant of object
});
}, [movieId]);
You should also use Optional Chaining on the more deeply nested films prop object properties in MovieDetail, or conditionally render MovieDetail only if the films state has something to display.
const MovieDetail = ({ films }) => {
return (
<div>
<h1>{films.properties?.title}</h1>
<h3>{films.description}</h3>
</div>
);
};
Demo:
Modify App.js like this:
function App() {
return (
<>
<Navbar />
<Routes>
<Route exact path="/" element={<Home />} />
<Route exact path="/MovieDetail/:movieId" element={<ItemContainer />} />
</Routes>
</>
);
}
Private route /profile rendering me an error.
My app works until I try to go on /profile.
I have never seen this error yet, so while Im searching Im posting here, maybe someone has an idea.
Also Im looking for a good documentation/tutorial on how to make private routes using jwt and js-cookie.
error
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
App.js
import { useState } from 'react';
import Axios from "axios";
import Cookies from "js-cookie";
import { BrowserRouter as Router, Link, Route, Switch } from "react-router-dom";
import ProtectedRoute from './components/ProtectedRoute';
import Profile from "./pages/Profile";
const App = () => {
const [emailRegistration, setEmailRegistration] = useState("");
const [passwordRegistration, setPasswordRegistration] = useState("");
const [emailLogin, setEmailLogin] = useState("");
const [passwordLogin, setPasswordLogin] = useState("");
const [isAuth, setIsAuth] = useState(false);
const register = () => {
Axios.post('http://localhost:3000/api/signup', {
user:{
email: emailRegistration,
password: passwordRegistration
}
}).then((response) => {
console.log(response);
});
};
const login = () => {
fetch("http://localhost:3000/api/login", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user: {
email: emailLogin,
password: passwordLogin,
},
}),
})
.then((res) => {
if (res.ok) {
console.log(res.headers.get("Authorization"));
localStorage.setItem("token", res.headers.get("Authorization"));
setIsAuth(true)
return res.json();
} else {
return res.text().then((text) => Promise.reject(text));
}
})
.then((json) => console.dir(json))
.catch((err) => console.error(err));
};
const getArticles = () => {
fetch("http://localhost:3000/articles", {
headers: {
"Content-Type": "application/json",
Authorization: localStorage.getItem("token"),
},
})
.then((res) => {
if (res.ok) {
return res.json();
} else if (res.status == "401") {
throw new Error("Unauthorized Request. Must be signed in.");
}
})
.then((json) => console.dir(json))
.catch((err) => console.error(err));
}
const logout = () => {
fetch("http://localhost:3000/api/logout", {
method: "delete",
headers: {
"Content-Type": "application/json",
Authorization: localStorage.getItem("token"),
},
})
.then((res) => {
if (res.ok) {
setIsAuth(false)
return res.json();
} else {
return res.json().then((json) => Promise.reject(json));
}
})
.then((json) => {
console.dir(json);
})
.catch((err) => console.error(err));
};
const testBearer = () => {
let cookie = Cookies.get('token');
console.log(cookie)
}
return (
<Router>
<div className="app">
{isAuth ?
(
<button onClick={logout} > logout </button>
)
:
(
<div className="auth">
<div className="registration">
<h1> Registration </h1>
<label> Email </label>
<input
type="email"
onChange={(e) => {
setEmailRegistration(e.target.value);
}}
/>
<label> Password </label>
<input
type="password"
onChange={(e) => {
setPasswordRegistration(e.target.value);
}}
/>
<button onClick={register} > Register </button>
</div>
<div className="login">
<h1> Login </h1>
<label> Email </label>
<input
type="email"
onChange={(e) => {
setEmailLogin(e.target.value);
}}
/>
<label> Password </label>
<input
type="password"
onChange={(e) => {
setPasswordLogin(e.target.value);
}}
/>
<button onClick={login} > Login </button>
</div>
</div>
)
}
<br />
<hr />
<br />
<button onClick={getArticles} >getArticles </button>
<button onClick={testBearer} >testCookie </button>
<button onClick={logout} > logout </button>
</div>
<Route path="/">
</Route>
<ProtectedRoute exact path="/profile" component={Profile} isAuth={isAuth} />
</Router>
)};
export default App;
Profile.js
import React from 'react';
import {withRouter} from "react-router-dom";
const Profile = () => {
return (
<div>
if you see this you entered the auth.
</div>
);
};
export default withRouter(Profile);
ProtectedRoute.js
import React from 'react';
import {Route, Redirect} from "react-router-dom";
const ProtectedRoute = ({isAuth: isAuth, component: Component, ...rest }) => {
return (
<Route {...rest} render={(props) => {
if (isAuth) {
return <Component />
} else {
return (
<Redirect to={{pathname: "/profile", state: {from: props.location }}} />
);
}
}}/>
);
};
export default ProtectedRoute;
Let your ProtectedRoutes to have a type based on their auth capabilities such as LoginRoute and PrivateRoute. Your Profile route acts as the login page. Therefore, it doesn't need to be user authenticated to view that page. Then create another route as dashboard, which will be the page user able to view in a successful log in. Now, keep your routes as follows.
<Route path="/"></Route>
<ProtectedRoute
exact
path="/profile"
component={Profile}
isAuth={isAuth}
type="LoginRoute"
/>
<ProtectedRoute
exact
path="/dashboard"
component={() => <div>User Entered into Dashboard</div>}
isAuth={isAuth}
type="PrivateRoute"
/>
Then, you need to handle the type based authentication functionality inside your ProtectedRoute component as follows.
import React from "react";
import { Route, Redirect } from "react-router-dom";
const getRouteVariables = (type, isAuth) => {
switch (type) {
case "LoginRoute":
return [!isAuth, "/dashboard"];
case "PrivateRoute":
return [isAuth, "/profile"];
default:
return [isAuth, "/profile"];
}
};
const ProtectedRoute = ({ type, isAuth, component: Component, ...rest }) => {
const [isRouteChanged, redirectPath] = getRouteVariables(type, isAuth);
return (
<Route
{...rest}
render={(props) => {
if (isRouteChanged) {
return <Component />;
} else {
return (
<Redirect
to={{ pathname: redirectPath, state: { from: props.location } }}
/>
);
}
}}
/>
);
};
export default ProtectedRoute;
getRouteVariables function return isRouteChanged and redirectPath. isRouteChanged means whether route needs to be changed based on isAuth and redirectPath means which path needs to be taken if redirection happens.
For LoginRoute type /profile route only comes up when isAuth becomes false. If isAuth is true, then moving to /profile route will redirect to /dashboard.
For PrivateRoute type /dashboard route only possible to access when isAuth becomes true. If isAuth is false, then moving to /dashboard route will redirect to /profile.
I am using functional component which provides authentication to the specific routes such as /dashboard using server side authentication happening in useeffect of my app function.
Authentication is working fine and also when I click dashboard button I get directed to dashboard when I am logged in else redirected to home page.
The problem arises when I reload the /dashboard page . At that time what I observe is everything is re-rendered and before going through use effect it first passes from AuthenticatedRoute which doesn't give authentication because server side auth is happening in use effect and I am directly redirected to home page even when I am logged in.
App.js
const AuthenticatedRoute = ({ children, isAuthenticated , ...rest }) => {
return (
<Route
{...rest}
render={() =>
isAuthenticated ? (
<div>{children}</div>
) : (
<Redirect to="/home" />)}
></Route>
);
};
Route code:
App.js
<AuthenticatedRoute isAuthenticated = {isAuthenticated} path="/dashboard">
<AgentDashboard />
</AuthenticatedRoute>
App.js
function App() {
const [authTokenValid, setauthTokenValid] = useState(false)
useEffect(() => {
const token = localStorage.getItem('Authorization')
const authMainPageCheck = async () => {
await axios.post(tokenAuthCheckURL , {
'token':token,
}).then(result => result.data).then(
result => {
if(result.status === 200){
console.log("Authentication Given ")
setauthTokenValid(true)
}
else{
console.log("Authentication Not Given ")
setauthTokenValid(false)
}
})
}
authMainPageCheck()
}, [])
Please try this code below:
import React, { useEffect, useState } from "react";
import { Route, Redirect, BrowserRouter } from "react-router-dom";
// import axios from "axios";
// #ts-ignore
const AuthenticatedRoute = ({ children, isAuthenticated, ...rest }) => {
return (
<Route
{...rest}
render={() =>
isAuthenticated ? (
<div>
{children}
</div>
) : (<Redirect to="/error" />)
}
></Route>
);
};
export const App = () => {
// Set initial value to null
const [authTokenValid, setauthTokenValid] = useState(null)
useEffect(() => {
// Wait for request to complete
// Example...
setTimeout(() => {
// #ts-ignore
setauthTokenValid(true);
}, 3000);
// const token = localStorage.getItem('Authorization');
// const authMainPageCheck = async () => {
// await axios.post(tokenAuthCheckURL, {
// token,
// }).then(result => result.data).then(
// result => {
// if (result.status === 200) {
// console.log("Authentication Given ")
// setauthTokenValid(true)
// } else {
// console.log("Authentication Not Given ")
// setauthTokenValid(false)
// }
// }
// )
// }
}, []);
if (authTokenValid === null)
// Wait Until a Non Null Response Comes....
return (<h1>Loading...</h1>); // Create a new loading component...
else
return (
<BrowserRouter>
<AuthenticatedRoute isAuthenticated={authTokenValid} exact path="/">
<h1>This is Authenticated Home!!!</h1>
</AuthenticatedRoute>
<AuthenticatedRoute isAuthenticated={authTokenValid} exact path="/dashboard">
<h1>This is Authenticated Dashboard!!!</h1>
</AuthenticatedRoute>
</BrowserRouter>
);
}
I use the axios post to request to the back-end if the user have access to the application. The problem is the axios returns undefined and then true or false . Have a private Route to manage what to do in case returns true or false (in this case undefined = false) ,is axios the problem or is there some other way? like wait until returns true or false
IsLogin.jsx
import React from 'react'
const axios = require('axios');
export const AuthContext = React.createContext({})
export default function Islogin({ children }) {
const isAuthenticated =()=>{
try{
axios.post('/api/auth').then(response => {
var res = response.data.result;
console.log(res)
return res
})
} catch (error) {
console.error(error);
return false
}
}
var auth = isAuthenticated()
console.log(auth);
return (
<AuthContext.Provider value={{auth}}>
{children}
</AuthContext.Provider>
)
}
privateRoute.js
import React, { useContext } from 'react';
import { Route, Redirect } from 'react-router-dom';
import {AuthContext} from '../utils/IsLogin';
const PrivateRoute = ({component: Component, ...rest}) => {
const {isAuthenticated} = useContext(AuthContext)
return (
// Show the component only when the user is logged in
// Otherwise, redirect the user to /unauth page
<Route {...rest} render={props => (
isAuthenticated ?
<Component {...props} />
: <Redirect to="/unauth" />
)} />
);
};
export default PrivateRoute;
app.js
class App extends Component {
render() {
return (
<>
<BrowserRouter>
<Islogin>
<Header/>
<Banner/>
<Switch>
<PrivateRoute exact path="/index" component={Landing} />
<PrivateRoute path="/upload" component={Upload} exact />
<PublicRoute restricted={false} path="/unauth" component={Unauthorized} exact />
</Switch>
</Islogin>
</BrowserRouter>
</>
);
}
}
You don't want to return anything in your post request. You should be updating your context store
const isAuthenticated = () => {
try {
axios.post('/api/auth').then(response => {
var res = response.data.result;
console.log(res)
// update your context here instead of returning
return res
})
} catch (error) {
console.error(error);
return false
}
}
In your private route, have a componentDidUpdate style useEffect hook to check for changes in authentication status and update an internal flag on an as-needed basis
const PrivateRoute = ({ component: Component, ...rest }) => {
const { isAuthenticated } = useContext(AuthContext)
const [validCredentials, setValidCredentials] = React.useState(false)
React.useEffect(() => {
if (typeof isAuthenticated === 'boolean') {
setValidCredentials(isAuthenticated)
}
}, [isAuthenticated])
return (
// Show the component only when the user is logged in
// Otherwise, redirect the user to /unauth page
<Route {...rest} render={props => (
validCredentials ?
<Component {...props} />
: <Redirect to="/unauth" />
)} />
);
};
I am curious as to why you didn't use 'async await',lol.
You are making a post request to the endpoint '/api/auth',but you didn't give it any data to post,like:
try{
axios.post('/api/auth',{username,password}).then(response => {
var res = response.data.result;
console.log(res)
return res
})
} catch (error) {
console.error(error);
return false
}