Redirect router on after login - reactjs

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

Related

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?

How can i check if user is authenticated more efficently in react?

I have a this.props.auth reducer in my protected route (returns user if authenticated or false when not). Currently I'm checking that if this.props.auth is false or has a user inside of it. When false, then redirect to /login or if true, render the page with this.props.auth data. I currently have to rewrite the auth logic for all components same way. What would be the best way to make the is user authenticated check reusable?
Protected Component Code:
class StaffMeetingsPage extends React.Component {
componentDidMount() {
document.title = "Staff Meetings";
}
renderContent() {
console.log(this.props.auth)
switch (this.props.auth) {
case null:
return 'still fetching';
case false:
return this.props.history.push('/login');;
default:
return <div>Logged in</div>;
}
}
render() {
console.log(this.props);
return(
<div>
{this.renderContent()}
</div>
);
}
}
function mapStateToProps(state) {
return {
auth: state.authenticationProperty
};
}
export default connect(mapStateToProps)(StaffMeetingsPage);
Console logging this.props.auth when logged in:
email: "test#mailtest.com"
username: "user"
_id: "adasdasdasfe3453"
__proto__: Object
Console logging this.props.auth when not logged in:
false
I usually create a PrivateRoute component that checks if the user is logged in (via prop, redux, localstorage or something).
Something like:
import { Route, Redirect } from 'react-router-dom'
const PrivateRoute = ({ isLoggedIn, ...props }) =>
isLoggedIn
? <Route { ...props } />
: <Redirect to="/login" />
In your case you could connect this PrivateRoute component to the part of your state where you handle the authentication:
function mapStateToProps(state) {
return {
isLoggedIn: !!state.authenticationProperty
};
}
export default connect(mapStateToProps)(PrivateRoute)
In the router I then use it for my, well, private routes :)
<Switch>
<PrivateRoute path="/staff-meetings" component={StaffMeetingsPage} />
<Route path="/login" component={Login}/>
</Switch>

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 React Router from Context Provider

I'm new to React Router and trying to do a redirect from inside a provider using the new Conext API. basically my provider looks like this.
/* AuthContext.js */
class AuthProvider extends React.Component {
state = { isLoggedIn: false }
constructor() {
super()
this.login = this.login.bind(this)
this.logout = this.logout.bind(this)
}
login() {
this.setState({ isLoggedIn: true })
// Need to redirect to Dashboard Here
}
logout() {
this.setState({ isLoggedIn: false })
}
render() {
return (
<AuthContext.Provider
value={{
isLoggedIn: this.state.isLoggedIn,
login: this.login,
logout: this.logout
}}
>
{this.props.children}
</AuthContext.Provider>
)
}
}
const AuthConsumer = AuthContext.Consumer
export { AuthProvider, AuthConsumer }
I've read a lot about how to pass the history object using props and how to use a component but I can't see how these approaches would work here. My context provider sits at the top of the tree so it's not a child of the Router so I can't pass props. It's also not a standard component so I can't just insert a component, unless I've misunderstood something (which is very possible).
Edit: Looks like the way to go is withRouter, but how to export my AuthProvider in the code above so that history.push is available in my login function? As you can see I'm exporting multiple components wrapped in {} so can you wrap one of these in a HOC and do you have to explicitly pass history in or is it always available inside the component that's being wrapped?
use withRouter, sth like this to get access of history.
const AuthButton = withRouter( ({ history }) =>history.push("/"));
Try This:
import { Route } from "react-router-dom";
class AuthProvider extends React.Component {
yourFunction = () => {
doSomeAsyncAction(() =>
this.props.history.push('/dashboard')
)
}
render() {
return (
<div>
<Form onSubmit={ this.yourFunction } />
</div>
)
}
}
export default withRouter(AuthProvider);
Best explanation can be found here: Programmatically navigate using react router

Redux/Firebase - Component not updating after uid is received in state

I'm using Firebase's authentication API for my Redux app. I have an app component that is supposed to be informed when a user's UID authentication is received and toggle which header is shown to the user (either logged in header or logged out header). However, in instances where the user is already logged in and revisiting the main route, I can't seem to get my main app to re-render when the UID is stored to the app state.
An outline of the flow is such:
In routes/index.js: Firebase's onAuthStateChanged observer
identifies if the user is already logged in or not when the main
route loads. If there's a user, dispatch actions to copy their UID
from Firebase to our state and send them to the "connect" page.
In actions.jsx: The startLoginForAuthorizedUser action creator
dispatches an action to update the auth reducer with the new UID &
reroute the user to "connect".
In reducers.jsx: The "auth" state is updated to include the user's
UID to allow components to toggle elements depending on
authentication status.
In App.jsx: For whatever reason, mapStateToProps is not receiving an
updated state, even though the user is authenticated and Redux dev
tools shows the state as updated with the new UID.
The end result is that authenticated users see the "connect" page as expected, but still see a logged-out header. Here's the code:
App.jsx
import React from 'react';
import { connect } from 'react-redux';
import * as actions from 'actions';
import HeaderLoggedOut from './HeaderLoggedOut';
import ModalOverlay from './ModalOverlay';
import MenuWrapper from './MenuWrapper';
import Header from './Header';
import Tabs from './Tabs';
export const App = React.createClass({
render(){
const { uid } = this.props;
console.log("App.jsx: uid:", uid);
if(!uid){
return(
<div>
<HeaderLoggedOut />
<div className="tonal-main">
<div className="tonal-content">
{ this.props.children }
</div>
</div>
<ModalOverlay />
</div>
);
}
return(
<MenuWrapper>
<Header />
<div className="tonal-content">
{ this.props.children }
</div>
<Tabs />
</MenuWrapper>
);
}
});
const mapStateToProps = (state) => {
// state is not updated
return {
uid: state.auth.uid
};
};
export default connect(mapStateToProps)(App);
router/index.js
import App from 'App.jsx';
import Connect from 'connect/Connect.jsx';
import firebase from 'app/firebase';
const store = require('store').configure();
firebase.auth().onAuthStateChanged((user) => {
if(user.emailVerified && user.uid){
store.dispatch(actions.startLoginForAuthorizedUser(user.uid));
} else {
browserHistory.push('/');
}
});
const requireLogin = (nextState, replace, next) => {
const currentUser = firebase.auth().currentUser;
if(!currentUser){
replace('/');
}
next();
};
const redirectIfLoggedIn = (nextState, replace, next) => {
const currentUser = firebase.auth.currentUser;
if(currentUser){
replace('/connect');
}
next();
};
export default (
<Router history={ browserHistory }>
<Route path="/" component={ App }>
<IndexRoute component={ Landing } onEnter={ redirectIfLoggedIn } />
<Route path="connect" component = { Connect } onEnter = { requireLogin } />
</Route>
</Router>
);
Actions.jsx
// ...actions, imports, etc...
export var startLoginForAuthorizedUser = (uid) => {
return (dispatch) => {
dispatch(login(uid));
dispatch(pushToRoute('/connect'));
};
};
export var login = (uid) => {
console.log('actions: logging in user with uid ', uid);
return{
type: 'LOGIN',
uid
};
};
export var pushToRoute = (route) => {
browserHistory.push(route);
};
Reducer.jsx
const authInitialState = {
uid: ""
};
export const authReducer = (state = authInitialState, action) => {
switch(action.type){
case 'LOGIN':
return {
...state,
uid: action.uid
};
case 'LOGOUT':
return {
...state,
uid: ""
};
default:
return state;
}
};
Any help from you experts would be greatly appreciated. This is driving me nuts.
I figured it out after an embarrassing amount of trial and error. For anyone else in the future who wants to use both react-router/react-router-dom and firebase's onAuthStateChanged() observer, just know you have to keep the observer code at your topmost component (where you render your provider in ReactDOM.render()).
If you keep it anywhere else, it doesn't appear to work properly and I frankly have no idea why. If someone more familiar with firebase wanted to explain what was happening there and why it would work in the top-level component but not in the routes index file, I'm sure it would help someone down the road.

Resources