React keep a state in the highest component - reactjs

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;
}

Related

Following a 301 Redirect from an API in a React App

I've written a custom ProtectedRoute component for my React app that redirects a user to a /api/login route on my Express API if that user is not authenticated. The /api/login route returns a 301 Redirect to the Auth0 Universal Login UI.
I know the /api/login route on the Express API works because I can hit it directly I get redirected to the Auth0 Universal Login (see the last code snippet).
I also know the ProtectedRoute is redirecting correctly because it redirects to localhost:3000/api/login, which is the correct route on the Express API to trigger the Auth0 Universal Login redirect.
What actually happens though is that localhost:3000/api/login shows up in the address bar but the redirect to the Auth0 Universal Login doesn't happen. That being said if I refresh the page then the redirect to the Universal Login UI works.
I'm not exactly sure why the Redirect returned from /api/login isn't followed in the Browser. I think it has something to do with how React is navigating to the route.
Here's the relevant code snippets. If more are needed let me know.
Protected Route Component
import { Navigate, useLocation } from 'react-router-dom';
interface ISession {
userId: string;
role: string;
details: any;
}
type RouteProps = {
children?: JSX.Element;
session: ISession;
loading: boolean;
};
const ProtectedRoute = ({ session, children, loading }: RouteProps) => {
const location = useLocation();
if (loading) return null;
else if (!!session.userId) {
return children ? children : <Outlet />;
}
else {
return <Navigate replace to="/api/login" state={{ redirectTo: location.pathname }} />;
}
};
export default ProtectedRoute;
How the ProtectedRoute Component is used with React Router
import { useContext } from 'react';
import { Route, Routes } from 'react-router-dom';
import Home from './pages/home';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import { SessionContext } from './context/SessionContext';
import ProtectedRoute from './middleware/protectedRoute';
const App = () => {
const { session, loading } = useContext(SessionContext);
console.log('Session', session);
return (
<Routes>
<Route element={<ProtectedRoute loading={loading} session={session} />}>
<Route path="/" element={<Home />} />
<Route path="/dne" element={ <p>Stuff</p> } />
</Route>
</Routes>
);
};
export default App;
NOTE: I'm excluding the code from the SessionContext component for brevity. Since the user isn't able to login because the redirect to the Auth0 Universal Login UI doesn't work no session is ever created.
The "/api/login" route handler on the Express API
const login = (req: Request, res: Response) => {
const { redirectTo } = req.query;
const domain = config.get('auth0.domain');
const clientId = config.get('auth0.clientId');
const host = config.get('host');
// These 301 Responses are the Redirect to Auth0's Universal Login UI
if (redirectTo) {
const encodedRedirect = base64.urlEncode(redirectTo as string); // A Custom Base64 encoder that is URL Safe
res.status(301).redirect(`${domain}/auth/authorize?response_type=code&scope=openid&client_id=${clientId}&redirect_uri=${host}/api/auth/callback&state=${encodedRedirect}`)
} else {
res.status(301).redirect(`${domain}/auth/authorize?response_type=code&scope=openid&client_id=${clientId}&redirect_uri=${host}/api/auth/callback`);
}
};
It's not really an answer for what is going on but it is a solution.
The problem was <Navigate to="/api/login" /> would cause React to Rerender the page and would change the URL in the address bar, but it would not cause the browser to make a GET request to the new address.
To solve this I just overwrote the window.location.href with /api/login in the ProtectedRoute component.
Here's the new version of the ProtectedRoute component.
const ProtectedRoute = ({ session, children, loading }: RouteProps) => {
const location = useLocation();
if (loading)
return null;
if (!!session.userId) {
return children ? children : <Outlet />;
}
else {
window.location.href = '/api/login';
return <Navigate replace={true} to='/api/login' />
}
};

Pass object to child then access object methods

So I have a file routes.jsx which handles the routing of my application. Following the docs for react-router-dom, I wanted to start working on login/logout.
For now, I have implemented a fakeAuth object which will authenticate against the backend and set an isAuthenticated var. However, I think I need to be able to pass this auth around to different components, such as the Login.jsx to actually change the state of isAuthenticated, and to let's say a NavBar.jsx so I can change a login button to a logout button (to clarify, all my components are functional components (trying) using hooks and do not extend React.Component in a class manner).
However, how do I pass the fakeAuth as a prop and still have access to the methods inside? If I declare fakeAuth inside the Login.jsx component, fakeAuth is defined. If I pass it in as a prop, it is considered not defined.
routes.js
export default (
<BrowserRouter>
<Route path='/'>
<LandingPage fakeAuth={fakeAuth}/> // Landing page has the NavBar component as a child.
</Route>
<Switch>
<Route path="/login">
<Login fakeAuth={fakeAuth}/> // I want the Login component to have access to fakeAuth.whatever
</Route>
<PrivateRoute path="/home">
<UserApp/>
</PrivateRoute>
</Switch>
</BrowserRouter>
);
const fakeAuth = {
isAuthenticated: false,
authenticate(cb) {
fakeAuth.isAuthenticated = true;
setTimeout(cb, 100); // fake async
},
signout(cb) {
fakeAuth.isAuthenticated = false;
setTimeout(cb, 100);
}
};
Login.jsx
const Login = ({fakeAuth}) => {
let history = useHistory();
let location = useLocation();
let { from } = location.state || { from: { pathname: "/home" } };
let login = () => {
fakeAuth.authenticate(() => { // Need to access authenticate.
history.replace(from);
});
};
return (
// login modal etc
<button onClick={login}>Log in</button>
);
}
The above code is the code that returns fakeAuth is undefined.
Any advice would be great!

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

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}
/>

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.

In a CRA app, how to wait for some action(redux) to get complete first and then only proceed with the App.js render() function?

I am trying to figure out a way to store the authentication state of a user inside the redux store. Suppose isAuthenticated store the state of user if they are logged-in or not. Now, I have a cookie(httpOnly) sent by the server which remembers the user, so that they don't need to enter there credentials every time they visit the app.
Flow: User some day logged in to the application and didn't logged out and closed the browser. Now, he returns and visit my app. Since, the cookie was there in browser, this will be sent automatically(without user interaction) by the application and if the cookie is valid, the isAuthenticated: true. Very simple requirement.
Tracking the authentication status should be the first thing done by the application, so I put that logic at very first, before the App.js renders.
class App extends Component {
store = configureStore();
render() {
return (
<Provider store={this.store}>
<ConnectedRouter history={history}>
<>
<GlobalStyle />
<SiteHeader />
<ErrorWrapper />
<Switch>
<PrivateHomeRoute exact path="/" component={Home} />
<Route exact path="/login" component={LoginPage} />
<PrivateHomeRoute path="/home" component={Home} />
........code
}
This is the configureStore()
export const history = createBrowserHistory();
const configureStore = () => {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer(history),
composeEnhancers(applyMiddleware(sagaMiddleware, routerMiddleware(history)))
);
sagaMiddleware.run(rootSaga);
store.dispatch({ type: AUTH.AUTO_LOGIN });
return store;
};
store.dispatch({ type: AUTH.AUTO_LOGIN }); is the code where I am trying the application to do the auto-login as the first operation in the application. This action is handled by a redux-saga
function* handleAutoLogin() {
try {
const response = yield call(autoLoginApi);
if (response && response.status === 200) {
yield put(setAuthenticationStatus(true));
}
} catch (error) {
yield put(setAuthenticationStatus(false));
}
}
function* watchAuthLogin() {
yield takeLatest(AUTH.AUTO_LOGIN, handleAutoLogin);
}
autoLoginApi is the axios call to the server which will carry the cookie with it. setAuthenticationStatus(true) is action creator which will set the isAuthenticated to true false.
So, yes this is working BUT not as expected. Since, the app should first set the isAuthenticated first and then proceed with the App.js render(). But, since setting the isAuthenticated take some seconds(api call), the application first renders with the isAuthenticated: false and then after the AUTH.AUTO_LOGIN gets completed, then the application re-render for authenticaed user.
What's the problem then? For the normal component it may not be the problem, e.g this SiteHeader component
class SiteHeader extends React.Component {
render() {
const { isLoggedIn } = this.props;
if (isLoggedIn === null) {
return "";
} else {
if (isLoggedIn) {
return (
<LoggedInSiteHeader />
);
} else {
return (
<LoggedOutSiteHeader />
);
}
}
}
}
const mapStateToProps = ({ auth, user }) => ({
isLoggedIn: auth.isLoggedIn,
});
export default connect(
mapStateToProps,
null
)(SiteHeader);
But, this solution doesn't work for the Custom routing.
const PrivateHomeRoute = ({ component: ComponentToRender, ...rest }) => (
<Route
{...rest}
render={props =>
props.isLoggedIn ? (
<ComponentToRender {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
const mapStateToProps = auth => ({
isLoggedin: auth.isLoggedIn
});
export default connect(
mapStateToProps,
null
)(PrivateHomeRoute);
PrivateHomeRoute gets resolved before the redux store gets updated, hence the Route always goes to "/login".
I am looking for a solution, where the application doesn't proceed further until the authentication action doesn't complete. But, I am no clue what and where to put that code?
Few things I tried:
async await on configureStore() - Error came
async await on App.js - Error
PS: Libraries I am using redux, redux-saga,react-router-dom, connected-react-router, axios
One way I figured out:
Create a separate component MyRouteWrapper which will return the routes based on the isLoggedIn status. To, resolve the issue I stop the routes to render until the auto-login changes the isLoggedIn state.
I set the default state of isLoggedIn to null. Now, if the state is null the MyRouteWrapper will return an empty string, and once the state gets changes to true/false, it will return the routes, and then respective components get rendered.
I changed my App.js
const store = configureStore();
class App extends Component {
render() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<MyRouteWrapper />
</ConnectedRouter>
</Provider>
);
}
}
export default App;
The component which make sure to return the Route only when the state gets changed to true/false
const MyRouteWrapper = props => {
if (props.isLoggedIn === null) {
return "";
} else {
return (
<>
<GlobalStyle />
<SiteHeader />
<ErrorWrapper />
<Switch>
<ProtectedHomeRoute
exact
path="/"
component={Home}
isLoggedIn={props.isLoggedIn}
/>
<Route path="/profile/:id" component={Profile} />
<Route path="/login" component={LoginPage} />
</Switch>
</>
);
}
};
const mapStateToProps = ({ auth }) => ({
isLoggedIn: auth.isLoggedIn
});
export default connect(mapStateToProps)(MyRouteWrapper);
This solved the issue.
I am still curious to know the solutions(better) anyone have in there mind.

Resources