uwrit / leaf

Leaf Clinical Data Explorer
https://www.youtube.com/watch?v=ZuKKC7B8mHI
Other
86 stars 47 forks source link

Serve Leaf from URL sub path #601

Open UCI-MIND-IT opened 8 months ago

UCI-MIND-IT commented 8 months ago

Hello,

Our institute is interested in deploying Leaf for multiple datasets/cohorts, and it seems like Leaf is built to be deployed at the root of a web domain. We only have a single physical location, and IP addresses are limited there, so it would be difficult for us to set up multiple dedicated domain names and IP addresses for each of our Leaf instances.

We do have a general-purpose website for researchers and we could feasibly deploy our Leaf instances as sub-paths within this website, but (let us know if we're mistaken) from what we could glean on documentation for building Leaf's front-end client and serving Leaf with Apache, Leaf does not support deployment behind a sub path.

For example, instead of serving Leaf from URLs that look like this:

leaf1.mind.uci.edu
leaf2.mind.uci.edu

Could Leaf be served from URLs that look like this?

site.mind.uci.edu/leaf1
site.mind.uci.edu/leaf2

We have previously deployed self-hosted apps that support this feature, such as Shlink (which involved recompiling their Node frontend web client with a small configuration tweak in package.json), and are wondering if something similar could be implemented for Leaf to improve its compatibility in different deployment environments.

Thank you for your time and consideration!

ndobb commented 8 months ago

Great, thanks for letting us know. Short answer: Yes, I believe Leaf should work out-of-the-box using a URL subpath. Without doing a new deployment with a subpath configured though it's difficult to say with 100% confidence.

Longer answer: the Leaf client uses the Axios library to make calls to the Leaf API. By design, the call paths are relative, e.g.:

export const getAuthConfig = async () => {
    const request = await Axios.get('/api/config');
    const config = request.data as AppConfig;
    return config;
};

Note we just use '/api/config', and let Axios determine the base URL to prepend. This works fine with subdomains. So the question is, how does Axios determine the base URL? (and would it include subpath?)

I'm not sure, but would guess the subpath would be included. I took a quick look through the Axios source code and docs, and didn't find how it is defined. I'm assuming the default base URL is window.location.href though, which would include the subpath. And thus should work just fine.

Please note this is assuming that your APIs would be served from paths like

site.mind.uci.edu/leaf1/api/
site.mind.uci.edu/leaf2/api/

Edit: I should add that if you found this didn't work, we'd want to support this case anyway and would be willing to make a change to the source code to get you up and running.

UCI-MIND-IT commented 8 months ago

We're serving the Leaf front-end with Apache on the same server as the API behind a sub path configured on a separate reverse proxy server (which proxies traffic to various other apps in our website). We were able to get some JSON data from /api/config when we visit https://site.mind.uci.edu/leaf/api/config, so the backend should be working as intended.

We think our issue is in the front-end UI client (or potentially how we're serving it): when we visit https://site.mind.uci.edu/leaf, no graphics load, and inspecting the page source reveals some hrefs and srcs to static resources that are invalid:

./images/logos/apps/leaf.svg -> https://site.mind.uci.edu/images/logos/apps/leaf.svg (404)
                     should be: https://site.mind.uci.edu/leaf/images/logos/apps/leaf.svg

./styles/custom.css -> https://site.mind.uci.edu/styles/custom.css (404)
            should be: https://site.mind.uci.edu/leaf/styles/custom.css

etc.

If we manually inject "leaf" into the correct position in these URLs, the resources load. Since the front-end doesn't construct the full web paths, nothing is rendered by the browser. For our specific case, we can probably work around these issues on the web server side, but being able to customize the UI client with a user-defined sub path could add some flexibility to how the UI client can be deployed. Our apologies if this is encroaching on support, do let us know if email would be a better channel from here on out and we can close this issue.

ndobb commented 8 months ago

Ah I see, thanks for the info. So this is not an issue with API, which appears to be working as intended.

The problem is instead the path the browser client assumes front-end resources (eg CSS, JS, and image files) would be found on the server 🧐. Got it.

I'm unfortunately not an Apache ninja myself and would need to ask my infrastructure colleagues if they have insight in this. Off the top of my head though (again, non-Apache expert here), could you add something like the below to the httpd.conf file?

...
Alias /images <document_root_on_server>/leaf/images/
Alias /styles <document_root_on_server>/leaf/styles/
...

My understanding is that the client requests could remain the same but Apache would route them. I could be wrong though.

UCI-MIND-IT commented 8 months ago

That would absolutely work! For our site, though, we'd prefer to keep the high-level paths as clean and direct as possible. If we decide to host an unrelated app in the future that needs a similar workaround, and if that app needs an Alias or access to the /images or /styles paths, there would be a conflict. I found an old serverfault post that goes into similar solutions for a similar problem.

In the meantime, we're working around this on the Apache side by using a separate <VirtualHost> with a new root domain instead of a <Location> block inside our main <VirtualHost>. Now we're compiling+serving the UI client on the reverse proxy server itself instead of serving the UI client from the API server. The API is still reachable from the reverse proxy server, so we just need to set up SSL and configure Shibboleth on the reverse proxy (and re-compile the API on the API server in Release mode instead of Debug mode).

ndobb commented 8 months ago

Got it, well I'm glad you have a workaround! This is an interesting problem. From the Leaf client app's perspective (written with create-react-app), there's no obvious means I can think of to change the path for loading CSS/JS/other files client-side. React dev does allow for proxying server calls, but that is (A) unfortunately not possible in production, and (B) even if it was possible, would affect API calls as well.

So it seems to me this almost certainly would need to be handled by nginx or Apache.

If you think of some change or optional configuration we could add to the source code to address this, please let us know.

UCI-MIND-IT commented 8 months ago

Unfortunately, our team isn't versed in JS/React/C#, so we aren't in a good position to make concrete suggestions, but thank you for hearing us out.

The high-level process would look something like this:

  1. A new configuration option could be added for compiling the UI client to add a prefix to the path (default value would be ""):
    
    # /src/ui-client/package.json

{ ... "homepage": "leaf", ... }

* The string should be sanitized internally with no leading `/` and a single trailing `/` - let users write `"/leaf"` or `"leaf/"` if they want to, it wouldn't matter
  * If a deeper sub path is needed, all slashes in this string could be checked during sanitization as well

2. While compiling the UI client with Node, local paths that refer to client-side resources would be edited to contain the sanitized string from `homepage`:

./styles/custom.css ^ leaf/


3. API traffic could still be proxied correctly on the server-side.
* Example in Apache: the line `<Location /api>` could be edited to be the resultant path while still proxying traffic to the server that hosts the API:

<Location /leaf/api> ProxyPass http://{node1-ip}:{node1-port}/api ProxyPassReverse http://{node1-ip}:{node1-port}/api

    <RequireAny>
        AuthType shibboleth

...


Although this may not work because the API server might then receive requests for `/leaf/api`, which leads to your point (B) where API calls would be affected by the change (we'd need to confirm this behavior). And if create-react-app doesn't support modifying URLs during compilation, then that's a dead end as well.
ndobb commented 8 months ago

Hmmm random aside while I meditate on the above: could you just modify your client build files on the Apache server to include a leaf directory, then put images, styles, etc. under that?

That way the client calls would be the same but actually match the paths on the server, I would think.

That may not be an ideal long-term solution (you'd need to do this every time you upgrade), but it would be relatively easy to automate (eg using a post-build script), or at least be quite simple even doing manually, I would think. Plus no need to fiddle with Apache configs.

Let me know if I'm getting my wires crossed or missing something.

Update: wait, wait, I clearly need more coffee this morning. That's the reverse of what you'd want, I think. Nevermind.