0

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 Answers2

0

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);                
       });
   }
Kia Kaha
  • 1,565
  • 1
  • 17
  • 39
  • That's a good point! I should not call it every time. I use an implementation of the MSAL library for login. I should go with the second approach. But when I call the useFetchPermissions() method inside the login() method, then it complains about "hook cannot be called from a function or component that is not a React function or hook." An example of calling the useFetchPermissions() from the login() method would be great. Please see updated question. – Ananth George Sep 23 '20 at 18:44
  • That's how hooks work, you can call them only at the top level of the component - but you don't really need a hook in this case in my opinion. If you are making the call just once upon login then you won't reuse the logic on other places and a hook is of no use. So just extract the `dataProvider` call from the `useEffect` hook and add it to your `login()` pipeline. Does it make sense? – Kia Kaha Sep 24 '20 at 07:51
  • 1
    Yes makes sense. I was slowly digging my way(https://marmelab.com/react-admin/doc/2.8/Authentication.html) to the answer and you hit the nail on the head. Yes the best way to do it is calling it the dataProvider directly from the login() pipeline. Implementing it now, will accept the answer in sometime. Thank you! That was very helpful. – Ananth George Sep 25 '20 at 23:45
0

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