Create wrapper to check user role in App.js component routes - reactjs

I know the question is very general but I cannot find the correct way and terms to search this online to see how it can be done.
My problem is that I have an app that users log in and then a user role is added to a context element (like if the user is admin or simple user etc).
I can restrict access to the components by checking the user role and redirect. somerhing like
return user==="admin"?<div>...</div>: "redirect to no access component"
Is a way to create a wrapper that will wrap every Route in the App.js and perform this check instead.

normally you should restrict the access in the router of your application.
but to anwser the question you can create a hooks that checks if user is admin or else redirect to the no access component using react-router:
const useCheckUserPermission = (permission) => {
// get the user from the context
useEffect(() => {
if(user === permission) // redirect to no access component
}, [])
return [];
}
then you can use it like this:
const Component = (props) => {
useCheckUserPermission("admin");
return // return your component
}

If its a page then you can follow the following Approach:
While attaching Routes to Router you can create custom Route component which will check the condition.
Like:
import { Route } from "react-router-dom";
const PrivateRouteAdmin=({path,component:Component...rest})=>{
const user = useContext(AuthContext).userType;// any global state which you have used
return user==='admin'?
<Route path={path} {...rest}
render={(props)=>{
return <Component {...props} />
}}/>: <Redirect to={"/"}/>
}
You can use it in you code as follows:
<BrowserRouter>
<PrivateRouteAdmin exact path={'/protected'} component={ProtectedPage} />
</BrowserRouter>
If you want to apply some condition on specific page element, then you can use the approach which you are following otherwise make another component for that purpose too like:
const AdminContent = ({children)=> {
const user = useContext(AuthContext).userType;// any global state which you have used
return user==='admin'?children:<Redirect to={"/"}/>
}
and you can call the component anywhere you want like:
<AdminContent> <div>protected</div></AdminContent>

Related

What am I doing wrong with React context?

Quick context, I am making a character-building app (think D&D Beyond). Users can log in, use the builder to generate a character, and then later recall and level-up that character. I'm hosting the data on MongoDB and using Realm Web for CRUD on the database. Based on this guide, I'm using React Context to keep track of the logged-in user as they navigate the site. I'm trying to create a widget in the NavMenu that will allow me to see the logged-in user for dev purposes (later, I can make a "Welcome, [name]!"-type greeting), but I keep getting the error that "mongoContext.user" is null. This makes sense, as the Context is set to null with useState, but as soon as the page renders there is an init() function which anonymously logs in the user if they aren't already. I can't seem to understand why this is happening, even after looking at 4 different articles on how to use React Context. Code Snippets below:
App.js
const [client, setClient] = useState(null);
const [user, setUser] = useState(null);
const [app, setApp] = useState(new Realm.App({id: process.env.REACT_APP_REALM_APP_ID}));
//Anonymously logs in to mongoDB App instance on page render
useEffect(() => {
const init = async () => {
if (!user) {
setUser(app.currentUser ? app.currentUser : await app.logIn(Realm.Credentials.anonymous()));
}
if (!client) {
setClient(app.currentUser.mongoClient('mongodb-atlas'));
}
}
init();
}, [app, client, user]);
//Function to render a given component and pass it the mongoContext prop
const renderComponent = (Component, additionalProps = {}) => {
return (
<MongoContext.Consumer>
{(mongoContext) => <Component mongoContext={mongoContext} {...additionalProps} />}
</MongoContext.Consumer>
);
}
return (
<MongoContext.Provider value={{app, client, user, setClient, setUser, setApp}}>
<Layout>
<Switch>
<Route exact path="/" render={() => renderComponent(Home)} />
... other Routes ...
</Switch>
</Layout>
</MongoContext.Provider>
);
}
As outlined in the guide, each Route receives a render function which wraps it in the Context.Consumer component and sends context. I couldn't figure out if this was possible with the Layout component (since it is itself a component wrapper), so instead I used React.useContext() inside the component I need context for: (the component relationship is Layout -> NavMenu -> UserDetail)
UserDetail.jsx
const UserDetail = (props) => {
const mongoContext = React.useContext(MongoContext);
return (
<div>
<h1>Logged in as: {mongoContext.user.id}</h1>
</div>
);
}
This is the component which gives the "mongoContext.user is null" error. The other components seem to use context just fine, as I have done some Create operations with the anonymous user. I can see that it probably has something to do with how the other components are rendered with the Consumer wrapper, but according to every guide I've read, the React.useContext in the UserDetail component should work as well. Thanks for any help, this has completely stymied the project for now.

Move between pages without losing pages content - React JS

I'm trying to navigate between pages without losing pages data. For instance, I have a page that contains many input fields, if a user filled all these fields and tried to move to another page and return back to the first one, all the inputs are gone. I am using react-router-dom but didn't find out a way to prevent that.
What I've done till now :
import { Route, Switch, HashRouter as Router } from "react-router-dom";
<Router>
<Switch>
<Route path="/home" exact component={Home} />
<Route path="/hello-world" exact component={HellWorld} />
</Switch>
</Router>
Home Component :
navigateToHelloWorld= () => {
this.props.history.push('/hello-world')
};
Hello World Component :
this.props.history.goBack();
I don't know that that can be supported in such generality. I would just store all state variable values in localStorage, and restore from there when values are present on component render (when using useState then as the default value). Something like this:
const Home = () => {
const [field, setField] = useState(localStorage.field || '');
const handleUpdate = (value) => {
setField(value);
localStorage.field = value;
}
// also add a submit handler incl. `delete localStorage.field`;
return ... // your input fields with handleUpdate as handler.
}
Generally, all you need to do is to store your data in someplace, either a component that doesn't unmount, like the component you are handling your routes in, which is not a good idea actually but it works!
another way is to use some kind of state manager like 'mobx','redux','mst', or something which are all great tools and have great documentation to get you started
another alternative is to store your data in the browser, for your example session storage might be the one to go for since it will keep data until the user closes the tab and you can read it in each component mount.

Cant figure out how to create a 'basic' HOC for a react / NextJS application

I have built an application that works well, but i now need to add some logic into each page that checks to see if the user has a subscription and if they do then send them to payment page to make a payment.
I have a hook (using SWR) that gets me the user session and within the hook it returns a boolean for isSubscribed. I intend to use this.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
An above example is what i currently do. But this solution requires me to copy pasta everytime to the page which obviously i can do, but it's no way efficient. I know that HOC exists and from what i know i an use a HOC to do this. But i have no idea how to write one that would fit this purpose.
As an added benefit, it would be useful to add the session as a prop to the 'new component' so that i dont have to call the hook twice.
Thanks for all and any help.
p.s. i mention it in the title, but i'm using NextJS. Not sure if this has any baring (i dont think it does, but worth mentioning)
You can create a wrapper HOC such as following;
const withSession = (Component: NextComponentType<NextPageContext, any, {}>) => {
const Session = (props: any) => {
const session = useSession();
if (session.isLoading) {
return <>Loading..</>
}
else {
return <Component {...props} />
}
};
// Copy getInitial props so it will run as well
if (Component.getInitialProps) {
Session.getInitialProps = Component.getInitialProps;
}
return Session;
};
And to use it in your page or component, you can simply do like;
const UserDetailPage: React.FC = (props) => {
// ...
// component's body
return (<> HI </>);
};
export default withSession(UserDetailPage);
I think this problem doesn't necessary require a HOC, but can be solved with a regular component composition. Depending on your actual use case, it may or may not be a simpler solution.
We could implement a Session component that would leverage the useSession hook and conditionally render components passed via the children prop:
const Session = props => {
const { isLoading } = useSession();
if (isLoading) {
return "Loading...";
}
return props.children;
};
Then nest the Page component into the Session:
const GuardedPage: React.FC<PageProps> = props => {
return (
<Session>
<Page {...props} />
</Session>
);
};
I see the question has already been answered, just wanted to suggest an alternative. One of the benefits of this approach is that we can wrap an arbitrary tree into the Session, and not just the Page.
Are you trying to return a page loading screen component and direct the user to the appropriate page based on thier subscription status? or isLoading handles one event and isSubscribed handles another?
Let's define (HOC) higher order component for the sake of your problem. By using HOC, logic can be modularized and redistributed throughout components. This HOC your creating should have the capability to call different methods on a single data source or one method to be applied across multiple components. For instance say you have an API component with 5 end points (login, subscribe, logout, unsubsubscribe) the HOC should have the ability to utilize any of the endpoints from any other component you use it in. HOC is used to create an abstraction that will allow you to define logic in a single place.
Your code calls one singular method to check if the session is in use of display the content of a page based on user subscription and page loading. Without seeing the components you are trying to use I can not determine the state that needs to be passed? but I will give it shot.
const session = useSession();
if(session.isLoading) {
return <></>;
}
if(!session.isSubscribed) {
/* redirect the user*/
}
return (
<p>HTML of the page / component</p>
)
First thing I see wrong in above code as a use case for an HOC component you have no export statement to share with other components. Also, why use 2 return statements for isLoading unless both conditions need to be checked (isLoading & isSubscribed) also, are these conditional statements depended on each other or seprate functions that can be called separately from another source? if you posted more of your code or the components you are pasting this into it would help?
To use this as an HOC in NEXT is essentially the same as react.
Dependant logic
const session = useSession(props);
// ad constructor and state logic
...
if(session.isLoading) {
return this.setState({isLoading: true});
} else {
return this.setState({isSubscribed: false});
}
Separate logic
const session = useSession(props);
// ad constructor and state logic
...
isLoading () => {
return this.setState({isLoading: true});
}
isSubscribed() => {
return this.setState({isSubscribed: true});
}
or something like this that uses routes...
import React, { Component } from 'react';
import { Redirect, Route } from 'react-router-dom';
export const HOC = {
isState: false,
isSubscribed(props) {
this.isState = false;
setTimeout(props, 100);
},
isLoading(props) {
this.isState = true;
setTimeout(props, 100);
}
};
export const AuthRoute = ({ component: Component, ...rest}) => {
return (
<Route {...rest} render={(props) => (
HOC.isAuthenticated === true ? <Component {...props} /> : <Redirect to='/' />
)}/>
)};
}
If you could share more of you code it would be more helpful? or one of the components you are having to copy and paste from your original HOC code. I would be easier than stabbing in the dark to assist in your problem but I hope this helps!
Cheers!

Routing components in React JS using login functionality

I am trying to build a full stack web application using Spring and React JS.
I have created login/register APIs in Spring and have connected them to React. They work just alright.
This is my UserService
import axios from 'axios';
const user_base_url = "http://localhost:8080/users";
class UserService{
createUser(user) {
return axios.post(user_base_url + '/register', user);
}
authenticateUser(user) {
return axios.post(user_base_url + '/login', user);
}
}
export default new UserService();
This how I validate my user in the LoginComponent.
validateUser = () => {
let user = {
username: this.state.email,
password: this.state.password,
};
UserService.authenticateUser(user).then((res) => {
if(res.data === 'SUCCESS') {
//logged in
console.log("logged in");
} else if(res.data === 'FAILURE') {
console.log("NO");
this.resetLoginForm();
this.setState({"error":"Invalid username or password"});
}
})
};
I now wish to add routes to my application so that certain components can only be accessed when logged in.
function App() {
return (
<div>
<Router>
<HeaderComponent/>
<div className="container">
<Switch>
<Route path="/" exact component={LandingPageComponent}></Route>
<Route path ="/customers" component = {ListCustomerComponent}></Route>
<Route path ="/add-customer/:id" component = {CreateCustomerComponent}></Route>
<Route path = "/view-customer/:id" component = {ViewCustomerComponent}></Route>
<Route path = "/admin-login" component = {AdminLoginComponent}></Route>
<Route path = "/admin-register" component = {AdminResgisterComponent}></Route>
</Switch>
</div>
<FooterComponent/>
</Router>
</div>
);
}
export default App;
How can this be achieved? I found solutions using tokens but I haven't used a token, I only check user is entering correct username and password from the database (MySQL) via my REST API.
Any help would be highly appreciated.
There are two types of Routes that are usually defined: Open Routes and Protected Routes.
Open Routes are the ones that are accessible by a user without any authentication and Protected Routes are the ones that require a certain form of authentication to be accessed.
Now, let's proceed to answer your questions.
How to Implement a Protected Route?
First, you need to know whether the user is authenticated or not and for the same, you will need to use a certain "value" or "token" (Like An ID Card) that says that this user is authenticated.
For a simple practise application, you could just store a Boolean saying whether the user is authenticated or not.
You will need to store this value in a place such as Local Storage, Cookies or Session Storage.
For this example, I have assumed that you are storing the value in a local Storage.
A Protected Route will be wrapped around with a condition that checks the Local Storage to find a value that says the user is authenticated.
isAuthenticated === true ===> Show The Desired Component.
isAuthenticated === false ===> Redirect the User to the login page.
// isAuthenticated is extracted from the local storage.
<Route path="/aClassfiedPath" render={() => (
isAuthenticated === true
? <DesiredComponent />
: <Redirect to='/login' />
)} />
You will also notice another practice, that is, to make a totally separate Layout for the Protected Components and inside the Layout, check whether the user is authenticated or not.
// Assuming the required Modules have been imported
const ProtectedLayout = ({children}) => {
if (!isAuthenticated) {
return (
<Redirect to="/loginPage" />
)
}
// children contains the Desired Component and `div` tag represents a custom
// container meant for Protected Routes. Like: A Speacial Header or Side Navigation.
return (
<div>
{children}
</div>
)
}
Recommended Read
An Article on Protected Routes
<Redirect> Reference
Render Prop of <Route>

Route handling a uniquely generated URL specific to a user in ReactJS and react-router-dom v4

In the app I am working on if a user forgot their password, they click on a link that brings them to a page where they enter the username. If the username is matched, it sends a uniquely generated URL to the email associated with the username. For example:
http://localhost:8000/auth/security_questions/0e51706361e7a74a550e995b415409fdab4c66f0d201c25cb4fa578959d11ffa
All of this works fine, but I am trying to figure out how to handle this on the front-end routing using React and react-router-dom v4. I made this route.
<Route exact path='/auth/security_questions/:key' component={SecurityQuestions} />
The correct component loads related to security questions, but that is not the behavior I am after. After all, it takes anything you put after security_questions/.
What it should be doing is matching :key against the database before it loads the component.
I'm not sure about a few things:
1) How to parse out the :key portion so that I can pass it as a value to verify against the database.
2) While I have a general idea of how to handle the verification, I am not sure how to tell React: "Ok, the key has been verified in the database. Finish loading the component."
I think it would in general look like:
// ./containers/security_questions.js
componentWillMount() {
this.props.verifyKey(:key);
}
And then actions:
// ./actions/authentication.index.js
export function verifyKey({ :key }) {
return function(dispatch) {
axios
.post(
`${ROOT_URL}/api/auth/security_questions/`,
{ :key }
)
.then(response => {
dispatch('Finish loading the rest of the component')
})
.catch(error => {
dispatch(authError(error.response.data.non_field_errors[0]));
});
}
}
Or maybe instead of it finishing loading the component, it should just route to a different URL that is a protected route.
You can grab the params from the path like so (https://reacttraining.com/react-router/web/api/Route):
<Route path="/user/:username" component={User}/>
const User = ({ match }) => <h1>Hello {match.params.username}!</h1>
You will want to conditionally render based upon some state set by verifyKey.
componentWillMount() {
this.props.verifyKey(this.props.match.params.key);
}
render() {
if (this.state.permitRender) {
return <Component>
} else {
return <LoadingComponent />
}
You can use the render method on the route to put in your verification logic and render the appropriate control. You would need to move the action to verify the key to the component which renders the route, rather than the SecurityQuestions component.
<Route exact
path='/auth/security_questions/:key'
render={(props)=>{
let finalComponent= <ComponentToRenderWhenKeyDoesNotMatch/>;
if(this.props.verifyKey(props.match.params.key)){
resultantComponent=<SecurityQuestions/>
}
return finalComponent;
}}
/>
Route params will be inside the match.params prop:
componentWillMount() {
this.props.verifyKey(this.props.match.params.key);
}
See https://reacttraining.com/react-router/web/example/url-params
--
Then on the success of your security questions I would set some state in your reducer and render based on that.

Resources