Struggling for passing React routing param - reactjs

I'd like to protect a react route and redirect toward this route when the user has logged in.
Here is my protectedRoute :
const ProtectedRoute = () => {
const location = useLocation();
return auth.getCurrentUser() ? <Outlet /> :
<Navigate to="/login" state={{from: location}}/>
};
And here is my Login component :
class LoginForm extends Form {
state = {
data: { username: "", password: "" },
errors: {}
};
schema = {
username: Joi.string()
.required()
.label("Username"),
password: Joi.string()
.required()
.label("Password")
};
doSubmit = () => {
// Some code for validate login
const { state } = this.props.location;
window.location = state ? state.from.pathname : "/";
};
render() {
if (auth.getCurrentUser()) return <Navigate to="/" />;
return (
<div>
<h1>Login</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("username", "Username")}
{this.renderInput("password", "Password", "password")}
{this.renderButton("Login")}
</form>
</div>
);
}
}
The program stops at doSubmit function when executing :
const { state } = this.props.location;
What do I do wrong ?

Issue
In react-router-dom v6 the Route components no longer have route props (history, location, and match), and the current solution is to use the React hooks "versions" of these to use within the components being rendered. React hooks can't be used in class components though.
To access the location object with a class component you must either convert to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.
Also, in v6 the history object was replaced by a navigate function. This can be accessed via React hook and injected as a prop as well.
Solution
I won't cover converting a class component to function component. Here's an example custom withRouter HOC:
import { useLocation, useNavigate } from 'react-router-dom';
const withRouter = WrappedComponent => props => {
const location = useLocation();
const navigate = useNavigate();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
location={location}
navigate={navigate}
// etc...
/>
);
};
And decorate the LoginForm component with the new HOC.
export default withRouter(LoginForm);
This will inject a location prop for the class component.
doSubmit = () => {
const { location, navigate } = this.props;
// Some code for validate login
const { state } = location;
navigate(state.from || "/", { replace: true });
};

Related

How can I control access to certain pages using custom claims on the client [duplicate]

This question already has answers here:
How to create a protected route with react-router-dom?
(5 answers)
Closed 14 days ago.
Essentially,
I have a higher order component (HoC), that takes routes, and determines whether or not the currently authenticated user can access the component passed in via props.
Im trying to have a useEffect in my HoC that checks the ID token result of a user, and extracts the custom claims on the user, which were created for the user on the server side at the time of creation using the firebaseAdmin SDK, the custom claims are just the following:
{trial: true, isSubscribed: false}
Next it stores the value of these custom claims in the state for the component and uses a series of if else statements to determine whether the user is authenticated, and whether or not the user is subscribed if the route trying to be accessed is one requiring a subscription.
Lastly in the return method of my HoC, I render the component conditionally using a ternary operator.
return (
<>
{isAuthenticated? (
props.component
) : (
<Navigate to='/' />
)}
</>
)
Here is my app.js where I pass in the protected routes into my Hoc which is ProtectedPage as a component prop, I also specify whether or not the route going into ProtectedPage requires a subscription or not using the isSubcribedRoute prop:
//App.js
import "./styles/bootstrap.css";
import Home from './components/Home'
import Signup from './components/Signup';
import Login from './components/Login'
import LandingPage from './components/LandingPage'
import Account from './components/Account/Account';
import UserDetails from './components/Account/UserDetails';
import Quizzes from './components/Quizzes';
import Lessons from './components/Lessons';
import Learn from './components/Learn/Learn';
import LearnCourses from './components/Learn/LearnCourses';
import Features from './components/Features'
import ProtectedPage from './components/ProtectedPage';
import { FbMethodContextProvider } from "./firebase/fbMethodContext";
import { createBrowserRouter, RouterProvider} from 'react-router-dom';
import { AuthContextProvider, } from "./firebase/authContext";
const router = createBrowserRouter([
{
path: '/',
element: <Home />,
children: [
{
index: true,
element: <LandingPage />
},
{
path: '/signup',
element: <Signup />
},
{
path: '/login',
element: <Login />
},
{
path:'/features',
element: <ProtectedPage component={<Features />} isSubscribedRoute={false } />
}
]
},
{
path: '/account',
element: <ProtectedPage component={ <Account />} isSubscribedRoute={true}/>,
children:[
{
index: true,
element: <UserDetails/>,
}
]
},
{
path: '/quizzes',
element: <Quizzes />,
},
{
path: '/lessons',
element: <Lessons />
},
{
path: '/learn',
element: <ProtectedPage component ={<Learn />} isSubscribedRoute={false}/>,
children:[
{
index: true,
element: <LearnCourses />
}
]
}
]);
function App() {
return (
<FbMethodContextProvider>
<AuthContextProvider>
<RouterProvider router={router}/>
</AuthContextProvider>
</FbMethodContextProvider>
);
}
export default App;
Next this is my protected page component:
//ProtectedPage.js
import React,{useEffect, useState} from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import {auth} from '../firebase/firebaseConfig';
import {redirect , useNavigate, Navigate} from 'react-router-dom';
const ProtectedPage = (props) => {
const [user, setUser] = useState(null)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [trial, setTrial] = useState()
const [isSubscribed, setIsSubscribed] = useState()
const isSubscribedRoute = props.isSubscribedRoute
const navigate = useNavigate();
useEffect(()=>{
onAuthStateChanged(auth, async (user) =>{
if(user){
const idTokenResult = await user.getIdTokenResult(true);
const onTrial = idTokenResult.claims.trial;
setTrial(onTrial);
console.log(trial);
const Subscribed = idTokenResult.claims.subscribed;
setIsSubscribed(Subscribed)
console.log(isSubscribed)
console.log(`Trial is ${onTrial} & Subscribed is ${isSubscribed}`)
setUser(user);
if(trial || isSubscribed) {
setIsAuthenticated(true);
}
} else {
setUser(user)
}
})
let isAuthorized;
if ( isSubscribedRoute) {
if(isSubscribed){
isAuthorized = true;
console.log('user is subscribed and can access this page')
} else if (trial && isSubscribed === false){
navigate('/login')
}
}
if (!isAuthenticated){
console.log('not authenticated')
navigate('/login')
//The code works when I replace this call to the navigate hook above with react-router-dom's redirect, and replace the <Navigate /> component in the render method below with <div></div>
} else if(!isSubscribedRoute && trial){
isAuthorized = true;
}else if(!isAuthorized){
console.log('not authorized')
navigate('/login')
}
} )
return (
<>
{isAuthenticated? (
props.component
) : (
<Navigate to='/' />
)}
</>
)
}
export default ProtectedPage
Essentially whenever a user logs in or signsup it should navigate them to the learn page. It all worked fine before I tried to control access with custom claims.
Now the only way I can get it to work is by replacing the navigate with redirect('/') in the if(!isAuthenticated) code block and replacing the <Navigate =to '/' /> with in the return function at the bottom.
Any help will be appreciated, or if any better methods or patterns are available please let me know as Im quite new to coding and don't know if my method is even good.
You should consider creating a hook that uses a global context where you handle all checks for the current logged in user. (Here is a guide to set it up)
Inside this hook you can have different states depending on if the user is on trial or subscribed. This way you have direct access when redirecting via routes and don't need to do the check every time.
Right now you fetch the token on each mount but you do the check for isAuthenticated before you've fetched the token.
If you use global context you will fetch token one time (the first time you enter the site) and it will be saved between routes.
This will probably resolve your problems.
Edit:
To make sure you are making comparisons once the fetch is complete you could use a loading state:
Initialize a boolean state to true: const [loading, setLoading] = useState(true)
Right after const idTokenResult = await user.getIdTokenResult(true); set the loading state to false:
const idTokenResult = await user.getIdTokenResult(true);
setLoading(false)
Then change your return to:
return (
<>
{loading? (
<span>Loading...</span>
) : isAuthenticated? (
props.component
) : (
<Navigate to='/' />
)}
</>
)

this.props.history.push is undefined

I am creating a form with some predefined values, and i want to route to the dashboard page once the form is submitted. I am using handleLoginSubmit func on the onsubmit of the form. For that, I have the following code:
handleLoginSubmit = (e) => {
e.preventDefault();
let hardcodedCred = {
email: "email#email.com",
password: "password123",
};
if (
this.state.email == hardcodedCred.email &&
this.state.password == hardcodedCred.password
) {
//combination is good. Log them in.
//this token can be anything. You can use random.org to generate a random string;
// const token = "123456abcdef";
// sessionStorage.setItem("auth-token", token);
//go to www.website.com/todo
// history.push("/dashboard");
this.props.history.push("/dashboard");
// console.log(this.state.route);
console.log(this.context);
console.log("logged in");
// <Link to={location} />;
} else {
//bad combination
alert("wrong email or password combination");
}
};
But, I am receiving the error saying that history is undefined.
Please help me out here
You need to export the component with router to access history
import { withRouter } from 'react-router-dom';
export default withRouter(ComponentName)
if your component is linked with Route then you can directly access that in this.props but if its sub-component or children of some component then you cant access it into this.props.
so there is multiple way to solve it
pass history as props in your component if that component is linked with Route
<Component history={this.props.history} />
use withRouter HOC, that bind all Route props in your component
import { withRouter } from 'react-router-dom';
export default withRouter(Component)
use useHistory hook and save it to constant ( functional component )
import { useHistory } from 'react-router-dom';
const history = useHistory();
Update of Nisharg Shah's answers of part 3 for future reference.
If you are using react-router-dom v6, then use useNavigate instead of useHistory.
You can use:
import { useNavigate } from "react-router-dom";
let navigate = useNavigate();
OR
import { useNavigate } from "react-router-dom";
function App() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}

How to send params in useHistory of React Router Dom?

I am using React Router hooks for navigation useHistory.
Navigate : history.push("/home", { update: true });
In home : I am trying to get params let {update} = useParams();
But update is always undefined. Whats wrong with this code. Any suggestions ?
The second parameter in the history.push() method is actually known as the location state,
history.push(path, [state])
Depending on your requirements, you may want to pass update as part of the location state, or the query string.
history.push({
pathname: '/home',
search: '?update=true', // query string
state: { // location state
update: true,
},
});
As stated on the React-Router documentation, you can access the state by accessing the location props. In your case, to get the value for update,
On class components, assuming that it is connected to the router,
this.props.location
For functional components, you can use the useLocation hook to access the location object.
import { useLocation } from 'react-router-dom';
.
.
const location = useLocation();
console.log(location.state.update) // for location state
console.log(location.search) // for query strings;
If you are using React Hooks follow this method because this.props is only available in React Class.
Component One:
import React from 'react'
import { useHistory } from "react-router-dom";
const ComponentOne = () => {
const history = useHistory();
const handleSubmit = () => {
history.push('/component-two',{params:'Hello World'})
}
return (
<div>
<button onClick={() => {handleSubmit()}}>Fire</button>
</div>
)
}
Component Two:
import React from 'react'
import { useLocation } from "react-router-dom";
const ComponentTwo = () => {
const location = useLocation();
const myparam = location.state.params;
return (
<div>
<p>{myparam}</p>
</div>
)
}
This is how you can pass
history.push("/home", { update: true });
and access like this if it's stateless component.
props.location.state.update;
if class based component.
this.props.location.update;
There's also a simpler way to access the state passed on if you're using functional components:
First, pass in the state in history.push
history = useHistory();
history.push('/path-to-component-2', 'state')
Next, u can retrieve the state in the location props
const Component2 = ({ location }) => {
console.log(location.state);
return null;
};

How to control routing for protected pages in React app?

Do I need check auth in every protected page container and if it false redirect to login page? what can i do if i have a lot of protected pages?
You can use a Higher order component (HOC) for your router. use a PrivateRouter hoc.
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={(props) => (
fakeAuth.isAuthenticated === true
? <Component {...props} />
: <Redirect to='/login' />
)} />
)
use this instead of route.
<PrivateRoute component={component} {...props} />
As #Nisfan said, making a HOC is not a bad idea.
For example:
// This HOC redirects to the landing page if user isn't logged in.
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { LANDING } from '../constants/routes';
const ERROR_MESSAGE = 'You need to be logged in to access that page.';
const withAuthentication = (condition, route = LANDING) => (Component) => {
class WithAuthentication extends React.Component {
componentDidMount() {
if (!condition(this.props.userState.loggedIn)) {
this.props.history.push(route);
// TODO: show error if you want
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.userState.loggedIn !== this.props.userState.loggedIn) {
if (!condition(nextProps.userState.loggedIn)) {
this.props.history.push(route);
// TODO: show error if you want
}
}
}
render() {
return this.props.userState.loggedIn ? <Component /> : null;
}
}
WithAuthentication.propTypes = {
history: PropTypes.object.isRequired,
userState: PropTypes.object,
};
const mapStateToProps = state => ({
userState: state.userState,
});
const temp = connect(mapStateToProps)(WithAuthentication);
return withRouter(temp);
};
export default withAuthentication;
Then, when you want to protect a route, you can wrap your component in withAuthentication with a condition.
For example, your condition could be whether or not the user is signed in, or whether or not the user is signed in and is an admin, etc.

Redirect router on after login

I am trying to figure out how to redirect my react app to proper page after it has been authenticated through the login.
Here is my App.js file with the routed (without imports):
ReactDOM.render((
<Provider store={store}>
<Router history={history}>
<Switch>
<PrivateRoute path="/test" component={Test} />
<Route path="/login" component={Login} />
<PrivateRoute path="/" render={() => <h1>Welcome</h1>} />
</Switch>
</Router>
</Provider>
), document.getElementById('root'));
I am using PrivateRoute component to make sure private routes get authenticated:
class PrivateRoute extends Route {
render() {
const { component: Component, isAuthenticated } = this.props;
let propsCopy = Object.assign({}, this.props);
delete propsCopy.component;
delete propsCopy.isAuthenticated;
return (
isAuthenticated
? <Component {...propsCopy} />
: <Redirect to={{
pathname: LOGIN_PATH,
state: { from: this.props.location }
}} />
);
}
}
/**
* Maps properties from Redux store to this component.
* #param {Object} state Redux object
* #return {Object} mapper properties
*/
function mapStateToProps(state) {
// pull out auth element from Redux store state
const { auth } = state;
// extract authenticated element from auth object
const { isAuthenticated } = auth;
return {
isAuthenticated
}
}
export default connect(mapStateToProps)(PrivateRoute);
My Login component that get redirected to (simplified for the sake of the example:
class LoginForm extends Component {
constructor() {
super();
this.state = {
email: '',
password: '',
}
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
handleFormSubmit(event) {
//event.preventDefault();
const validation = this.validator.validate(this.state);
this.setState({ validation });
this.submitted = true;
if (validation.isValid) {
// submit form here
this.props.loginUser({
email: this.state.email,
password: this.state.password
});
}
}
render() {
return (
// My login FROM code here
)
}
}
function mapStateToProps(state) {
return {
isFetching: state.auth.isFetching,
loginError: state.auth.loginError,
isAuthenticated: state.auth.isAuthenticated
};
}
function mapDispatchToProps(dispatch, props, state) {
return {
loginUser: (credentials) => {
dispatch(loginUser(credentials));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
Right now it works where it redirects to the LoginForm component if isAuthenticated is false. I can submit the login form to my login service and receive a success response and set isAuthenticated.
My question is how to I redirect to the original route? Where is redirection normally done? I'm assuming it's never done in the reducer so it would have to be done in the LoginForm component right?
I know there are a lot of resources discuss this whole login flow but I can't find one that deals with this issue(which surprised me). Everyone redirects to a specific page ('/', '/home' etc) but how do i capture and redirect to the original route.
The PrivateRoute component is storing the previous route in from when a redirect occurs due to isAuthenticated being false. This can be used in LoginForm to redirect the user when isAuthenticated is true. Just extract from from this.props.location.state and use that in combination with Redirect component from react-router-dom. If you log this.props.location.state.from you will see the property pathname containing the string route path that the user attempted to get to in an unauthenticated state, which can be used to redirect them once authentication is successful.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
class LoginForm extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
}
this.handleFormSubmit = this.handleFormSubmit.bind(this);
}
handleFormSubmit(event) {
event.preventDefault();
const validation = this.validator.validate(this.state);
this.setState({ validation });
this.submitted = true;
if (validation.isValid) {
// submit form here
this.props.loginUser({
email: this.state.email,
password: this.state.password
});
}
}
render() {
const { from } = this.props.location.state || { from: { pathname: "/" } };
const { isAuthenticated } = this.props;
if (isAuthenticated) {
return <Redirect to={from} />;
}
return (
{* login from code *}
);
}
}
function mapStateToProps(state) {
return {
isFetching: state.auth.isFetching,
loginError: state.auth.loginError,
isAuthenticated: state.auth.isAuthenticated
};
}
function mapDispatchToProps(dispatch, props, state) {
return {
loginUser: (credentials) => {
dispatch(loginUser(credentials));
},
};
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm));
You may need to update your PrivateRoute component is well to ensure it returns a Route, this would be in line with react-router-dom example:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Redirect, withRouter } from 'react-router-dom';
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route {...rest} render={props =>
rest.isAuthenticated ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: LOGIN_PATH,
state: { from: props.location }
}}
/>
)
}
/>
);
const mapStateToProps = ({ auth: { isAuthenticated } }) => ({ isAuthenticated });
export default withRouter(connect(mapStateToProps)(PrivateRoute));
I've created a simplified StackBlitz demonstrating the functionality in action.
Hopefully that helps!
#Alexander and #bos570 first of all I really appreciate the way you've done this. There are better approaches to this but here is an implementation to build it in controlled way:
In your index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import './styles.scss'; // it depends what styles you're using css,scss,jss
import App from './App';
ReactDOM.render(<App />, document.getElementById('app'));
In your App.js:
class App extends Component {
state = {
isAuthenticated: false,
};
componentDidMount(){
//call authentication API here
// setState isAuthentication to true upon authentication success
}
render(){
return(
<Provider store={store}>
<BrowserRouter>{isAuthenticated ? <Routes /> : <Login />}</BrowserRouter>
</Provider>
)}
Your Routes.js will have all routes in a Switch and It will keep showing loading screen unless API response gives success. and you re-route to a basic Login page.
Even If you've done code splitting It would not download any bundles from network except main.bundle.js upon authentication failure.
I see people when people use Redux like jack of all trades in programming. I know you future implementation for authentication will give you an refreshToken that's what Authentication API does for us. You can store it in localStorage and You need to do this if someone refresh the browser at any instant you can't keep it in redux as we know redux will lose state upon browser refresh so here localStorage comes into the play. Use it wisely I would really like to have conversion with both of you how can we make it more better.
I hope this would be helpful for you. and Maybe redux will be used side by side for Authentication. I have had a great time while implementing auth in an app.
Cheers
EDITED keep it as it you've done. But we need to know at which point, you need redux, localStorage, Context API

Resources