I'm creating a protected route for my react project but context values and redux reducers data are not persistent. So what is the optimal way to set, for example isVerified to true if the user is logged. If isVerified === true go to homepage else redirect to login, isVerified needs to be mutated every change in route or refresh, because context or redux data is not persistent.
Do I need to create a separate backend api for just checking the token coming from the client side? then I will add a useEffect in the main App.tsx, something like:
useEffect(() => {
/*make api call, and pass the token stored in the localStorage. If
verified success then: */
setIsVerified(true)
}, [])
Then I can use the isVerified to my protected route
You can create a Middleware component wrapping both, Protected and Non-protected components routes. And inside of each just check if the user is logged, then render conditionally.
This is usually how I implemented,
Protected:
// AuthRoute.js
import React, { useEffect, useState } from "react";
import { Redirect, Route } from "react-router-dom";
export const AuthRoute = ({ exact = false, path, component }) => {
const [isVerified, setIsVerified] = useState(false);
const checkLoginSession = () => {
// Write your verifying logic here
const loggedUser = window.localStorage.getItem("accessToken") || "";
return loggedUser !== "" ? true : false;
};
useEffect(() => {
(async function () {
const sessionState = await checkLoginSession();
return setIsVerified(sessionState);
})();
}, []);
return isVerified ? (
<Route exact={exact} path={path} component={component} />
) : (
<Redirect to={"/login"} />
);
};
Non-protected:
import React from "react";
import { Redirect, Route } from "react-router-dom";
import { useEffect, useState } from "react";
export const NAuthRoute = ({ exact = false, path, component }) => {
const [isVerified, setIsVerified] = useState(false);
const checkLoginSession = () => {
// Write your verifying logic here
const loggedUser = window.localStorage.getItem("accessToken") || "";
return loggedUser !== "" ? true : false;
};
useEffect(() => {
(async function () {
const sessionState = await checkLoginSession();
return setIsVerified(sessionState);
})();
}, []);
return isVerified ?
<Redirect to={"/"} />
: <Route exact={exact} path={path} component={component} />;
};
I usually use this process and so far it is the most helpful way to make a route protected.
ProtectedRoute.js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
export const ProtectedRoute = ({component: Component, ...rest}) => {
const isSessionAuth = sessionStorage.getItem('token');
return (
<Route
{...rest}
render={props => {
if(isSessionAuth)
{
return (<Component {...props}/>);
}else{
return <Redirect to={
{
pathname: "/",
state: {
from: props.location
}
}
}/>
}
}
}/>
);
};
In the place where you are defining routes you can use this like this.
import React from "react";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import { ProtectedRoute } from './ProtectedRoute.js';
import SignUp from "./pages/SignUp";
import SignIn from "./pages/SignIn";
import Dashboard from "./pages/Dashboard";
import NotFound from "./pages/NotFound";
const Routes = () => (
<BrowserRouter>
<Switch>
<Route exact path="/" component={SignIn} />
<ProtectedRoute exact path="/dashboard" component={Dashboard} />
<Route path="/signup" component={SignUp} />
<Route path="*" component={NotFound} />
</Switch>
</BrowserRouter>
);
export default Routes;
Related
I am trying to create my own custom authentication using React, AWS Amplify, and React Router V6, and my goal is to protect certain routes so users that are not logged in can't access them.
My code is here:
import './App.css';
import { ThemeProvider, createTheme } from '#mui/material/styles';
import Navbar from './components/Navbar/Navbar';
import Dashboard from './components/Dashboard/Dashboard';
import Reports from './components/Reports/Reports';
import Patients from './components/Patients/Patients';
import { BrowserRouter, Navigate, Outlet, Route, Routes, useLocation, } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import '#aws-amplify/ui-react/styles.css';
import Signup from './components/Signup/Signup';
import Signin from './components/Signin/Signin';
import { Auth } from 'aws-amplify';
const darkTheme = createTheme({
palette: {
mode: 'dark',
}
});
const useAuth = () => {
const [user, setUser] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
})
return { user };
}
function RequireAuth() {
const auth = useAuth();
const location = useLocation();
console.log(auth);
return (
auth
? <Outlet/>
: <Navigate to='/signin' state={{ from: location}} replace/>
);
}
const App = () => {
return (
<ThemeProvider theme={darkTheme}>
<BrowserRouter>
<Routes>
<Route element={<RequireAuth />}>
<Route path="/" element={<Navbar />}>
<Route path="dashboard" element={<Dashboard />} />
<Route path="patients" element={<Patients />} />
<Route path="reports" element={<Reports />} />
</Route>
</Route>
<Route path="/signin" element={<Signin />} />
<Route path="/signup" element={<Signup />} />
</Routes>
</BrowserRouter>
</ThemeProvider>
);
}
export default App;
I have spent the whole day trying countless methods but I keep getting the same results. What I'm trying to do is protect 3 routes Dashboard, Patients, and Reports. Whenever I click the sign-in button, I get around 500 logs of the current user as shown below:
Does anyone know what is triggering this component to re-render infinitely (hence executing my console.log(user) function), and is there any solution to fix this?
It appears the useEffect hook is missing a dependency array, so its callback is triggered each render cycle. Since the effect updates state, it triggers a rerender.
Add a dependency array.
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, []);
If you need to run this effect more than once when the routes are mounted, then you may need to add a dependency, like location if/when the route path changes.
Example:
const { pathname } = useLocation();
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, [pathname]);
Since the useEffect hook runs at the end of the initial render cycle you may want to also conditionally wait to render the outlet or redirect until the user state is populated.
Example:
const useAuth = () => {
const [user, setUser] = useState(); // <-- initially undefined
useEffect(() => {
const fetchUser = async () => {
const response = await Auth.currentAuthenticatedUser();
setUser(response);
}
fetchUser();
}, []);
return { user };
}
...
function RequireAuth() {
const auth = useAuth();
const location = useLocation();
if (auth === undefined) {
return null; // or loading indicator, etc...
}
return (
auth
? <Outlet/>
: <Navigate to='/signin' state={{ from: location }} replace/>
);
}
I'm a beginner and I'm currently developing an application using a React Template. The template uses React Router v6 for the router with use Routes() hook.
Every route of the application is protected and can be only accessed by logging in
I'm planning to implement login using use Context() hook but I cant seem to figure out how to wrap the routes in the Provider tag.
My two doubts are:
How do I wrap my routes in the <Context Provider> tag
Should I wrap all my routes in an application like this.
First of all you will need the Context. I always prefer to write a hook:
import { createContext, useContext, useState } from "react";
const AuthContext = createContext({
isAuthenticated: false,
login: () => {},
logout: () => {}
});
export function AuthProvider({ children }){
const [isAuthenticated, setAuthenticated] = useState(false);
const login = () => {
setAuthenticated(true);
}
const logout = () => {
setAuthenticated(false);
}
return (
<AuthContext.Provider value={{isAuthenticated: isAuthenticated, login: login, logout: logout}}>
{children}
</AuthContext.Provider>
)
}
export default function AuthConsumer() {
return useContext(AuthContext);
}
Then you will need a private route component like this:
import React from 'react';
import { Navigate } from 'react-router-dom';
import useAuth from "./useAuth";
function RequireAuth({ children, redirectTo }) {
const { isAuthenticated } = useAuth();
return isAuthenticated ? children : <Navigate to={redirectTo} />;
}
export default RequireAuth;
Finally you will mix in your routes:
import React from 'react';
import { BrowserRouter, Route, Routes, Navigate } from 'react-router-dom';
import Login from './pages/Login';
import RequireAuth from './components/PrivateRoute';
import useAuth from "./components/useAuth";
const Home = () => <div><h1>Welcome home</h1></div>
const Dashboard = () => <h1>Dashboard (Private)</h1>;
function App() {
const { isAuthenticated } = useAuth();
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={
<RequireAuth redirectTo="/login">
<Dashboard />
</RequireAuth>
}>
</Route>
<Route path="/login" element={<Login />} />
<Route path='*' element={<Navigate to={isAuthenticated ? '/dashboard' : '/'} />} />
</Routes>
</BrowserRouter>
);
}
export default App;
I hope this will help.
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
}
}}
/>
);
};
I am migrating an app from Firebase to AWS Amplify. I want to create a React context which will provide route protection if the user is not logged in.
For example, my Auth.js file:
import React, { useEffect, useState, createContext } from 'react'
import fire from './firebase'
export const AuthContext = createContext()
export const AuthProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState(null)
useEffect(() => {
fire.auth().onAuthStateChanged(setCurrentUser)
}, [])
return (
<AuthContext.Provider value={{ currentUser }}>
{children}
</AuthContext.Provider>
)
}
And my App.js file:
import * as React from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import Navbar from './components/navbar/navbar'
import Home from './routes/Home'
import Register from './routes/Register'
import Footer from './components/footer/Footer'
import AlertProvider from './components/notification/NotificationProvider'
import MyAlert from './components/notification/Notification'
import { AuthProvider } from './Auth'
import PrivateRoute from './PrivateRoute'
const App = () => {
return (
<AuthProvider>
<BrowserRouter>
<AlertProvider>
<div className="app">
<Navbar />
<MyAlert />
<Switch>
<Route path="/" exact component={Home} />
<Route
path="/register"
exact
component={Register}
/>
<Route
path="/forgot-password"
render={(props) => <div>Forgot Password</div>}
/>
<Route path="*" exact={true} component={Home} />
</Switch>
<Footer />
</div>
</AlertProvider>
</BrowserRouter>
</AuthProvider>
)
}
export default App
This all works fine.
How would I do something similar with AWS Amplify? Essentially how would I create a Auth.js file that would wrap around my routes and give them a user context (which would update when the authentication status for the user is changed).
Thanks!
You can achieve this by setting up a custom protectedRoute HOC that will be used to protect any route that requires authentication. It will check if the user is signed-in and if the user is not signed-in then it will re-direct them to a specified route.
protectedRoute.js
import React, { useEffect } from 'react'
import { Auth } from 'aws-amplify'
const protectedRoute = (Comp, route = '/profile') => (props) => {
async function checkAuthState() {
try {
await Auth.currentAuthenticatedUser()
} catch (err) {
props.history.push(route)
}
}
useEffect(() => {
checkAuthState()
})
return <Comp {...props} />
}
export default protectedRoute
You can specify the default route or another route like the following:
// default redirect route
export default protectedRoute(Profile)
// custom redirect route
export default protectedRoute(Profile, '/sign-in')
You could also use the pre-built HOC from aws-amplify called withAuthenticator and that provides the UI as well as checking the users authentication status.
Sample use case for a profile page:
import React, { useState, useEffect } from 'react'
import { Button } from 'antd'
import { Auth } from 'aws-amplify'
import { withAuthenticator } from 'aws-amplify-react'
import Container from './Container'
function Profile() {
useEffect(() => {
checkUser()
}, [])
const [user, setUser] = useState({})
async function checkUser() {
try {
const data = await Auth.currentUserPoolUser()
const userInfo = { username: data.username, ...data.attributes, }
setUser(userInfo)
} catch (err) { console.log('error: ', err) }
}
function signOut() {
Auth.signOut()
.catch(err => console.log('error signing out: ', err))
}
return (
<Container>
<h1>Profile</h1>
<h2>Username: {user.username}</h2>
<h3>Email: {user.email}</h3>
<h4>Phone: {user.phone_number}</h4>
<Button onClick={signOut}>Sign Out</Button>
</Container>
);
}
export default withAuthenticator(Profile)
The routing for both would be the same and below I have linked a sample that I have used for both.:
import React, { useState, useEffect } from 'react'
import { HashRouter, Switch, Route } from 'react-router-dom'
import Nav from './Nav'
import Public from './Public'
import Profile from './Profile'
import Protected from './Protected'
const Router = () => {
const [current, setCurrent] = useState('home')
useEffect(() => {
setRoute()
window.addEventListener('hashchange', setRoute)
return () => window.removeEventListener('hashchange', setRoute)
}, [])
function setRoute() {
const location = window.location.href.split('/')
const pathname = location[location.length-1]
setCurrent(pathname ? pathname : 'home')
}
return (
<HashRouter>
<Nav current={current} />
<Switch>
<Route exact path="/" component={Public}/>
<Route exact path="/protected" component={Protected} />
<Route exact path="/profile" component={Profile}/>
<Route component={Public}/>
</Switch>
</HashRouter>
)
}
export default Router
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 "/".