Our pure JavaScript Scheduler component


Post by roberto »

Hi all,

Calls to our backend require the Authorization header to be filled with an access token. We do this using EventStore's onBeforeRequest callback.

An issue we're running into, is that the token can't be retrieved synchronously; our auth mechanism (MSAL) usually needs to do an AJAX call to refresh the token.

Therefore it would be nice if onBeforeRequest accepted a Promise as its return value, so any backend calls triggered by EventStore will wait until the Promise (i.e. the token) is resolved.

See below how we currently work around it. It's not ideal, as it doesn't play nice with hot reloads. We can maybe use a ref to further work around that.

Questions:

  1. Is there already a better way to do this?
  2. @ Bryntum: can onBeforeRequest be changed to accept a Promise?

Thanks,
Roberto


const MyScheduler = ({ authToken }: { authToken: string }) => {
  const onBeforeRequest = useCallback(
    (event: any) => {
      (event.source as AjaxStore).setConfig({
        headers: { Authorization: `Bearer ${authToken}` },
      });
      return event;
    },
    [authToken]
  );

  const eventStore = useMemo(
    () =>
      new EventStore({
        // ...
        onBeforeRequest,
      }),
    [onBeforeRequest]
  );

  // Memoized to avoid re-rendering when our custom modal opens
  const schedulerConfig: BryntumSchedulerProps = useMemo(() => {
    return {
      eventStore,
    };
  }, [eventStore]);

  return (
    <>
      <BryntumScheduler {...schedulerConfig} />
    </>
  );
};

/**
 * Hook which intends to provide a synchronous way to
 * get a valid MSAL (oAuth) token at all times,
 * dynamically updating in the background.
 */
function useMsalToken() {
  const { instance } = useMsal();
  const [token, setToken] = useState<string | undefined>(undefined);

  const acquireToken = useCallback(async () => {
    const account = instance.getActiveAccount();
    if (!account) return;

    const request = { scopes: msalScopes, account };

    try {
      const response = await instance.acquireTokenSilent(request);
      setToken(response.idToken);
    } catch (error) {
      console.error("useMsalToken error: ", error);
    }
  }, [instance]);

  useEffect(() => {
    acquireToken().catch((e) => {
      console.error("Error acquiring token in background", e);
    });
    const intervalId = window.setInterval(acquireToken, 1000 * 60); // Refresh every minute
    return () => window.clearInterval(intervalId);
  }, [acquireToken]);

  return token;
}

const MsalTokenWrapper = ({
  children,
}: {
  children: (data: { token: string }) => React.ReactNode;
}) => {
  const token = useMsalToken();

  if (!token) return <>Waiting for auth token...</>;

  return children({ token });
};

const MySchedulerWrapped = (
  <MsalTokenWrapper>
    {({ token }) => <MyScheduler authToken={token} />}
  </MsalTokenWrapper>
);

Post by nickolay »

Hi,

onBeforeRequest is a built-in "event handler" for the https://bryntum.com/products/gantt/docs/api/Core/data/Store#event-beforeRequest event. That event is synchronous, so onBeforeRequest is synchronous too. Actually we don't have a concept of asynchronous event handlers, so making onBeforeRequest async probably is not feasible.

As of better way of doing this - the only thing I can think of is to subclass the EventStore and override async sendLoadRequest() method:

class MyEventStore extends EventStore {
    async sendLoadRequest(resolve, reject) {
        await prepareMSAL()
        return super.sendLoadRequest(resolve, reject)
    }
}

However, this is a private method, so its not a good practice.

Internally, sendLoadRequest uses https://bryntum.com/products/gantt/docs/api/Core/helper/AjaxHelper#function-get-static, so one can also override that. AjaxHelper however is shared for all requests, so one would need to distinguish the requests of event store somehow - feels cumbersome.


Post by roberto »

Thank you, this worked! Definitely much more concise. Sharing my implementation below.

At some point though, this could stop working.
Do you know if there is any outstanding feature request for this? Else I'll create one.

// app/index.tsx
const msalInstance = createMsalInstance(...);
export const getMsalAccessToken = () => getAccessToken(msalInstance);

// app/.../MyScheduler.tsx
class MyEventStore extends EventStore {
  // @ts-ignore
  async sendLoadRequest(resolve, reject) {
    const token = await getMsalAccessToken();
    this.setConfig({
      headers: { Authorization: `Bearer ${token?.idToken}` },
    });
    // @ts-ignore
    return super.sendLoadRequest(resolve, reject);
  }
}

const MyScheduler = () => {
  const eventStore = useMemo(
    () =>
      new MyEventStore({
        // ...
      }),
    []
  );

  // Memoized to avoid re-rendering when our custom modal opens
  const schedulerConfig: BryntumSchedulerProps = useMemo(() => {
    return {
      // ...
      eventStore,
    };
  }, [eventStore]);

  return (
    <>
      <BryntumScheduler {...schedulerConfig} />
    </>
  );
};

Post by nickolay »

Glad it works, yw! As far as I know, there's no such request, created one: https://github.com/bryntum/support/issues/10199


Post Reply