diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 1c54f80d..b8c96916 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -78,6 +78,11 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - build-args: ${{ matrix.service == 'cab-standalone-frontend' && format('VITE_COGNITIVE_TOKEN={0}', secrets.VITE_COGNITIVE_TOKEN) || '' }} + # VITE_* values are baked into the bundle at build time. VITE_POWERGRID_SIMU + # is the same-origin base the frontend posts "apply action" to; without it the + # POST lands on / and nginx answers 405. Blank lines are ignored by the action. + build-args: | + ${{ matrix.service == 'cab-standalone-frontend' && format('VITE_COGNITIVE_TOKEN={0}', secrets.VITE_COGNITIVE_TOKEN) || '' }} + ${{ matrix.service == 'cab-standalone-frontend' && 'VITE_POWERGRID_SIMU=/powergrid-simu' || '' }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/config/dev/cab-standalone/.env.example b/config/dev/cab-standalone/.env.example index 04b75ccb..825583ac 100644 --- a/config/dev/cab-standalone/.env.example +++ b/config/dev/cab-standalone/.env.example @@ -12,14 +12,15 @@ HOST_IP=172.17.0.1 # Server : http://192.168.208.61:5000/api/v1/recommendation (LAN) # Public : https://interactiveagent.passerelle.irt-systemx.fr/api/v1/recommendation RL_AGENT_API_URL=http://host.docker.internal:5123/api/v1/recommendation -RL_AGENT_API_TOKEN=619e707da0981a48d9982019d996f98e20bdd885eab8b20ae05b4ef0f2e95097 - +RL_AGENT_API_TOKEN= # PowerGrid simulator upstream for the frontend's /powergrid-simu/ nginx proxy. # The frontend keeps VITE_POWERGRID_SIMU=/powergrid-simu (same-origin, no CORS); # only this upstream changes per environment: # Local dev : http://host.docker.internal:5122/ (simulator container on the host) # LAN : http://192.168.208.61:5100/ -# (Public/k8s uses nginx-kubernetes.conf via the helm chart, not this variable.) +# Public : https://interactivepowergrid.passerelle.irt-systemx.fr/ +# (Public/k8s deploys get this location from deploy-chart/configmap-assistant-platform.yaml, +# not from this variable.) POWERGRID_SIMU_UPSTREAM=http://host.docker.internal:5122/ # VITE build-time variables (also required in frontend/env/.env.local for local dev) diff --git a/config/dev/cab-standalone/nginx-cors-permissive.conf b/config/dev/cab-standalone/nginx-cors-permissive.conf index 24577d55..2b4dc8bd 100644 --- a/config/dev/cab-standalone/nginx-cors-permissive.conf +++ b/config/dev/cab-standalone/nginx-cors-permissive.conf @@ -344,10 +344,16 @@ server { # PowerGrid simulator "apply action" proxy. The upstream is environment-specific # and injected at container start from $POWERGRID_SIMU_UPSTREAM (see start-webui.sh): - # local -> http://host.docker.internal:5122/ LAN -> http://192.168.208.61:5100/ + # local -> http://host.docker.internal:5122/ + # LAN -> http://192.168.208.61:5100/ + # public -> https://interactivepowergrid.passerelle.irt-systemx.fr/ + # No explicit Host header: nginx defaults it to $proxy_host (the authority from + # proxy_pass), which is what an ip:port upstream AND a vhost upstream both need. + # Forwarding $http_host instead would make the passerelle route to the wrong vhost. location /powergrid-simu/ { - proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $remote_addr; + proxy_ssl_server_name on; + proxy_ssl_verify off; proxy_pass __POWERGRID_SIMU_UPSTREAM__; } diff --git a/deploy-chart/configmap-assistant-platform.yaml b/deploy-chart/configmap-assistant-platform.yaml index 895d2bc5..3a06da54 100644 --- a/deploy-chart/configmap-assistant-platform.yaml +++ b/deploy-chart/configmap-assistant-platform.yaml @@ -262,6 +262,19 @@ data: proxy_set_header X-Forwarded-For $remote_addr; } + # PowerGrid (grid2op) simulator "apply action" proxy. Keeps the browser POST + # same-origin (no CORS) — the frontend is built with + # VITE_POWERGRID_SIMU=/powergrid-simu and posts to /powergrid-simu/api/v1/recommendations. + # Host must be the upstream vhost, not $http_host: the passerelle routes by hostname. + location /powergrid-simu/ { + add_header Cache-Control "no-cache"; + proxy_set_header Host interactivepowergrid.passerelle.irt-systemx.fr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_ssl_server_name on; + proxy_ssl_verify off; + proxy_pass https://interactivepowergrid.passerelle.irt-systemx.fr/; + } + error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; diff --git a/deploy-chart/values.ovh.yaml b/deploy-chart/values.ovh.yaml index e6dc00cd..9da2074b 100644 --- a/deploy-chart/values.ovh.yaml +++ b/deploy-chart/values.ovh.yaml @@ -2,25 +2,25 @@ cabcontext: image: repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-context pullPolicy: Always - tag: "1.3.6" + tag: "1.3.7" cabevent: image: repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-event pullPolicy: Always - tag: "1.3.6" + tag: "1.3.7" cabhistoric: image: repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-historic pullPolicy: Always - tag: "1.3.6" + tag: "1.3.7" cabrecommendation: image: repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-recommendation pullPolicy: Always - tag: "1.3.6" + tag: "1.3.7" extraEnv: - name: RL_AGENT_API_URL value: "https://interactiveagent.passerelle.irt-systemx.fr/api/v1/recommendation" @@ -34,10 +34,20 @@ cabcapitalization: image: repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-capitalization pullPolicy: Always - tag: "1.3.6" + tag: "1.3.7" frontend: image: repository: harbor.irtsysx.fr/docker-proxy-cache/irtsystemx/interactiveai-cab-standalone-frontend pullPolicy: Always - tag: "1.3.6" + tag: "1.3.7" + extraEnv: + - name: VITE_POWERGRID_SIMU + value: "/powergrid-simu" + - name: POWERGRID_SIMU_UPSTREAM + value: "https://interactivepowergrid.passerelle.irt-systemx.fr/" + - name: VITE_COGNITIVE_TOKEN + valueFrom: + secretKeyRef: + name: cab-frontend + key: vite-cognitive-token diff --git a/frontend/default.conf b/frontend/default.conf index 1e81cc41..c7a9f6d2 100644 --- a/frontend/default.conf +++ b/frontend/default.conf @@ -39,10 +39,14 @@ server { # Proxy for the PowerGrid (grid2op) simulator — keeps the "apply" POST same-origin # so the browser never does a cross-origin request (avoids CORS entirely). # Frontend posts to /powergrid-simu/... with VITE_POWERGRID_SIMU=/powergrid-simu. - # NB: this upstream is a LAN address reachable from cab-standalone; adjust per environment. + # NB: this upstream is a LAN address reachable from cab-standalone; adjust per environment + # (public deployments use https://interactivepowergrid.passerelle.irt-systemx.fr/). + # No explicit Host header: nginx defaults it to $proxy_host, which is correct for an + # ip:port upstream and for a vhost upstream behind the passerelle alike. location /powergrid-simu/ { - proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $remote_addr; + proxy_ssl_server_name on; + proxy_ssl_verify off; proxy_pass http://192.168.208.61:5100/; } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 99727e7a..edc6bd3c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -19,8 +19,13 @@ import Modal from './components/atoms/Modal.vue' import Navbar from './components/molecules/Navbar.vue' import { mode } from './plugins/colorMode' import { useAppStore } from './stores/app' +import { useAuthStore } from './stores/auth' mode.value = 'auto' const appStore = useAppStore() + +// A session restored from localStorage has no refresh timer running yet; arm it +// so a reloaded tab keeps renewing its token instead of dying at the next poll. +useAuthStore().scheduleRefresh() diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts index fa5a28fd..37164375 100644 --- a/frontend/src/api/auth.ts +++ b/frontend/src/api/auth.ts @@ -14,6 +14,25 @@ export function login(username: string, password: string) { ) } +/** + * Exchanges the refresh token for a fresh access token. Client credentials are + * injected by the nginx proxy as a Basic auth header (same as `login`), so only + * the grant and the refresh token travel in the body. + */ +export function refreshToken(refresh_token: string) { + return http.post( + '/auth/token', + new URLSearchParams({ + refresh_token, + grant_type: 'refresh_token', + clientId: 'opfab-client' + }), + // A dead refresh token is expected: the caller turns it into a session + // expiry, so it must not raise a generic error popup of its own. + { _silent: true } + ) +} + export function checkToken(token: string) { return http.post<{ active: boolean }>( '/auth/check_token', diff --git a/frontend/src/api/cards.ts b/frontend/src/api/cards.ts index a5fb140a..ebcea3de 100644 --- a/frontend/src/api/cards.ts +++ b/frontend/src/api/cards.ts @@ -3,6 +3,7 @@ import i18n from '@/plugins/i18n' import { useAppStore } from '@/stores/app' import { useAuthStore } from '@/stores/auth' import type { Card, CardEvent } from '@/types/cards' +import { handleSessionExpired } from '@/utils/session' let controller: AbortController = new AbortController() @@ -15,7 +16,8 @@ export async function subscribe( rangeStart?: string notification?: 'true' | 'false' }, - handler: (card: CardEvent) => void + handler: (card: CardEvent) => void, + retried = false ) { const authStore = useAuthStore() const appStore = useAppStore() @@ -35,6 +37,13 @@ export async function subscribe( signal: controller.signal } ) + // This request bypasses the axios interceptors, so the token dance lives here + if (response.status === 401) { + if (!retried && (await authStore.refresh())) return subscribe(config, handler, true) + appStore.status.notifications.state = 'OFFLINE' + handleSessionExpired() + return response + } const reader = response.body!.getReader() const decoder = new TextDecoder('utf-8') // eslint-disable-next-line no-constant-condition diff --git a/frontend/src/entities/ATM/CAB/Context.vue b/frontend/src/entities/ATM/CAB/Context.vue index 14b20887..3f87ea19 100644 --- a/frontend/src/entities/ATM/CAB/Context.vue +++ b/frontend/src/entities/ATM/CAB/Context.vue @@ -19,12 +19,11 @@ const servicesStore = useServicesStore() const mapStore = useMapStore() const appStore = useAppStore() -const contextPID = ref(0) const faulty = ref(false) onBeforeMount(async () => { locale.value = `en-ATM` - contextPID.value = await servicesStore.getContext('ATM', (context: { data: ContextType }) => { + await servicesStore.getContext('ATM', (context: { data: ContextType }) => { // New context data: iterate over the airplanes array // 1- Clear last tick's markers and ROUTE waypoints mapStore.removeCategoryWaypoint('ROUTE') @@ -108,6 +107,6 @@ onBeforeMount(async () => { onUnmounted(() => { locale.value = window.navigator.language.split('-')[0] || import.meta.env.VITE_DEFAULT_LOCALE || 'en' - clearInterval(contextPID.value) + servicesStore.stopContext() }) diff --git a/frontend/src/entities/PowerGrid/CAB/Context.vue b/frontend/src/entities/PowerGrid/CAB/Context.vue index 619c209c..0d8cebda 100644 --- a/frontend/src/entities/PowerGrid/CAB/Context.vue +++ b/frontend/src/entities/PowerGrid/CAB/Context.vue @@ -40,7 +40,7 @@