I want to implement RBAC in my React project and here I have different role Like ADMIN,SUPERADMIN etc. and I am storing that user role in LocalStorage and showing information according to the user role so what actually happening that if user is ADMIN that store in localStorage have only access to limited things but when he change its role to SuperAdmin from localstorage from dev or console he get all the access of SuperAdmin what I can do?
You can Create one Middleware so every route passed or render from that Middleware and It's a better way to you can use API's call in your middleware for checking the Roles.
Example :
PrivateRoute.tsx
import React, { useContext, useEffect } from 'react'
import { Redirect, Route } from 'react-router-dom'
export const PrivateRoute = props => {
// Get User info from local storage
const userData = localStorage.getItem('user')
const userRole = userData?.role;
useEffect(() => {
// do something here for async check
},[])
const hasPermission = useMemo(() => {
// some condition checks for roles access for perticluar module
return ...
}, [userRole]);
if (hasPermission) {
return <Route {...props} />
}
return <Redirect to={'Some Path'} />
};
The fact is that handling this kind of authorization is not enough on the frontend side. You can ask your backend to give you whether the user is ADMIN or SUPERADMIN or whatever. Then you can store this authentication status in a state management, so you'd easily access that after.
For implementing different situations, You can use guard some routes or render components conditionally considering which type the user is.
In my opinion, HOCs (wrapping the components with them) & conditional rendering can help you with RBAC.
Related
I am currently building a react app and am in the beginning stages of setting up my user authentication. The actual user management is pretty easy, as I am using a database service (Supabase) that handles all the user authentication / session management for me. However, I have run into an issue with conditionally rendering parts of my app based on whether or not the user is signed in. For example, I am wanting to render a different navigation bar with different buttons based on if there is a user signed in or not. I have created a custom hook to hold all the sign in stuff from supabase and am attempting to create a context that I can reference elsewhere in the app. My context seems to be giving me trouble though. When I initially log in with a valid user, everything works as expected. I can see the user ID in the context store, and my navigation bar renders according to a logged in user. However, I have found that if I reload the page, my context value clears. Here is the code where I am initializing my context (please note my use of typescript):
import React, { useEffect } from "react";
import { createContext, useContext, useState } from "react";
import useCurrentUser from "../hooks/useCurrentUser";
import supabase from "../supabaseClient";
type TestT = [String, React.Dispatch<React.SetStateAction<String>>];
const UserContext = createContext<TestT>(undefined!);
export function useUser() {
return useContext(UserContext);
}
export function UserProvider({ children }: any) {
console.log("IN USER PROVIDER");
console.log(useCurrentUser());
const [test, setTest] = useState(useCurrentUser()!);
return (
<UserContext.Provider value={[test, setTest]}>
{children}
</UserContext.Provider>
);
}
export { UserContext };
So here is the problem. You will notice I am setting the value in the Provider to a state value, which is defaulting to my useCurrentUser hook. However, when I reload the page, I can see in react dev tools that the 'test' state, and therefore the Provider value, are being set to undefined. This is perplexing, because just prior to setting the 'test' state value to the useCurrentUser hook value, I am printing it, and it does in fact print a valid user ID. So, I am confused why, even though useCurrentUser DOES have a value, it is not setting to my state value and provider value. What am I doing wrong here? How can I get my context to retain the user ID even when the page reloads?
I have a menu component that appears globally. What is the best practice for getting data into that component?
I'm trying to take advantage of static generation that Next.js offers but all data fetching guidance from the Next.js team relates to pages. getStaticProps and getStaticPaths seem to pertain to page generation, not data for components. Is their SWR package the right answer, or Apollo Client?
Typically in hooks-based React, I'd just put my data call into useEffect. I'm not sure how to reason this out being that everything is rendered at build time with Next.
This is such a tricky problem, I think we need to lay out some background before a solution comes into focus. I'm focusing in the React.js world but a lot of this would apply to Vue/Nuxt I'd imagine.
Background / Static Generation Benefits:
Gatsby and Next are focused on generating static pages, which vastly improves performance and SEO in React.js sites. There is a lot of technical overhead to both platforms beyond this simple insight but let's start with this idea of a digital machine pumping out fancy HTML pages for the browser.
Data Fetching for Pages
In the case of Next.js (as of v9.5), their data fetching mechanism getStaticProps does most of the heavy lifting for you but it's sandboxed to the /pages/ directory. The idea is that it does the data fetching for you and tells the Next.js page generator in Node about it during build time (instead of doing it component-side in a useEffect hook - or componentDidMount). Gatsby does much the same with their gatsby-node.js file, which orchestrates the data fetching for page building in concert with a Node server.
What about Global Components that need data?
You can use both Gatsby and Next to produce any kind of website but a huge use case are CMS-driven websites, because so much of that content is static. These tools are an ideal fit to that use case.
In typical CMS sites, you will have elements that are global - header, footer, search, menu, etc. This is where static generation faces a big challenge: how do I get data into dynamic global components at build time? The answer to this question is... you don't. And if you think about this for a minute it makes sense. If you had a 10K page site, would you want to trigger a site-wide rebuild if someone adds a new nav item to a menu?
Data Fetching for Global Components
So how do we get around this? The best answer I have is apollo-client and to do the fetch client side. This helps us for a number of reasons:
For small size queries, the performance impact is negligible.
If we need to rebuild pages for changes at the CMS layer, this slides by Next/Gatsby's detection mechanisms, so we can make global changes without triggering gigantic site-wide rebuilds.
So what does this actually look like? At the component level, it looks just like a regular Apollo-enhanced component would. I usually use styled-components but I tried to strip that out so you can could better see what's going on.
import React from 'react'
import { useQuery, gql } from '#apollo/client'
import close from '../public/close.svg'
/**
* <NavMenu>
*
* Just a typical menu you might see on a CMS-driven site. It takes in a couple of props to move state around.
*
* #param { boolean } menuState - lifted state true/false toggle for menu opening/closing
* #param { function } handleMenu - lifted state changer for menuState, handles click event
*/
const NAV_MENU_DATA = gql`
query NavMenu($uid: String!, $lang: String!) {
nav_menu(uid: $uid, lang: $lang) {
main_menu_items {
item {
... on Landing_page {
title
_linkType
_meta {
uid
id
}
}
}
}
}
}
`
const NavMenu = ({ menuState, handleMenu }) => {
// Query for nav menu from Apollo, this is where you pass in your GraphQL variables
const { loading, error, data } = useQuery(NAV_MENU_DATA, {
variables: {
"uid": "nav-menu",
"lang": "en-us"
}
})
if (loading) return `<p>Loading...</p>`;
if (error) return `Error! ${error}`;
// Destructuring the data object
const { nav_menu: { main_menu_items } } = data
// `menuState` checks just make sure out menu was turned on
if (data) return(
<>
<section menuState={ menuState }>
<div>
{ menuState === true && (
<div>Explore</div>
)}
<div onClick={ handleMenu }>
{ menuState === true && (
<svg src={ close } />
)}
</div>
</div>
{ menuState === true && (
<ul>
{ data.map( (item) => {
return (
<li link={ item }>
{ item.title }
</li>
)
})}
</ul>
)}
</section>
</>
)
}
export default NavMenu
Set Up for Next to Use Apollo
This is actually really well documented by the Next.js team, which makes me feel like I'm not totally hacking the way this tool is supposed to work. You can find great examples of using Apollo in their repo.
Steps to get Apollo into a Next app:
Make a custom useApollo hook that sets up the connection to your data source (I put mine in /lib/apollo/apolloClient.js within Next's hierarchy but I'm sure it could go elsewhere).
import { useMemo } from 'react'
import { ApolloClient, InMemoryCache, SchemaLink, HttpLink } from '#apollo/client'
let apolloClient
// This is mostly from next.js official repo on how best to integrate Next and Apollo
function createIsomorphLink() {
// only if you need to do auth
if (typeof window === 'undefined') {
// return new SchemaLink({ schema })
return null
}
// This sets up the connection to your endpoint, will vary widely.
else {
return new HttpLink({
uri: `https://yourendpoint.io/graphql`
})
}
}
// Function that leverages ApolloClient setup, you could just use this and skip the above function if you aren't doing any authenticated routes
function createApolloClient() {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(),
cache: new InMemoryCache(),
})
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract()
// Restore the cache using the data passed from getStaticProps/getServerSideProps
// combined with the existing cached data
_apolloClient.cache.restore({ ...existingCache, ...initialState })
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
// This is goal, now we have a custom hook we can use to set up Apollo across our app. Make sure to export this!
export function useApollo(initialState) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}
Modify _app.js in the /pages/ directory of Next. This is basically the wrapper that goes around every page in Next. We're going to add the Apollo provider to this, and now we can globally access Apollo from any component.
import { ApolloProvider } from '#apollo/react-hooks'
import { useApollo } from '../lib/apollo/apolloClient'
/**
* <MyApp>
*
* This is an override of the default _app.js setup Next.js uses
*
* <ApolloProvider> gives components global access to GraphQL data fetched in the components (like menus)
*
*/
const MyApp = ({ Component, pageProps }) => {
// Instantiates Apollo client, reads Next.js props and initialized Apollo with them - this caches data into Apollo.
const apolloClient = useApollo(pageProps.initialApolloState)
return (
<ApolloProvider client={ apolloClient }>
<Component {...pageProps} />
</ApolloProvider>
)
}
export default MyApp
And now you can get dynamic data inside of your components using Apollo! So easy right ;) HA!
For global data fetching in NextJS, I use react-query and there is no need for a global state because it lets you to cache the data. Let's say you have a blog with categories and you want to put the category names in the navbar as a dropdown menu. In this case you can call the API to fetch the data with react-query from the navbar component and cache it. The navbar data will be available for all pages.
I have an app which header contains icon which should be shown when the user is logged in. I keep my logged in info in sessionStorage but when it changes my component is not rendered again. I tried to use useEffect for that and useMemo but it doesn't worked.
The updating part:
const isLoggedIn = useMemo(() => sessionStorage.getItem('isLogged'), [sessionStorage.getItem('isLogged')]);
The usage:
{isLoggedIn === 'true' ? ['left'].map((anchor) => (
...some jsx
)) : null}
The sessionStorage value is a string: "false" or "true".
I have routes and constant header, the header is not a part of routes so when it changes my header is not rerenders so I tried to use useMemo for that.
Posting my answer as per clarification gained through comments.
If you are using Redux:
I would recommend to store the user logged-in information in redux store and connect to the isolated Header component via connect HOC and mapStateToProps. Whenever you update (upon successful user login) the user login status the component will listen to store updates.
Or
You can use React context approach if there is no redux used
// Declare it outside of your App component/any other file and export it
const GlobalState = React.createContext();
// Declare state variable to store user logged in info inside of your App component
const [isLoggedIn, setIsLoggedIn] = useState(false);
// Add them to context to access anywhere in your components via useContext
// In App render or where you have route mapping
<GlobalState.Provider value={{
isLoggedIn,
setIsLoggedIn
}}>
....
</GlobalState.Provider>
// Update the status using setIsLoggedIn upon successful login where you are making login call
// In your Header get it via useContext
const context = useContext(GlobalState);
`context.isLoggedIn` is what you need.
// You can use useEffect/useMemo approach to get the login status updates
Find more about React context and useContext
sessionStorage is not an observer object and you have to store the current authentication state into a variable or React state and use that variable in your component. And when you authenticated the user, you should update the variable to true and change that to false when the user logged out.
To implement what I said, you can get help from these ways:
Redux
React context
You can implement the React context by your self from scratch or using the React-hooks-global-state
UseMemo is used for memoizing calculated values. You should be using useCallback.useCallback is used for memoizing function references.
Refer this
const isLoggedIn = useCallback(() => sessionStorage.getItem('isLogged'), [sessionStorage.getItem('isLogged')]);
Can you try to put your sessionStorage data into State and update that state? As far as I know, react will not know about the session storage. So even if you change the manipulate the data in the sessionStorage directly it won't gonna update your UI.
let [storeData, setStoreData] = useState(true);
let isLoggedIn = useMemo(() => ({ sessionData: storeData }), [storeData]);
{isLoggedIn === 'true' ? ['left'].map((anchor) => (
...some jsx
)) : null}
<button
onClick={() => {
sessionStorage.setItem("isLogged", !storeData);
setStoreData(sessionStorage.getItem("isLogged"));
}} > Update Store </button>
I built an app starting from a super easy example on medium.
To summarize, it appears the user comes into my app at the bottom part. I then pass the user down through props:
function App({user)){
return <AppContainer user={users} />
}
export default withFirebaseAuth({
providers,
firebaseAppAuth,
})(App);
Later in my app I want to update the user:
function AppContainer( { signOut, user } ){
const [user, setUser] = useState(user);
firebase.firestore().collection('users').doc(user.uid).get().then(function(doc) {
if (doc.exists) {
setUser(doc.data())
But it doesn't work. What's the Best way to do this?
Additionally it would be ideal if the two API calls could be combined somehow.
Last question is - I don't really understand how the export part of the code works - is it like context and i should be able to access the data coming in as props all the way down? Can I refactor it into context?
Usually for similar API calls useEffect hook is used, as documentation states:
If you’re familiar with React class lifecycle methods, you can think of useEffect Hook as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
First I would add the firebase calls into that hook then secondly I would change the user variable's name, you have one coming through props and similarly you are defining with useState.
I would consider using as the following:
function AppContainer( { signOut, user } ){
const [currentUser, setCurrentUser] = useState(user);
useEffect(() => {
firebase.firestore().collection('users').doc(currentUser.uid).get().then(function(doc) {
if (doc.exists) {
setUser(doc.data());
}
});
}, []);
At the end, you will have the returned user information from firebase in currentUser state object.
I hope that helps!
I'm following this tutorial for Apollo here: https://github.com/howtographql/react-apollo/blob/master/src/components/Header.js.
In the Header.js file they have:
const userId = localStorage.getItem(GC_USER_ID)
which seems to me a bit ugly: everytime Header render() is called you read from localStorage?
With react-redux I used to go with something like this:
render() {
const { authentication } = this.props;
...
}
const mapStateToProps = state => ({
authentication: state.authentication
});
export default connect(mapStateToProps, { logoutAction })(Navbar);
And also if I read from localStorage on every render() and let's say I wanna store in localStorage all my user's data (eg. {username: "john", age: "15"}) when I pass this const to one of my children components everytime it re-render because the JSON.parse makes it a new object! Everytime!
How to have something like a Redux Store with Apollo?
Apollo's solution for managing state is through apollo-link-state. Here is some general info and the docs. After setting it up, you can then query state within a GraphQL query.
It has not reached 1.0 release yet, but it's usable and fairly easy to setup. Their examples aren't great yet. However, if you have some familiarity with GraphQL resolvers, it's fairly simple to setup. I'm using it here with success.