I created ACL based routing application, Which means some user have authorisation to view pages and some other don't have. In run-time Authorised user can change privileged to unauthorised user to access pages.
Example : Initially Checker don't have permission to access page, but when admin allows the page access in run-time, checker can access that page. I will change navbar access according to the user-permission.
Is that valid to check privilege within the constructor and push to desired page.
class SomeComponent extends React.Component{
constructor(props){
super(props)
this.state = {
...some values
}
const user_data = localStorage.getItem('userData') // user data getting while login
if(user_data.isAdmin === false || user_data.checker === false){
this.props.history.push('/noauth');
}
}
}
Is that valid to push to another component without anything to render. If it is invalid, this will leads to performance or unpredictable issue ?
Ideally this check needs to be done at the level of Router. Something like this
const isAuthenticated = () => {
const user_data = localStorage.getItem('userData');
if (user_data.isAdmin === false || user_data.checker === false) {
return false;
}
return true;
};
const AuthenticatedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={(props) => {
return isAuthenticated() ? <Component {...props} /> : <Redirect to="/noauth" />;
}}
/>
);
};
Here isAuthenticated function returns where the User is authenticated or not. You can check the localStorage value in isAuthenticated and return.
I do RBAC and authorization on the backend part of the application (NodeJS) and never really bothered about enforcing authorization on the UI as well.
However, let's say I have the following React Router 4 routes:
<Switch>
<Route exact path="/books/all" component={BookList} />
<Route exact path="/books/new" component={BookNew} />
<Route exact path="/books/:bookId" component={BookDetails} />
<Route exact path="/books/:bookId/edit" component={BookEdit} />
</Switch>
And I want to make sure that if a logged in user visits a book that is not his, he is not able to render the route /books/<not my book>/edit. I am able to do this by implementing a simple check at the ComponentDidMount() function:
checkAuthorisation = userId => {
if (this.props.authenticated._id !== userId) {
this.props.history.push("/books/all");
}
};
But I was wondering whether there is a better approach / design pattern of doing it in ReactJS? I was wondering whether removing the bookId altogether from the route and just push props like edit and bookId:
<Route exact path="/books/edit" component={BookEdit} />
I would recommend to do a conditionnal render in your BookEdit component (especially if you need to do an async operation to determine authorization).
I would not use a private route here in order to keep the role/auth based routing simple.
In your edit component : check authorization, if false handle this as an error and render an error component (error message and back button, can also be your 404 view), else render your edit component.
To be consistent, you must also make sure you do not have links to this error (conditionnal disabled "Edit" button on the book view if not authorized).
Example (using async check, may not be your case here but this is a general idea) :
class EditComponent extends React.Component {
state = {
loading: true,
error: null,
bookProps: null,
};
componentDidMount() {
const { match, userId } = this.props;
getBook(match.params.bookId) // request book props
.then(book => {
if (book.ownerId !== userId) { // authorization check
this.setState({ error: 'Unauthorized', loading: false });
} else {
this.setState({ bookProps: book, loading: false });
}
})
.catch(err => {
this.setState({ error: err.message, loading: false });
});
}
render() {
const { loading, error, bookProps } = this.state;
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorComponent message={error} />;
}
return <BookEditComponent book={bookProps} />;
}
}
I am using React Router 4 for routing and Apollo Client for data fetching & caching. I need to implement a PrivateRoute and redirection solution based on the following criteria:
The pages a user is permitted to see are based on their user status, which can be fetched from the server, or read from the cache. The user status is essentially a set of flags we use to understand where the user is in our funnel. Example flags: isLoggedIn, isOnboarded, isWaitlisted etc.
No page should even begin to render if the user's status does not permit them to be on that page. For example, if you aren't isWaitlisted, you are not supposed to see the waitlist page. When users accidentally find themselves on these pages, they should be redirected to a page that is suitable for their status.
The redirection should also be dynamic. For example, say you try to view your user profile before you are isLoggedIn. Then we need to redirect you to the login page. However, if you are isLoggedIn but not isOnboarded, we still don't want you to see your profile. So we want to redirect you to the onboarding page.
All of this needs to happen on the route level. The pages themselves should be kept unaware of these permissions & redirections.
In conclusion, we need a library that given the user status data, can
compute whether a user can be on a certain page
compute where they need to be redirected to dynamically
do these before rendering any page
do these on the route level
I'm already working on a general-use library, but it has its shortcomings right now. I'm seeking opinions on how one should approach this problem, and whether there are established patterns to achieve this goal.
Here is my current approach. This is not working because the data the getRedirectPath needs is in the OnboardingPage component.
Also, I can't wrap the PrivateRoute with the HOC that could inject the props required to compute the redirect path because that would not let me use it as a child of the Switch React Router component as it stops being a Route.
<PrivateRoute
exact
path="/onboarding"
isRender={(props) => {
return props.userStatus.isLoggedIn && props.userStatus.isWaitlistApproved;
}}
getRedirectPath={(props) => {
if (!props.userStatus.isLoggedIn) return '/login';
if (!props.userStatus.isWaitlistApproved) return '/waitlist';
}}
component={OnboardingPage}
/>
General Approach
I would create an HOC to handle this logic for all of your pages.
// privateRoute is a function...
const privateRoute = ({
// ...that takes optional boolean parameters...
requireLoggedIn = false,
requireOnboarded = false,
requireWaitlisted = false
// ...and returns a function that takes a component...
} = {}) => WrappedComponent => {
class Private extends Component {
componentDidMount() {
// redirect logic
}
render() {
if (
(requireLoggedIn && /* user isn't logged in */) ||
(requireOnboarded && /* user isn't onboarded */) ||
(requireWaitlisted && /* user isn't waitlisted */)
) {
return null
}
return (
<WrappedComponent {...this.props} />
)
}
}
Private.displayName = `Private(${
WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'
})`
hoistNonReactStatics(Private, WrappedComponent)
// ...and returns a new component wrapping the parameter component
return Private
}
export default privateRoute
Then you only need to change the way you export your routes:
export default privateRoute({ requireLoggedIn: true })(MyRoute);
and you can use that route the same way you do today in react-router:
<Route path="/" component={MyPrivateRoute} />
Redirect Logic
How you set this part up depends on a couple factors:
How you determine whether a user is logged in, onboarded, waitlisted, etc.
Which component you want to be responsible for where to redirect to.
Handling user status
Since you're using Apollo, you'll probably just want to use graphql to grab that data in your HOC:
return graphql(gql`
query ...
`)(Private)
Then you can modify the Private component to grab those props:
class Private extends Component {
componentDidMount() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
}
} = this.props
if (requireLoggedIn && !isLoggedIn) {
// redirect somewhere
} else if (requireOnboarded && !isOnboarded) {
// redirect somewhere else
} else if (requireWaitlisted && !isWaitlisted) {
// redirect to yet another location
}
}
render() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
...passThroughProps
} = this.props
if (
(requireLoggedIn && !isLoggedIn) ||
(requireOnboarded && !isOnboarded) ||
(requireWaitlisted && !isWaitlisted)
) {
return null
}
return (
<WrappedComponent {...passThroughProps} />
)
}
}
Where to redirect
There are a few different places you can handle this.
Easy way: routes are static
If a user is not logged in, you always want to route to /login?return=${currentRoute}.
In this case, you can just hard code those routes in your componentDidMount. Done.
The component is responsible
If you want your MyRoute component to determine the path, you can just add some extra parameters to your privateRoute function, then pass them in when you export MyRoute.
const privateRoute = ({
requireLoggedIn = false,
pathIfNotLoggedIn = '/a/sensible/default',
// ...
}) // ...
Then, if you want to override the default path, you change your export to:
export default privateRoute({
requireLoggedIn: true,
pathIfNotLoggedIn: '/a/specific/page'
})(MyRoute)
The route is responsible
If you want to be able to pass in the path from the routing, you'll want to receive props for these in Private
class Private extends Component {
componentDidMount() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted
} = this.props
if (requireLoggedIn && !isLoggedIn) {
// redirect to `pathIfNotLoggedIn`
} else if (requireOnboarded && !isOnboarded) {
// redirect to `pathIfNotOnboarded`
} else if (requireWaitlisted && !isWaitlisted) {
// redirect to `pathIfNotWaitlisted`
}
}
render() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
// we don't care about these for rendering, but we don't want to pass them to WrappedComponent
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted,
...passThroughProps
} = this.props
if (
(requireLoggedIn && !isLoggedIn) ||
(requireOnboarded && !isOnboarded) ||
(requireWaitlisted && !isWaitlisted)
) {
return null
}
return (
<WrappedComponent {...passThroughProps} />
)
}
}
Private.propTypes = {
pathIfNotLoggedIn: PropTypes.string
}
Private.defaultProps = {
pathIfNotLoggedIn: '/a/sensible/default'
}
Then your route can be rewritten to:
<Route path="/" render={props => <MyPrivateComponent {...props} pathIfNotLoggedIn="/a/specific/path" />} />
Combine options 2 & 3
(This is the approach that I like to use)
You can also let the component and the route choose who is responsible. You just need to add the privateRoute params for paths like we did for letting the component decide. Then use those values as your defaultProps as we did when the route was responsible.
This gives you the flexibility of deciding as you go. Just note that passing routes as props will take precedence over passing from the component into the HOC.
All together now
Here's a snippet combining all the concepts from above for a final take on the HOC:
const privateRoute = ({
requireLoggedIn = false,
requireOnboarded = false,
requireWaitlisted = false,
pathIfNotLoggedIn = '/login',
pathIfNotOnboarded = '/onboarding',
pathIfNotWaitlisted = '/waitlist'
} = {}) => WrappedComponent => {
class Private extends Component {
componentDidMount() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted
} = this.props
if (requireLoggedIn && !isLoggedIn) {
// redirect to `pathIfNotLoggedIn`
} else if (requireOnboarded && !isOnboarded) {
// redirect to `pathIfNotOnboarded`
} else if (requireWaitlisted && !isWaitlisted) {
// redirect to `pathIfNotWaitlisted`
}
}
render() {
const {
userStatus: {
isLoggedIn,
isOnboarded,
isWaitlisted
},
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted,
...passThroughProps
} = this.props
if (
(requireLoggedIn && !isLoggedIn) ||
(requireOnboarded && !isOnboarded) ||
(requireWaitlisted && !isWaitlisted)
) {
return null
}
return (
<WrappedComponent {...passThroughProps} />
)
}
}
Private.propTypes = {
pathIfNotLoggedIn: PropTypes.string,
pathIfNotOnboarded: PropTypes.string,
pathIfNotWaitlisted: PropTypes.string
}
Private.defaultProps = {
pathIfNotLoggedIn,
pathIfNotOnboarded,
pathIfNotWaitlisted
}
Private.displayName = `Private(${
WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'
})`
hoistNonReactStatics(Private, WrappedComponent)
return graphql(gql`
query ...
`)(Private)
}
export default privateRoute
I'm using hoist-non-react-statics as suggested in the official documentation.
I personnaly use to build my private routes like this :
const renderMergedProps = (component, ...rest) => {
const finalProps = Object.assign({}, ...rest);
return React.createElement(component, finalProps);
};
const PrivateRoute = ({
component, redirectTo, path, ...rest
}) => (
<Route
{...rest}
render={routeProps =>
(loggedIn() ? (
renderMergedProps(component, routeProps, rest)
) : (
<Redirect to={redirectTo} from={path} />
))
}
/>
);
In this case, loggedIn() is a simple function that return true if user is logged (depends on how you handle the user session), you can create each of your private route like this.
Then you can use it in a Switch :
<Switch>
<Route path="/login" name="Login" component={Login} />
<PrivateRoute
path="/"
name="Home"
component={App}
redirectTo="/login"
/>
</Switch>
All subRoutes from this PrivateRoute will first need to check if user is logged in.
Last step is to nest your routes according to their required status.
I think you need to move your logic down a bit. Something like:
<Route path="/onboarding" render={renderProps=>
<CheckAuthorization authorized={OnBoardingPage} renderProps={renderProps} />
}/>
You will have to use ApolloClient without 'react-graphql' HOC.
1. Get instance of ApolloClient
2. Fire query
3. While Query returns data render loading..
4. Check and Authorise a route based on data.
5. Return Appropriate Component or redirect.
This can be done in following way:
import Loadable from 'react-loadable'
import client from '...your ApolloClient instance...'
const queryPromise = client.query({
query: Storequery,
variables: {
name: context.params.sellername
}
})
const CheckedComponent = Loadable({
loading: LoadingComponent,
loader: () => new Promise((resolve)=>{
queryPromise.then(response=>{
/*
check response data and resolve appropriate component.
if matching error return redirect. */
if(response.data.userStatus.isLoggedIn){
resolve(ComponentToBeRendered)
}else{
resolve(<Redirect to={somePath}/>)
}
})
}),
})
<Route path="/onboarding" component={CheckedComponent} />
Related API reference:
https://www.apollographql.com/docs/react/reference/index.html
If you are using apollo react client you can also import Query #apollo/components and use it like so in your private route:
<Query query={fetchUserInfoQuery(moreUserInfo)}>
{({ loading, error, data: userInfo = {} }: any) => {
const isNotAuthenticated = !loading && (isEmpty(userInfo) || !userInfo.whoAmI);
if (isNotAuthenticated || error) {
return <Redirect to={RoutesPaths.Login} />;
}
const { whoAmI } = userInfo;
return <Component user={whoAmI} {...renderProps} />;
}}
</Query>
where isEmpty is just checking if the given object is empty:
const isEmpty = (object: any) => object && Object.keys(object).length === 0
I'm wondering how others manage return visitors who are already logged into their site (Proved with cookie token)
Currently, I'm using a redux action in the root component, which triggers an API call to check if the cookie is valid or not.
When this comes back as TRUE of FALSE the next action is dependant on the URL that a user is trying to visit.
Currently:
- If a user is validated and visits / they will be sent to /contacts
If a user is not validated and they visit any protected route (e.g. /contacts) the will be kicked out to /.
Issues:
- A validated user will momentarily see the login page if they visit / before being forwarded.
- If they visit another protect route such as /form they will be kicked over to /contacts
I know that both these items are a result of being temporarily not logged in while the API call validates the user.
Question
I'm wondering how you handle this in your application?
1. Avoid seeing the login page while validation is happening if user is logged in.
2. If user is logged in get them to the page they've requested?
// App.js Router
class App extends Component {
componentWillMount = async () => {
await this.props.checkLogin();
}
render() {
const { loggedIn } = this.props;
const myProtectedRoutes = [
{component: Form, path: "/form", exact: true },
{component: Contacts, path: "/contacts", exact: true },
{component: ContactCard, path: "/contacts/card/:clientID", exact: true },
]
return (
<BrowserRouter basename="/" >
<div>
<NavBar isLoggedIn={loggedIn} />
<main>
<Switch>
<Route exact path="/" component={Login}/>
<Route exact path="/logout" component={Logout }/>
{myProtectedRoutes.map(
(d, i) =>
<ProtectedRoute
key={i}
isAccessible={ loggedIn ? true : false }
exact
redirectToPath={"/"}
path={d.path}
component={d.component}
/>
)}
</Switch>
</main>
<footer>
<Switch>
<Route path="/" component={Footer}/>
</Switch>
</footer>
</div>
</BrowserRouter>
);
}
}
// LoginForm.js component => appears at '/'
class App extends Component {
constructor(props){
super(props);
this.state = {
emailaddress: '',
password: '',
}
}
onLoginSubmit = (e) => {
e.preventDefault();
const { emailaddress, password } = this.state;
if (emailaddress && password) {
this.props.doLogin(emailaddress, password, stayloggedin);
}
}
render() {
let { loggedIn } = this.props
// If logged in is true go to contacts or
// I'd like to be able to get to the typed
// URL if logged in is true.
if ( loggedIn ){
return <Redirect push to={"/contacts"} />
}
return (
<div id="mainloginform" className="container">
<!-- Login form Fields go here -->
</div>
);
}
}
In general, user validation and redirects should happen in the server.
In express, when the user requests any route (/ or /contacts), following things should happen
Check if the user is valid(using cookie token)
If valid, redirect the user to the requested route.
If invalid, redirect the user to /(using req.redirect('/'))
Hope this helps!
In redux it is a common pattern to make three actions.
Consider showing a "in_progress" UI during fetch?
{ type: 'FETCH_REQUEST' }
{ type: 'FETCH_FAILURE', error: 'Oops' }
{ type: 'FETCH_SUCCESS', response: { ... } }
Refer here for more details.
I am creating a new React single page application and am trying to integrate with Auth0. I have been working with the React example but can't seem to get it working properly so that you are required to login immediately after the app loads. The problem I have now is that you are redirected to the login page twice because the app is rendering before the authentication is finished the first time. These are the important chunks of code:
index.js:
function initAuth(callback) {
const auth = store.getState().app.auth;
if(!auth.isAuthenticated()) {
auth.login();
}
callback();
}
//Authorize the app and then render it
initAuth(function () {
render(
<Provider store={store}>
<ConnectedRouter history={history}>
<div>
<App />
</div>
</ConnectedRouter>
</Provider>,
target
);
});
app.js:
const App = props => (
<div>
//... Other stuff
<Routes/>
//... More stuff
</div>
);
routes.js:
const Routes = props => (
<main>
<Switch>
//...Other Routes Are Here
<Route path="/callback" render={(props) => {
return <Callback data={props} {...props} />;
}}/>
<Redirect from='*' to='/'/> {/*Any unknown URL will go here*/}
</Switch>
</main>
);
callback.js:
const auth = store.getState().app.auth;
const handleAuthentication = (nextState) => {
console.log('Evaluation: ', /access_token|id_token|error/.test(nextState.location.hash))
if (/access_token|id_token|error/.test(nextState.location.hash)) {
auth.handleAuthentication();
}
};
class Callback extends Component {
componentDidMount() {
handleAuthentication(this.props.data);
}
//... Render method etc
}
auth.js:
login() {
this.auth0.authorize();
}
handleAuthentication() {
this.auth0.parseHash((err, authResult) => {
if (authResult && authResult.accessToken && authResult.idToken) {
this.setSession(authResult);
history.replace('/home');
} else if (err) {
history.replace('/home');
alert(`Error: ${err.error}. Check the console for further details.`);
}
});
}
isAuthenticated() {
// Check whether the current time is past the
// access token's expiry time
let expiresAt = JSON.parse(localStorage.getItem('expires_at'));
return new Date().getTime() < expiresAt;
}
Right now my app basically works. The only issue is that you are required to login twice because there is a race condition between the login response and the app load/check. Why it is happening makes perfect sense but I am not sure how best to fix it.
I keep a flag on state indicating that the application is booting. This flag is true until a few checks have been made including verifying if the user is already authenticated.
This prevents rendering of any components and just shows a loading indicator. Once the booting flag has been set to false the router will render.