diff --git a/apps/assisted-ui/README.md b/apps/assisted-ui/README.md index 5f7871eb67..96c34900f8 100644 --- a/apps/assisted-ui/README.md +++ b/apps/assisted-ui/README.md @@ -96,7 +96,7 @@ $ podman build -t quay.io/edge-infrastructure/assisted-installer-ui:latest . --b You can run the standalone UI with chatbot enabled. You need to be logged in via `ocm`. ``` -$ AIUI_CHAT_API_URL= AIUI_OCM_TOKEN=$(ocm token) yarn start:assisted_ui +$ AIUI_CHAT_API_URL= OCM_REFRESH_TOKEN=$(ocm token --refresh) AIUI_SSO_API_URL=https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token yarn start:assisted_ui ``` ## Available Scripts diff --git a/apps/assisted-ui/deploy/nginx.conf b/apps/assisted-ui/deploy/nginx.conf index 97aeacfec8..8bb6fd1652 100644 --- a/apps/assisted-ui/deploy/nginx.conf +++ b/apps/assisted-ui/deploy/nginx.conf @@ -15,7 +15,7 @@ location /api { } location /chatbot/ { - proxy_pass $AIUI_CHAT_API_URL/; + proxy_pass $AIUI_CHAT_API_URL; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; @@ -28,6 +28,18 @@ location /chatbot/ { client_max_body_size 2M; } +location /token { + proxy_pass $AIUI_SSO_API_URL; + proxy_http_version 1.1; + proxy_cache_bypass $http_upgrade; + proxy_connect_timeout 120; + proxy_send_timeout 120; + proxy_read_timeout 120; + send_timeout 120; + client_max_body_size 2M; + proxy_ssl_server_name on; +} + location / { try_files $uri /index.html; } \ No newline at end of file diff --git a/apps/assisted-ui/deploy/start.sh b/apps/assisted-ui/deploy/start.sh index 47a53985b0..b3d0ecf2f3 100755 --- a/apps/assisted-ui/deploy/start.sh +++ b/apps/assisted-ui/deploy/start.sh @@ -2,17 +2,18 @@ set -eo pipefail export ASSISTED_SERVICE_URL="${ASSISTED_SERVICE_URL:-'http://localhost:8090'}" -export AIUI_CHAT_API_URL="${AIUI_CHAT_API_URL:-'http://localhost:1234'}" +export AIUI_CHAT_API_URL="${AIUI_CHAT_API_URL:-'http://localhost:1234/'}" +export AIUI_SSO_API_URL="${AIUI_SSO_API_URL:-'http://localhost:1235'}" # shellcheck disable=SC2016 -envsubst '$ASSISTED_SERVICE_URL $AIUI_CHAT_API_URL' < /deploy/nginx.conf > "$NGINX_DEFAULT_CONF_PATH/nginx.conf" +envsubst '$ASSISTED_SERVICE_URL $AIUI_CHAT_API_URL $AIUI_SSO_API_URL' < /deploy/nginx.conf > "$NGINX_DEFAULT_CONF_PATH/nginx.conf" if [ "$ASSISTED_SERVICE_SCHEME" = "https" ]; then # shellcheck disable=SC2016 envsubst '${HTTPS_CERT_FILE} ${HTTPS_KEY_FILE}' < /deploy/nginx_ssl.conf > "${NGINX_CONF_PATH}" fi -envsubst '$AIUI_OCM_TOKEN' < $NGINX_APP_ROOT/src/env.template.js > $NGINX_APP_ROOT/src/env.js +envsubst '$AIUI_OCM_REFRESH_TOKEN' < $NGINX_APP_ROOT/src/env.template.js > $NGINX_APP_ROOT/src/env.js # Do not listen on IPv6 if it's not enabled in the hardware if grep 'ipv6.disable=1' /proc/cmdline; then diff --git a/apps/assisted-ui/env.template.js b/apps/assisted-ui/env.template.js index 1043339cb6..7c44279c89 100644 --- a/apps/assisted-ui/env.template.js +++ b/apps/assisted-ui/env.template.js @@ -1 +1 @@ -window.OCM_TOKEN = '${AIUI_OCM_TOKEN}'; +window.OCM_REFRESH_TOKEN = '${AIUI_OCM_REFRESH_TOKEN}'; diff --git a/apps/assisted-ui/public/env.js b/apps/assisted-ui/public/env.js index 0a26df6185..57756c25cb 100644 --- a/apps/assisted-ui/public/env.js +++ b/apps/assisted-ui/public/env.js @@ -1 +1 @@ -window.OCM_TOKEN = ''; +window.OCM_REFRESH_TOKEN = ''; diff --git a/apps/assisted-ui/src/components/App.tsx b/apps/assisted-ui/src/components/App.tsx index 11f26071cc..6f807997f9 100755 --- a/apps/assisted-ui/src/components/App.tsx +++ b/apps/assisted-ui/src/components/App.tsx @@ -4,7 +4,7 @@ import { CompatRouter, Route } from 'react-router-dom-v5-compat'; import { Page } from '@patternfly/react-core'; import * as OCM from '@openshift-assisted/ui-lib/ocm'; import { Header } from './Header'; -import ChatBot, { getOcmToken } from './Chatbot'; +import ChatBot, { refreshToken } from './Chatbot'; import '../i18n'; const { HostsClusterDetailTabMock, UILibRoutes, Features, Config } = OCM; @@ -16,7 +16,7 @@ export const App: React.FC = () => ( } isManagedSidebar defaultManagedSidebarIsOpen={false}> : undefined} + additionalComponents={refreshToken ? : undefined} > } /> diff --git a/apps/assisted-ui/src/components/Chatbot.tsx b/apps/assisted-ui/src/components/Chatbot.tsx index f1c1a50851..76da6b8e0b 100644 --- a/apps/assisted-ui/src/components/Chatbot.tsx +++ b/apps/assisted-ui/src/components/Chatbot.tsx @@ -4,16 +4,55 @@ import '@patternfly-6/react-core/dist/styles/base.css'; import '@patternfly-6/chatbot/dist/css/main.css'; import '@patternfly-6/patternfly/dist/patternfly-addons.css'; -export const getOcmToken = () => - (import.meta.env.AIUI_OCM_TOKEN as string | undefined) || window.OCM_TOKEN; +export const refreshToken = + (import.meta.env.AIUI_OCM_REFRESH_TOKEN as string | undefined) || window.OCM_REFRESH_TOKEN; +let expiration = Date.now(); +let token = ''; + +export const getOcmToken = async () => { + // if token expires in less than 5s, refresh it + if (Date.now() - 5000 > expiration) { + if (!refreshToken) { + throw new Error('No refresh token available'); + } + const params = new URLSearchParams(); + params.append('grant_type', 'refresh_token'); + params.append('refresh_token', refreshToken || ''); + params.append('client_id', 'cloud-services'); + + try { + const response = await fetch('/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: params.toString(), + }); + + if (!response.ok) { + throw new Error(`Token refresh failed: ${response.status}`); + } + + const data = (await response.json()) as { access_token: string; expires_in: number }; + token = data.access_token; + expiration = Date.now() + data.expires_in * 1000; + } catch (error) { + // eslint-disable-next-line + console.error('Failed to refresh token:', error); + throw error; + } + } + return token; +}; const ChatBot = () => { const onApiCall: ChatBotWindowProps['onApiCall'] = async (input, init) => { + const token = await getOcmToken(); return fetch(`/chatbot${input.toString()}`, { ...(init || {}), headers: { ...(init?.headers || {}), - Authorization: `Bearer ${getOcmToken() || ''}`, + Authorization: `Bearer ${token}`, }, }); }; diff --git a/apps/assisted-ui/src/main.tsx b/apps/assisted-ui/src/main.tsx index 8fa15c2b66..bc3db78852 100755 --- a/apps/assisted-ui/src/main.tsx +++ b/apps/assisted-ui/src/main.tsx @@ -4,7 +4,7 @@ import { App } from './components/App'; declare global { interface Window { - OCM_TOKEN?: string; + OCM_REFRESH_TOKEN?: string; } } diff --git a/apps/assisted-ui/vite.config.ts b/apps/assisted-ui/vite.config.ts index 425d529f3d..141fb2274b 100644 --- a/apps/assisted-ui/vite.config.ts +++ b/apps/assisted-ui/vite.config.ts @@ -53,6 +53,11 @@ export default defineConfig(async ({ mode }) => { changeOrigin: true, rewrite: (path: string) => path.replace(/^\/chatbot/, ''), }, + '/token': { + target: env.AIUI_SSO_API_URL, + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/token/, ''), + }, }, }, };