diff --git a/docker-compose.yml b/docker-compose.yml index 89cb36c..791dda9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -382,6 +382,19 @@ services: # installation commands; it is never passed as a build ARG. secrets: - github_token + # ragbio-ui (the frontend built by the ui-builder stage, which the + # `backend` target still depends on and COPYs dist/ from -- see the + # Dockerfile) bakes this in as import.meta.env.VITE_RAGBIO_API_KEY at + # build time (Vite env vars are compile-time, not runtime -- setting + # RAGBIO_API_KEY under `environment:` below only reaches the running + # backend process, never the already-built frontend bundle). Without + # this build arg the Dockerfile's `ARG RAGBIO_API_KEY` stays empty + # regardless of what .env holds, so every ragbio-ui request sends a + # literal "Authorization: Bearer undefined" and the backend correctly + # (if unhelpfully) reports "Not authenticated" -- confirmed missing + # here, not an nginx routing gap. + args: + RAGBIO_API_KEY: ${RAGBIO_API_KEY} restart: on-failure # image: ghcr.io/omnibioai/omnibioai-rag:latest # Frontend served at http://localhost:8090/ via nginx (port 5175 inside container) diff --git a/docker/nginx-router.conf b/docker/nginx-router.conf index bc39999..fd97c8e 100644 --- a/docker/nginx-router.conf +++ b/docker/nginx-router.conf @@ -271,7 +271,15 @@ server { index index.html; autoindex off; } - location ^~ /_svc/workbench { rewrite ^/_svc/workbench(/.*)$ $1 break; set $workbench_upstream workbench:8000; proxy_pass http://$workbench_upstream; } + # NOTE: `set` must come BEFORE `rewrite ... break;`, not after -- `break` + # stops processing of the *entire* current set of ngx_http_rewrite_module + # directives (that's `set` too, not just `rewrite`/`if`), so a `set` + # written after it in the same location never runs. Got this backwards + # on the first pass: `rewrite ...; set $x ...; proxy_pass http://$x;` + # silently left $workbench_upstream (and five siblings below) permanently + # empty -- "using uninitialized ... variable" / "invalid URL prefix in + # http://" / 500, confirmed live in the error log. + location ^~ /_svc/workbench { set $workbench_upstream workbench:8000; rewrite ^/_svc/workbench(/.*)$ $1 break; proxy_pass http://$workbench_upstream; } location ^~ /_svc/lims { # NOTE: this location defines its own proxy_set_header directives, which means # none of the server-level ones above are inherited (nginx behavior: a location @@ -281,8 +289,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $router_xfp; - rewrite ^/_svc/lims(/.*)$ $1 break; set $lims_upstream lims:7000; + rewrite ^/_svc/lims(/.*)$ $1 break; proxy_pass http://$lims_upstream; } # LIMS's session login (django.contrib.auth's LoginView/LogoutView, @@ -508,8 +516,8 @@ server { location ^~ /_svc/toolserver { rewrite ^/_svc/toolserver(/.*)$ $1 break; proxy_pass http://toolserver; } location ^~ /_svc/auth { limit_req zone=auth_limit burst=5 nodelay; - rewrite ^/_svc/auth(/.*)$ $1 break; set $auth_upstream auth-service:8001; + rewrite ^/_svc/auth(/.*)$ $1 break; proxy_pass http://$auth_upstream; } # Bare /auth/* — OAuth2 login/callback redirect targets registered with @@ -544,8 +552,8 @@ server { # /api/license/* traffic. location ^~ /api/license/ { limit_req zone=auth_limit burst=5 nodelay; - rewrite ^/api/license(/.*)$ $1 break; set $auth_upstream auth-service:8001; + rewrite ^/api/license(/.*)$ $1 break; proxy_pass http://$auth_upstream; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -704,16 +712,16 @@ server { location ^~ /ops/ { set $workbench_upstream workbench:8000; proxy_pass http://$workbench_upstream; } location ^~ /plugins/ { set $workbench_upstream workbench:8000; proxy_pass http://$workbench_upstream; } location ^~ /_svc/policy { - rewrite ^/_svc/policy/?(.*)$ /$1 break; set $policy_upstream policy-engine:8002; + rewrite ^/_svc/policy/?(.*)$ /$1 break; proxy_pass http://$policy_upstream; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; } location ^~ /_svc/hpc { - rewrite ^/_svc/hpc/?(.*)$ /$1 break; set $hpc_upstream hpc-policy-engine:8003; + rewrite ^/_svc/hpc/?(.*)$ /$1 break; proxy_pass http://$hpc_upstream; proxy_http_version 1.1; proxy_set_header Connection ""; diff --git a/src/ui/pages/Jobs.jsx b/src/ui/pages/Jobs.jsx index 29179ee..2ef1787 100644 --- a/src/ui/pages/Jobs.jsx +++ b/src/ui/pages/Jobs.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useRef } from "react"; -import { isElectron } from "../lib/session"; +import { isElectron, getToken } from "../lib/session"; function getHost() { return ( @@ -18,9 +18,19 @@ function tesUrl(path) { } async function apiFetch(path, opts = {}) { + // TES's own auth middleware requires a bearer token ({"detail":"missing + // bearer token"} on :8081) -- this fetch() is a normal same-origin XHR, + // not an iframe navigation (unlike RAG/Control Center), so it's fully + // able to attach a real header itself instead of relying on nginx to + // synthesize one from a cookie. Same pattern as rolesApi.js's request(). + const token = getToken(); const res = await fetch(tesUrl(path), { ...opts, - headers: { "Content-Type": "application/json", ...(opts.headers || {}) }, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(opts.headers || {}), + }, signal: opts.signal || AbortSignal.timeout(10000), }); if (!res.ok) {