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 />;
};
Related
I am having an issue where my react state is not updating.
I am trying to make a role-based protected route, following this tutorial style https://dev.to/iamandrewluca/private-route-in-react-router-v6-lg5, using the following component:
const MasterRoute = ({ children }) => {
const [role, setRole] = useState('');
const [isLoading, setIsLoading] = useState(false);
const checkAuth = async () => {
setIsLoading(true);
let response = await getRole();
setRole(response.role);
setIsLoading(false);
}
useEffect(() => {
checkAuth();
}, [])
useEffect(() => {
console.log(role);
}, [role])
return role === 'ADMIN' ? children : <Navigate to="/" />;
}
Logging the role in the useEffect function displays an empty result in the console.
Logging the variable response directly after the await function displays the correct response retrieved from the server.
I've tried to console log the role directly after the checkAuth() function in useEffect(), but also obtained an empty line in the console.
What could be the problem?
This component is used as the following in App.js file:
<Route
element={
<MasterRoute>
<Dashboard child={<Admin />}></Dashboard>
</MasterRoute>
}
path={'/roles'}
></Route>
Issue
It seems the general problem is that the initial role state is '', and since '' === 'ADMIN' evaluates false the <Navigate to="/" /> is rendered and the route changes. In other words, the route changed and MasterRoute likely isn't being rendered when the checkAuth call completes.
Solution
You could use that isLoading state to conditionally render null or some loading indicator while the auth/role status us checked. You'll want MasterRoute to mount with isLoading initially true so no routing/navigation action is taken on the initial render cycle.
Example:
const MasterRoute = ({ children }) => {
const [role, setRole] = useState('');
const [isLoading, setIsLoading] = useState(true); // <-- initially true
const checkAuth = async () => {
setIsLoading(true);
let response = await getRole();
setRole(response.role);
setIsLoading(false);
}
useEffect(() => {
checkAuth();
}, []);
useEffect(() => {
console.log(role);
}, [role]);
if (isLoading) {
return null; // or loading indicator/spinner/etc
}
return role === 'ADMIN' ? children : <Navigate to="/" replace />;
}
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 />;
}
Expected:
the async function checks if the user is authenticated, then return true, so that the protected component gets rendered, or false, that redirects the user to the login page.
What actually happens:
the getAuth() function returns "Promise ", breaking the code.
export default function RequireAuth({ children, redirectTo }) {
const BASE_API_URL = "https://api-backend.test";
const getAuth = async () => {
const isAuth = await axios
.get(BASE_API_URL + "/user_auth.php", {
withCredentials: true,
})
.then((res) => {
if (res.status === 201) {
return true;
}
})
.catch((err) => {
if (err.response.status === 401) {
return false;
}
});
return isAuth;
};
let isAuthenticated = getAuth();
return isAuthenticated ? children : <Navigate to={redirectTo} />;
}
This is how the "protected" component should be displayed according to the official documentation here.
<Route
path="/dashboard"
element={
<RequireAuth redirectTo="/login">
<Dashboard />
</RequireAuth>
}
/>
You have to do it properly using useEffect, try this
export default function RequireAuth({ children, redirectTo }) {
const BASE_API_URL = "https://api-backend.test";
const [isAuthenticated, setAuthenticated] = useState(false);
useEffect(() => {
const getAuth = () => {
const isAuth = axios
.get(BASE_API_URL + "/user_auth.php", {
withCredentials: true
})
.then((res) => {
if (res.status === 201) {
setAuthenticated(true);
}
})
.catch((err) => {
if (err.response.status === 401) {
setAuthenticated(false);
}
});
};
getAuth();
}, [redirectTo]);
if (isAuthenticated) {
return children;
}
return <Navigate to={redirectTo} />;
}
getAuth is an async function, so it will return a Promise. You have to either await its output or use .then to work with the resolved promise.
Side note, it's usually preferable to either use async/await or .then, but not both.
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
}