farmOS.js in a module

I am working on a module for FarmOS that essentially loads a Vue.js app into the content section of the farmOS page. I would like to use farmOS.js in this Vue.app to access/modify the underlying database.

I have been successful in doing so, but I have had to provide farmOS.js with a username/password to authenticate with the farmOS server. This makes good sense for applications external to farmOS but seems unnecessary when running in a module on the server. The user has already authenticated with farmOS, so it would be nice to not have to ask then for a username/password again. I’m hoping someone has experience with this and can help.

Has anyone been successful in using farmOS.js within a Drupal module installed on a farmOS server? Anyone know of a module that has done so that I could study? Any thoughts on if it is possible or if it would require modifications to farmOS.js?

Any suggestions or pointers would be appreciated.

2 Likes

Oh interesting @braught! That makes perfect sense as a use-case (loading farmOS.js client-side from the farmOS server itself, which can leverage the existing user session cookie so no additional authentication is required). If it isn’t possible currently, it’s only because historically farmOS.js evolved alongside Field Kit, which is not served from the same server as farmOS, so authentication is always required. I wonder if farmOS.js would need any changes, or if there’s a way to do it that “just works”. Probably an easy question for @jgaehring to answer!

2 Likes

Bit of a tangent, but it also makes me think: should we include farmOS.js as a library packaged with farmOS itself? We do that already with farmOS-map.js.

There isn’t a direct need for farmOS.js in farmOS itself right now, and I probably wouldn’t argue for including it in core until there is, but perhaps we could start leveraging it more ourselves for JS-based client side UI elements in farmOS core over time.

And it would allow apps like Farm Data to reuse the library, rather than packaging it itself. If multiple apps are doing the same thing then farmOS.js ends up getting included multiple times, which leads to unnecessary duplication.

2 Likes

@braught I’m curious have you tried using farmOS.js but skipping the “Connect to a remote farmOS server” step described here? https://github.com/farmOS/farmOS/blob/97cf31b5bdaf4b8fa83d4f375be2201c86deb62c/composer.libraries.json#L9

I wonder if simply skipping the farm.remote.authorize(username, password); step would allow other functions to be used, or if they depend on a token being set.

1 Like

I’ve given that a try. The farmOS.js requests come back with 401 responses. I haven’t dug too deeply into it but it seems likely, as you suggest, that farmOS.js still tries to send the token but it is not set.

1 Like

Just skimming this convo and will try to follow up in more detail later, but way back when @paul121 and I implemented OAuth2, we made the OAuth stuff fairly pluggable. The main farmOS instance is two parts, a client and a model. The client is what does all the networks stuff. In turn, when the client is initialized (just using a factory function), it takes an optional AuthMixin as a part of its options parameter, which is itself just a callback function. It defaults to the OAuthMixin, but could be swapped out easily enough.

There are more details here, I believe:

I forget exactly how Paul was using that, but there’s a gist linked in there that may explain.

3 Likes

Excellent. I’ll check this out and see what I can do.

1 Like

@jgaehring Thanks again for the pointers. I have now been able to get this working :grin:

In case it is helpful to anyone later on here is what I’ve done.

To create the farmOS instance I pass in a different auth mixin as follows:

  const response = await fetch('http://farmos/session/token');
  const csrfToken = await response.text();

  // Similar to: https://gist.github.com/paul121/26bed0987b73c6886fa3a0743c0f47eb
  const config = {
    host: '',
    clientId: 'farm',
    auth: (request) => {
      request.interceptors.request.use((config) => {
        // Don't add CSRF  header to GET requests as it can leak the token.
        // https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
        if (config.method === 'get') {
          return {
            ...config,
            headers: {
              ...config.headers,
            },
          };
        } else {
          return {
            ...config,
            headers: {
              ...config.headers,
              'X-CSRF-TOKEN': csrfToken,
            },
          };
        }
      });
    },
  };

  const options = { remote: config };

  const farm = farmOS(options);
  await farm.schema.fetch();

Notes:

  • For GET requests, the auth mixin just returns the provided config and headers.
  • For other operations a CSRF token is required. So the X-CSRF-TOKEN is added to the header.
  • Because this code running on a page served from farmOS, the step to authorize with the remote via username/password not necessary.

Other code in the page can then create, delete and modify assets, logs, etc as expected, for example (using await here for simplicity):

  const p1 = {
    type: 'log--activity',
    name: 'did some stuff',
    notes: 'initial note',
  };
  console.log('create first log');
  const a1 = farm.log.create(p1);
  await farm.log.send(a1);
  console.log('done');

  console.log('delete first log');
  await farm.log.delete('activity', a1.id);
  console.log('done');

  const p2 = {
    type: 'log--activity',
    name: 'did some more stuff',
    notes: 'another note',
  };
  console.log('create a second log');
  const a2 = farm.log.create(p2);
  await farm.log.send(a2);
  console.log('done');

  console.log('Try to update log');
  const updateProps = {
    notes: 'An updated note',
    status: 'done',
  };
  const updateLog = farm.log.update(a2, updateProps);
  await farm.log.send(updateLog);
  console.log('done');
2 Likes