React-Router v4 and Redux authentication - reactjs

I'm using react-router v4 and Redux for a new project.
I have the following code:
export class App extends Component {
componentDidMount() {
const { dispatch } = this.props;
dispatch(initAuth());
}
render() {
const { user } = this.props;
return (
<BrowserRouter>
<div>
<NavContainer />
<Match pattern="/login" component={LogInForm} />
<MatchWhenAuthorized pattern='/users' component={Users} user={user} />
</div>
</BrowserRouter>
);
}
}
Where initAuth dispatches an action that checks if there's an existing token in localStorage and if there is, a logIn action is dispatched as well.
The problem is that if I go directly to myapp.com/users the action hasn't returned yet, so there's no user logged in and in that case MatchWhenAuthorized redirects me to my LogInForm, which I don't want if my initAuth logs a user in.
Is there an elegant way to solve this?
I think I could solve it by rendering the MatchWhenAuthorized component only if there's a user logged in, but that doesn't seem right.

The initial login state should be set when the page is loaded and before you mount your app. I'm not sure why the initAuth is a redux action creator when you could just check the localStorage without involving redux.
index.js
import { createStore } from 'redux'
import reducer from './reducer'
import { getUser } from './storage'
// load the user data from localStorage and
// use the value in the store's initial data
const store = createStore(reducer, {
user: getUser()
})
Then you can connect your <MatchWhenAuthorized> component to the store to access the user value and redirect if there is no user in the store.

Related

How to async/await on firebase.auth().currentUser?

I am using ReactJs, and defined a Route which will load <Loans />component if the path is mywebsite.com/loans. Below is the code snippet for the <Loans />component. In the componentDidMount, I have async/await to get the currentUser from firebase. If user is null, page will be redirected to /signin page.
class Loans extends Component {
componentDidMount = async () => {
const user = await firebase.auth().currentUser;
if (!user) {
this.props.history.push("/signin");
}
};
render () {
...}
}
Here is the code snippet for <SignIn />component. In SignIn component, there is a listener to listen any auth state change, if user is logged in, page will be redirected to /loanspage.
class SignIn extends React.Component {
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
this.props.history.push("/loans");
}
});
}
render () {
...
}
}
I actually already logged in. But I observed a weird behavior that whenever I refreshed the page /loans, page will be redirected to /signin page for less than a second and then quickly redirected back to /loans page.
My question is if I already have firebase.auth().currentUser to be async/await, how could I still get null for the user in <Loans /> component, and I only see <Loans /> component when the page is redirected from <SignIn /> page? How can I aviod to see the SignIn page if I already have user logged in in my case. Thanks!
firebase.auth().currentUser isn't a promise, it's a User object, so using await on it doesn't make much sense. According to the API documentation, it's only going to be a User object, or null. It will be null when there is no user signed in, or the User object just isn't available yet.
What you should be doing instead is using the same style of listener in SignIn to determine when a User object is ready, and render any content only after that listener indicates a User is present.
I recently built a HOC to handle this. Quick example below
import React from "react"
import { useAuthState } from "react-firebase-hooks/auth"
import { auth } from "./firebase" // Where to store firebase logic
import { Navigate } from "react-router-dom"
export const RequireAuth = ({ children }: { children: JSX.Element }) => {
const [user, loading] = useAuthState(auth)
if (loading) {
return <></>
} else if (user?.uid && !loading) {
return children
} else {
return <Navigate to="/login" />
}
}
Then in the router (I'm using RR v6) you just wrap the page component in the hoc.
<Route
path="/"
element={
<RequireAuth>
<Dashboard />
</RequireAuth>
}
/>
You could also extract this out to a hook and call it in every page instead of at the router level but I feel like this is a bit more readable as far as seeing which routes are protected. This also follows the example of protected routes in the RR docs.

How to get stored data from store.getState()

I have using react with redux for the first time.
In my app's render, if I console log store.getState() I can see the stored data {username: 'foo', password: 'bar'}
but when I want to get the data like store.getState().password
I get undefined error. I am trying to pass this data to
my components for private route as:
inside PrivateRoute I then try to check if the user is logged in or not to send to dashboard
so, how do I get data to pass to my props?
even const { isLoggedIn, username } = store.getState() doesn't work, it shows
Property isLoggedIn does not exist on type {}
btw I know this might be bad but it's my first react app so I am trying to learn redux
if you are calling the store from the react application you have to use provider and pass the store to the react app, and then bind state, actions and/or methods of the store to the react props as shown in this link
connect https://react-redux.js.org/5.x/api/connect
but if you are using redux in normal javascript then it will work fine.
example in react
first
import { Provider } from 'react-redux'
import { store } from "store";
ReactDOM.render(
<Provider store={store}>
<YourReactApp/> // e.g <Container />
</Provider>, document.getElementById('root')
);
then in you can bind anything from your store to react component like this
import { connect } from "react-redux";
const mapStateToProps = (state) => {
const { isLoggedIn, username }= state
return {
isLoggedIn,
username
};
};
const mapDispatchToProps = (dispatch :any) => {
return {
login: ()=> {
return dispatch(your action creator)
}
}
}
const Containter = connect(mapStateToProps,mapDispatchToProps)(AnyReactComponentYouWantToPassThisStore);
export default Containter;
the you can use it in your page like this
function AnyReactComponentYouWantToPassThisStore (props){
return(
<div> {props.username} </div>
)
}
then instead of calling <AnyReactComponentYouWantToPassThisStore />
now use <Container />

How to pass state to React JS High Order Component

I am using OIDC redux connector for user state. I have a few components that require authentication. I would like to use something like export default connect(mapStateToProps, mapDispatchToProps)(withAuth(Component)); and request data from state inside my authentication service.
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { push } from 'connected-react-router'
export const withAuth = (Component) => {
return props => {
return <Component {...props} />
}
}
Is it possible to get state in the render function? So I can check the user beinig logged in and redirect to the sign-in page if there is no user signed in?
BTW: How would I redirect? I have tried using redirect-router-dom and <Redirect /> But then it complains about set state being changed too often ... But that might be my mistake. I get this error when I render a Redirect: Error: Maximum update depth exceeded.
If I understand correctly you want to "decorate" your component with additional logic to handle an authentication redirect?
I suggest using a "decorator" pattern here e.g.:
export const withAuth = (Component) => {
const mapStateToProps = (state, ownProps) => ({
authenticated: state.authenticated // Or what you need to do to determine this
});
return connect(mapStateToProps)(class extends React.Component {
render() {
const { authenticated, ...componentProps } = props;
if (authenticated) {
return <Component {...componentProps }>;
}
return <Redirect to="/login" />;
}
});
}
Then when you need this you can do things like:
export default withAuth(connect(yourOwnMapStateToProps)(YourComponent))
Just figured it out, I changed the store so instead of returning a function, it returns the object. So I can load in all js files. It might not be the best solution. If there is a better way to get the store in code, I would love to hear about how to do that. The configurestore function is what I found in quite a lot of examples.
import { store } from '../configureStore';
Using store.getState() I can get the current state.
The redirect issue I am having is similar to: How to use react-transition-group with react-router-dom

Login in React.js with localStorage token

Im developing a Login system of a React WebSite. We're using api_tokens for access to the API and retrieve the info as validate user credentials. I need to restringe everything when the auth fails and redirect to the user login page, currently Im using redux as app global state. The issue is I'm saving the api_token in browser localStorage, and I need to dispatch the UNAUTH_USER when the api_token is modified by Browser Clear History or other things........
I was wondering if I can attach some eventListener to that... and if is the right solution..
The code looks below:
import React from 'react'
import Header from './Header'
import Navigation from '../navigation/components/Navigation'
import Ribbon from '../ribbon/Ribbon'
import Footer from './Footer'
import Shortcut from '../navigation/components/Shortcut'
import LayoutSwitcher from '../layout/components/LayoutSwitcher'
import {connect} from 'react-redux'
// require('../../smartadmin/components/less/components.less');
class Layout extends React.Component {
constructor(props) {
super(props);
}
componentWillMount() {
if(!this.props.authenticated) {
this.props.router.push('/login');
}
}
componentWillUpdate(nextProps) {
if(!nextProps.authenticated) {
this.props.router.push('/login');
}
}
render() {
if (!this.props.authenticated) {
this.props.router.push('/login');
return null;
}
else {
return (
<div>
<Header />
<Navigation />
<div id="main" role="main">
<LayoutSwitcher />
<Ribbon />
{this.props.children}
</div>
<Footer />
<Shortcut />
</div>
)
}
}
}
const mapStateToProps = (state) => {
return {
authenticated: state.auth.authenticated
};
}
export default connect(mapStateToProps)(Layout);
I do the same thing in my app. For every call my Sagas make, if an api error occurs and a 403 expired/unvalid token is raised, I dispatch there a redux action that clears localstorage and disconnect the user (I have a refresh token mechanism retry before that).
I'm not sure that you can attach an eventListener to a localStorage expiration, but placing a check at your API calls mechanism would be IMO a good practice.
Best

Prevent routing in React when user manually changes url in browser tab

I am stuck in a issue that happens when user manually changes the route in browser tab and presses enter. This forces my react router to navigate to the state entered by user. I want to prevent this and allow routing only through the flow I have implemented by button clicks in my website.
Some of my screens need data that will be available only if the user navigates the site using the flow expected. If user directly tries to navigate to a particular route by manually changing the route in url then he may skip the desired flow and hence the app will break.
Other scenario, in case I want to restrict some users from accessing some routes but the user knows the path and manually enters that in browser url then he will be presented with that screen but should not be.
What I do is use a prop from previous page, if that prop is undefined(meaning user did not follow due process :) hehe ) I simply send the user back to the landing page or wherever.
You can create a route guard using HOC. For example, you don't want unauthorized user to pass route /profile, then you can do the following:
// requireAuthorized.js (HOC)
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {Redirect} from 'react-router-dom'
const connector = connect(
state => ({
isAuthorized: state.profile !== null // say, you keep user profile in redux
})
)
export default (WrappedComponent) => {
return (
connector(
class extends Component {
static propTypes = {
isAuthorized: PropTypes.bool.isRequired
}
render () {
const {isAuthorized, ...clearedProps} = this.props
if (isAuthorized) {
return <WrappedComponent {...clearedProps} />
} else {
return <Redirect to={{pathname: '/login'}} />
}
}
}
)
)
}
// ProfilePage.jsx
import React from 'react'
...
import requireAdmin from '../hocs/requireAdmin' // adjust path
class ProfilePage extends React.Component {
...
render () {
return (
<div>
...
</div>
)
}
}
export default requireAdmin(ProfilePage)
Pay attention to the export statement in my ProfilePage.js
I'd suggest using this library for cleanest solution (or at least make personal similar implementation of it).
Then you'd create authentication check HOC:
export const withAuth = connectedReduxRedirect({
redirectPath: '/login',
authenticatedSelector: state => state.user.isAuthenticated, // or whatever you use
authenticatingSelector: state => state.user.loading,
wrapperDisplayName: 'UserIsAuthenticated'
});
And you could easily create flow HOC:
export const withFlow = (step) = connectedReduxRedirect({
redirectPath: '/initial-flow-step',
authenticatedSelector: state => state.flow[step] === true,
wrapperDisplayName: 'FlowComponent'
});
Then initialize your component
const AuthenticatedComponent = withAuth(Dashboard)
const SecondStepComponent = withFlow("first-step-finished")(SecondStep)
const ThirdStepComponent = withFlow("second-step-finished")(ThirdStep)
You can easily create authenticated flow step by composing HOC:
const AuthSecondStepComponent = withAuth(withFlow("first-step-finished")(SecondStep))
Only thing that is important is that you update your redux state correctly as going through your step flow. When user finishes first step you'd set
state.flow["first-step-finished"] = true // or however you manage your state
so that when user navigates manually to specific page, he wouldn't have that redux state because its an in-memory state and would be redirected to redirectPath route.
Something like this is suitable. You make HOC Route with a wrap to function that deals with authentication/context props.
Note: this deals with direct access to the route, not to the menu items and such. That must be treated in a simmilar way on the menu / menuItem components.
import requireAuth from "../components/login/requireAuth";
class Routes extends React.Component<RoutesProps, {}> {
render() {
return (
<div>
<Switch>
<Route exact={true} path="/" component={requireAuth(Persons, ["UC52_003"])} />
<Route path="/jobs" component={requireAuth(Jobs, ["UC52_006"])} />
</Switch>
</div>
)
}
}
export default function (ComposedComponent, privileges) {
interface AuthenticateProps {
isAuthenticated: boolean
userPrivileges: string[]
}
class Authenticate extends React.Component<AuthenticateProps, {}> {
constructor(props: AuthenticateProps) {
super(props)
}
render() {
return (
isAuthorized(this.props.isAuthenticated, privileges, this.props.userPrivileges) &&
<ComposedComponent {...this.props} /> || <div>User is not authorised to access this page.</div>
);
}
}
function mapStateToProps(state) {
return {
isAuthenticated: state.userContext ? state.userContext.isAuthenticated : false,
userPrivileges: state.userContext ? state.userContext.user ? state.userContext.user.rights : [] : []
};
}
return connect(mapStateToProps, null)(Authenticate);
}
you can put the condition in useEffect of the given page/screen and push it back if it doesnt have the required values.. example below

Resources