How to build a React app whit only a few public routes? - reactjs

I'm working on a React app where the user needs to be logged-in to do anything. This means that by default every route requires authentication, expect the few pages needed to create an account and so on.
Every article or tutorial I found on the subject (How to implement authenticated routes in React Router 4?) explains how to put all your private pages behind one route (usually "dashboard/"). But I don't want to artificially force my application to have this route structure. When I used to work with AngularJS, I would specify for each route if the user needs to be authenticated or not to access it.
So what's currently the best way to structure your router in react to specify that a few routes are publicly accessible, and the others require authentication and redirect you to the login page if you are not?

I agree that the solution is with a high order component, here is another example to avoid asking on each route and have a more general way to make private a page
You have a wrapper component: withAuthorization that wraps your component to check if you have access or no to that content.
This is just a quick example, I hope it can helps you
const withAuthorization = Component => {
return class WithAuthorization extends React.Component {
constructor(props){
super(props);
this.state = {
auth: false
}
}
async componentDidMount() {
// ask in your api for the authorization
// if user has authorization
this.setState({ auth: true })
}
render () {
const { auth } = this.state;
return (
{auth ? <Component {...this.props} /> : <Redirect to={`login`} />}
)
}
}
}
export default withAuthorization;
Then when you export your components just have to do it in this way:
withAuthorization(ComponentToExport)

Essentially you can create a Higher Order Component that use can use to check the auth and do what is necessary... I do something like this for my protected routes:
export const PrivateRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) =>
checkAuth(user) === true ? (
<Component {...props} />
) : (
<Redirect to="/auth/login" />
)
}
/>
);
};
There are several ways to pass in your user object...so I have not put that in there
then in my router I use it as follows:
<PrivateRoute
exact
path="/application/version"
component={AppVersion}
/>

Related

React keep a state in the highest component

I am trying to create a React RBAC system where my backend has a field called role: admin for example which tells the access the user has. After a successful sign in, I direct the user to a specific route (using Protected Route) but I want to check that if the user has the clearance level (if role is admin and not general). I thought that if I keep a state where I am routing which stores the role of the user, I can check if the user has the required access and send him accordingly but I am not sure whether this is a good approach and how to do it.
App.js - RequireAuth just checks if the user session exists or not (it then redirects it to login)
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Router>
<Switch>
<Route exact path = '/'
component = {LandingPage}
/>
<Route exact path = '/register'
component = {Register}
/>
<Route exact path = '/addBill'
component = {RequireAuth(AddBill)}
/>
<Route exact path = '/addItem'
component = {RequireAuth(AddItem)}
/>
<Route exact path = '/deleteItem'
component = {RequireAuth(DeleteItem)}
/>
</Switch>
</Router>
</div>
);
}
}
export default withRouter(App);
SignIn.js (I just route the user to the endpoint if it is a successful login or else display an error message)
if(status === 200) {
this.props.history.push('/addItem')
}
RequireAuth does not have access the role of the user but I wanted to implement RBAC on this.
Add your permission data eg : Roles to global App State/Store, you could do this easily with React context API.
//these will probably go in a file AppProvider.js
const AppContext = React.createContext({userRole:'general', setUserRole: ()=>{}});
const AppProvider = ({children}) => {
const [userRole,setUserRole] = React.useState('general');
return <AppContext.Provider value={{ userRole, setUserRole }}>{children}</AppContext.Provider>
}
//end AppProvider.js
const RequireAuth = (component) => {
const {userRole} = React.useContext(AppProvider);
const Component = () => {
//check your RBAC logic here now that you have access to userRole
}
return Component;
}

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.

Combine Redux (fetch userinfo) with React-Router (private routes + pass userinfo as props to routes)

I am struggling to determine the best way to handle authentication in my React / React-Router / Redux / MERN-stack application, and any help is appreciated. In particular, I am not weaving the various libraries together all that well, and the result is that I am handling checking-if-a-user-is-logged-in at the individual component levels, whereas I think this should be done in App.js.
My application uses passport.js for auth, and my mongodb has a users collection that keeps track of each user's info. A document in the users collection looks like this:
My app has a redux action userActions that uses the authorizedReducer to add to the reduxState the objects one or more of userInfo, authorized, loading. loading simply returns TRUE/FALSE depending on whether the action is complete, authorized returns a TRUE/FALSE boolean as to whether or not the logged in, if authorized == TRUE, then userInfo returns the user's info from its mongodb document (otherwise no userInfo is returned).
With all of that said, he's how my App.js and index.js files are currently structured:
App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Route, Switch } from 'react-router-dom';
import AppNavbar from './components//Navbars/AppNavbar';
import LoginModal from './components/LoginModal';
import SignupModal from './components/SignupModal';
import HomePage from './path-to-component';
import AboutUs from './path-to-component';
import HelpPage from './path-to-component';
import NoMatch from './path-to-nomatch';
... import of ~15 other routes ...
// Want to use this redux userAction to get info on the user (if he's logged in, other acct info, etc.)
import { userActions } from '../../../actions/auth/auth-actions.js';
class App extends Component {
constructor(props, context) {
super(props, context);
this.handleShowLogin = this.handleShowLogin.bind(this);
this.handleCloseLogin = this.handleCloseLogin.bind(this);
this.handleShowSignup = this.handleShowSignup.bind(this);
this.handleCloseSignup = this.handleCloseSignup.bind(this);
this.state = {
showLogin: false,
showSignup: false
};
}
// dont worry about these - just used for displaying login / signup modals
handleCloseLogin() { this.setState({ showLogin: false }); }
handleShowLogin() { this.setState({ showLogin: true }); }
handleCloseSignup() { this.setState({ showSignup: false }); }
handleShowSignup() { this.setState({ showSignup: true }); }
// want to be able to do 1-time grab of user actions, storing the user's info in reduxstate
componentDidMount() {
this.props.dispatch(userActions.authorize());
}
render() {
return (
<React.Fragment>
<AppNavbar login={this.handleShowLogin} signup={this.handleShowSignup} />
<LoginModal close={this.handleCloseLogin} showLogin={this.state.showLogin} signup={this.handleShowSignup} />
<SignupModal close={this.handleCloseSignup} showSignup={this.state.showSignup} />
<Switch>
<Route exact path='/' render={(props) => <HomePage {...props} />} />
<Route exact path='/about' render={(props) => <AboutUs {...props} />} />
<Route exact path='/site-guide' render={(props) => <HelpPage />} />
... ~15 more routes ...
<Route component={NoMatch} />
</Switch>
<AppFooter />
<TableHeaderTooltip/>
</React.Fragment>
);
}
}
// want to have this here to grab userInfo & authorized, but this doesnt work in App.js
function mapStateToProps(reduxState) {
return {
userInfo: reduxState.authorizedReducer.userInfo,
authorized: reduxState.authorizedReducer.authorized,
loading: reduxState.authorizedReducer.loading
};
}
export default connect(mapStateToProps)(App);
Index.js
import store from './store';
... other imports ...
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'));
In short, the above App.js does not work as intended. The userInfo and authorized do not work, and I wonder if redux data-fetching cannot be done in the App.js file.
In summary, being able to grab auth info (is a user logged in, and if so, whats the users info) in main App.js file would be great. I want to pass the authorizedReducer.authorized boolean, and the userInfo object, as props into routes. Instead, currently I am calling userActions() to get userInfo/auth info, but this is being called in each component that needs this info (eg. HomePage and About, but also many of my apps routes that are excluded above (...) need userInfo and authorized info).
Is this possible? Am I moving along the right track, or should I go in some other direction? Any help with this is super greatly appreciated!!
EDIT: if this helps - this is what my reduxState looks like w.r.t. the relevant reducers for auth. I have an authentication reducer that checks for login, and then the authorizedReducer reducer that also checks for login, and also has the userInfo.
EDIT2: to clarify, the authentication reducer populates with TRUE automatically when a user is logged in, which is great. However, the userInfo and authorized from authorizedReducer only populates when the userActions() action is dispatched.

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

React router 1.0 pass multiple props to children routes

I'm using react-router 1.0 and react-redux on an app, and I'm wondering what strategy is best to pass props to children on larger apps. Here's a basic situation:
Let's say I have a route /admin/users/edit/:id with the following structure on its components:
Routes:
<Router>
<Route path="admin" component={Admin}>
<Route path="users" component={Users}>
<Route path="edit/:id" component={Edit}/>
</Route>
</Route>
</Router>
Admin:
class Admin extends React.Component {
render() {
return (
{this.props.children}
)
}
}
Users:
class User extends React.Component {
edit = (id, params) => {
const { dispatch } this.props;
dispatch(edit(id, params));
}
other functions (add, remove) ...
render() {
return (
{this.props.children}
)
}
}
function mapStateToProps(state) {
const { users } = state;
return { users };
}
export default connect(mapStateToProps)(User);
Edit:
class Edit extends React.Component {
submit () => {
const { id } = this.props.params;
const { firstName } = this.refs;
this.props.edit(id, {firstName: firstName.value});
}
render() {
const { id } = this.props.params;
const user = this.props.users[id];
return (
<form>
<input ref='firstName' defaultValue={user.firstName}/>
<button onClick={this.submit}>Submit</button>
</form>
)
}
}
How would I pass the users & edit function props down to the children?
I know about React.cloneElement() (as in https://github.com/rackt/react-router/tree/master/examples/passing-props-to-children), but if I have multiple routes like /users/add, /users/remove/:id, etc, I would be passing and exposing all the functions (edit, add, remove...) to all children. That solution doesn't seem to work very well when you have more than one children.
I would like to keep my children as dumb as possible, and use this same structure across the application (/images/add, /images/remove/:id, etc).
Any suggestions?
You have a few options:
First level children:
Use React.cloneElement(), that's something you are already aware of. I wouldn't use it for deeply nested Components though.
To all routes:
Use createElement():
<Router createElement={createElement} />
// default behavior
function createElement(Component, props) {
// make sure you pass all the props in!
return <Component {...props}/>
}
// maybe you're using something like Relay
function createElement(Component, props) {
// make sure you pass all the props in!
return <RelayContainer Component={Component} routerProps={props}/>
}
Check more on that in the React Router docs.
Use context:
Note:
Context is an advanced and experimental feature. The API is likely to change in future releases.
See how to use it in the React docs.
There is also the whole thread about it in the react-router#1531.

Resources