How to control routing for protected pages in React app? - reactjs

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.

Related

Struggling for passing React routing param

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

Private Routing authentication with Reactn and Typescript

I am stuck in a part implementing private routing
So here is what I want to ultimately achieve:
I would like to sign in with Google, and once I am signed in I would like to go to a private route page, if I am signed out, I should not be able to do that.
For the code part:
a class to track login state (true/false):
class Auth {
isAuthenticated: boolean
constructor(){
this.isAuthenticated = false;
}
login(){
this.isAuthenticated= true;
}
logout(){
this.isAuthenticated = false;
}
isLoggedIn(){
return this.isAuthenticated;
}
}
export default new Auth();
A summarised snippet for the google login:
import React, { Component } from 'react';
import { GoogleLogin, GoogleLogout} from 'react-google-login';
import auth from './auth';
login (response: any) {
if(response.accessToken){
this.setState(state => ({
isLogined: true,
userName: response.profileObj.givenName + ' ' + response.profileObj.familyName
}));
this.refreshTokenSetup(response);
auth.login();
}
}
render() { //this is what is rendered (what users see) in the react web app pahge
return (
<div>
<GoogleLogin
clientId={ CLIENT_ID }
buttonText='Login'
onSuccess={ this.login}
isSignedIn={true}
onFailure={ this.handleLoginFailure }
cookiePolicy={ 'single_host_origin' }
responseType='code,token'
/>
}
and finally, my protected route:
import * as React from 'react';
import {Route, Router, Redirect, RouteProps} from 'react-router-dom';
import auth from './auth';
const ProtectedRoute: React.FC<RouteProps> = ({component: Component, ...rest}) => {
if (!Component) return null;
return (
<div>
<Route {...rest} render={props => (
auth.isAuthenticated ?
<Component {...props} />:
<Redirect to="/" />
)} />
</div>
);
};
export default ProtectedRoute;
According to a tutorial I followed, if they set this.isAuthenticated=true in ClassX, the value is still true when they import auth and access it in ClassA, I don't know how since I assume new instances are created every time they import?
My question is: is it possible to edit the value to "true" in one class, and access it from another, if so how?
If not, any suggestions on what can I do to fix my problem?

Custom route to handle authorized users?

I would like to create a custom express route that would allow to determine if a user has the right to access a page or not.
So far I got it working by using something like that :
import React from 'react';
import { Route } from 'react-router-dom';
import { AuthenticationContext } from '../../contexts/authentication/context';
import Unauthorized from '../Unauthorized';
import axios from 'axios';
const ProtectedRoute = ({ component: Component, redirect: Redirect, contextProvider: ContextProvider, path, ...routeProps }) => {
const { authenticationState: {isAuthenticated, isFetchingTheUser, currentUser} } = React.useContext(AuthenticationContext)
const [authorized, setAuthorized] = React.useState([]);
const [isFetchingAuthorizations, setIsFetchingAuthorizations] = React.useState(false);
React.useEffect(() => {
setIsFetchingAuthorizations(true);
axios.get(`${global.REST_API_ADDR}/api/pages/${encodeURIComponent(path)}`)
.then((response) => {
setAuthorized(response.data.authorized);
setIsFetchingAuthorizations(false);
})
.catch((error) => {
setIsFetchingAuthorizations(false);
// console.log("Protected route use Effect error : ", error);
})
}, [path])
return (
<Route {...routeProps}
render={ props => {
if(isFetchingTheUser || isFetchingAuthorizations) return <div>Chargement...</div>
if(isAuthenticated && authorized.includes(currentUser.rank)){
return ContextProvider ? <ContextProvider><Component {...props} /></ContextProvider> : <Component {...props} />
}else if(isAuthenticated && !authorized.includes(currentUser.rank)) {
return <Unauthorized {...props} />;
}
else{
return <Redirect {...props}/>;
}
}}/>
);
};
export default ProtectedRoute;
The problem here is that if the user changes routes a second time, the component will keep the previous authorized array for a few milliseconds, render the component and if the component has any kind of
useEffect with some API calls it will throw an error in the console. I would like to know if there is any way to prevent this ?
Like maybe emptying the state after the route renders the component ?
Thanks in advance,
EDIT 1 : from the user's point of view there are no visual bugs or latency but as a developer I have a hard time leaving an error message in the console if there is any way to avoid it

Flow React: Cannot create element because React.Component [1] is not a React component

I just started to make use of flow a few weeks ago and from a week ago i've been getting a flow error on which i've no idea how to fix.
The code goes like this:
// #flow
import React, { Component } from "react";
import { Redirect, Route } from "react-router-dom";
import CookieStorage from "./../services/CookieStorage";
import type { Component as ComponentType } from "react";
type Props = {
component: ComponentType<any, any>
}
class ProtectedRoute extends Component<Props> {
render() {
const isAuthenticated = this.isAuthenticated();
const {...props} = this.props;
const AuthorizedComponent = this.props.component;
return (
<Route
{...props}
render={props => (
isAuthenticated ?
<AuthorizedComponent {...props} /> :
<Redirect to="/"/>
)}
/>
);
}
isAuthenticated(): boolean {
const data = CookieStorage.get("foobar");
return data !== null;
}
}
export default ProtectedRoute;
In here flow throws this error:
Error:(23, 8) Cannot create `AuthorizedComponent` element because `React.Component` [1] is not a React component.
I don't know if i am doing a wrong import type or a wrong type declaration for the component that is to be rendered when the authentication example is ok.
I've copied this code from a website i don't remember where, but he was making use of this snippet using const {component: Component} = this.props and render it as <Component {...props} /> which for me it seems a little ambiguous, which is why i changed the declaration a bit to make it easy to understand when reading, but still even doing the exact same code like the snipped where i copied this code, flow still throws that error.
I've made a gist of this in case someone would knows a solution for this and would like to make a change, if no one is able to help me fix this in here, then i will send a ticket issue to their project using this gist
Try to use React.ComponentType instead?
import type { ComponentType } from "react";
import React, { Component } from "react";
import { Redirect, Route } from "react-router-dom";
import CookieStorage from "./../services/CookieStorage";
type Props = {
component: ComponentType<any>
}
class ProtectedRoute extends Component<Props> {
render() {
const isAuthenticated = this.isAuthenticated();
const { component: AuthorizedComponent, ...props } = this.props;
return (
<Route
{...props}
render={props => (
isAuthenticated ?
<AuthorizedComponent {...props} /> :
<Redirect to="/"/>
)}
/>
);
}
isAuthenticated(): boolean {
const data = CookieStorage.get("foobar");
return data !== null;
}
}
export default ProtectedRoute;
See https://flow.org/en/docs/react/types/#toc-react-componenttype

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