How do I configure the getPermissions() method in the AuthProvider in react-admin?

1k views Asked by At

I have tried creating custom hooks to call the DataProvider getOne() method to fetch the permissions based on the username I get from the authProvider after login, but the constant requirement of calling the hook from the body of the function throws an error because I call it from a method.

Where is the permissions coming from? How is 'ra-core' calling this getPermissions()? Why is there an error called .then() not a function in getPermissions()?

There needs to be better documentation on this aspect of the AuthProvider to help even experienced react-admin folks. Just saying.

Hook to fetch permissions:

const useFetchPermissions = ({ emailId }) => {
  const dataProvider = useDataProvider();
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState();
  console.log("fetching permissions");

  const [permissions, setPermissions] = useState();

  useEffect(() => {
    dataProvider
      .getOne("users/permissions", { id: emailId })
      .then(({ data }) => {
        setPermissions(data);
        setLoading(false);
      })
      .catch((error) => {
        setError(error);
        setLoading(false);
      });
  }, []);

  if (loading) return <Loading />;
  if (error) return <Error />;
  if (!permissions) return null;

  return permissions;
};

AuthPRovider login method:

login: () => {
    /* ... */
    console.log("Logging in..");
    //localStorage.setItem("permissions", "CREATE_ITEM");
    return tfsAuthProvider.login();

AuthProvider getPermissions method:

getPermissions: () => {
    const role = localStorage.getItem("permissions");
    console.log(role);
    //useFetchPermissions(authProvider.getAccount().userName); throw error
    return role === null ? Promise.resolve() : role;
  },

App.js dataProvider(url,useHttpClient) calls this:

const useHttpClient = (url, options = {}) => {
  if (!options.headers) {
    options.headers = new Headers({ Accept: "application/json" });
  }
  // add your own headers here
  //options.headers.set("Access-Control-Allow-Origin", "true");
  //const token = localStorage.getItem("token");
  //const userName = authProvider.getAccount().userName;
  //const [permissions, setPermissions] = useState();
  //const permissions = useFetchPermissions(userName);  throws error

  //localStorage.setItem("permissions", permissions);
  options.headers.set(
    "Authorization",
    `Bearer ${authProvider.getAccount().userName}`
  );
  return fetchUtils.fetchJson(url, options);
};
2

There are 2 answers

0
Ananth George On BEST ANSWER

Thanks to @KiaKaha 's answer I was able to implement something that worked for me that's a workaround the exceptions of react-hooks.

EDIT: Here's an improved solution

login: () => {
    /* ... */
    console.log("Logging in..");

    console.log(tfsAuthProvider.authenticationState);

   
    return tfsAuthProvider.login().then(async () => {
      const result = await fetch(
        process.env.REACT_APP_BASE_URL +
          "/users/permissions/" +
          tfsAuthProvider.getAccount().userName
      );
      
      const body = await result.json();
      console.log(body);
      localStorage.setItem("permissions", body);
      return Promise.resolve();
    });
   
  },
  1. The dataProvider.getOne() method is a react-hook and hence I was not able to call it. So used fetch(). Used environment variable to fetch the URL that dataProvider uses. (This still stays true).

<-- Everything after this is unnecessary , may be helpful to solve other problems.-->

  1. The response could not be converted to string directly so I used the response.body.getReader() and UTF8 decoder to decode the result of read().

    login: () => {
    
     console.log("Logging in..");
    
     console.log(tfsAuthProvider.authenticationState);
    
     const utf8Decoder = new TextDecoder("utf-8");
    
     return tfsAuthProvider
       .login()
       .then(() =>
         fetch(
           new Request(
             process.env.REACT_APP_BASE_URL +
               "/users/permissions/" +
               tfsAuthProvider.getAccount().userName,
             {
               method: "GET",
               headers: new Headers({ "Content-Type": "application/json" }),
             }
           )
         )
       )
       .then((response) => {
         if (response.status < 200 || response.status >= 300) {
           throw new Error(response.statusText);
         }
         return response.body.getReader();
       })
       .then((reader) => reader.read())
       .then(({ done, value }) => {
         // if (done) {
         //   controller.close();
         //   return;
         // }
    
         const permissions = utf8Decoder.decode(value);
    
         console.log(permissions);
         localStorage.setItem("permissions", permissions);
         return Promise.resolve();
       });
    },
    

Hope this helps anyone coming through this rabbit hole.

More on Readable Stream Objects - https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams

More on the login implementation - https://marmelab.com/react-admin/Authorization.html

3
Kia Kaha On

Why calling the API for the user's permissions every time if they are based only on the emailId which is not changing thoughout the user's session?

I would make the call in the login method of the authProvider where you have two options:

  1. Depends on the API and the backend, you could (if you have the option) return the permissions in a complex json object or inside the jwt along with the other user info upon login.

  2. Based on the emailId and after succcessful login response, make a successive call (as the one above in the useFetchPermissions) and again store them somewhere you can access them later on getPermissions().

I have not tested this piece of code so it probably will have some errors but just an idea how you can go without the hook and build the login pipeline.

   login: () => {
      console.log("Logging in..");
      
      tfsAuthProvider
         .login() 
         .then(response => {
             if (response.status < 200 || response.status >= 300) {              
                throw new Error(response.statusText);
             }

             return response.json();
         })
        .then(({ emailId }) => (
             dataProvider.getOne("users/permissions", { id: emailId })
        ))
        .then(({ data }) => {
             const permissionsString = JSON.stringify(permissions);
             // store wherever you want
             localStorage.setItem(userPermissionsKey, permissionsString);                                  
        })
       .catch((error) => {
            throw new Error(error);                
       });
   }