diff --git a/.env b/.env index f6fd4d0..8f03a83 100644 --- a/.env +++ b/.env @@ -1,6 +1,4 @@ -REACT_APP_USE_AUTHENTICATION=true - -REACT_APP_API_GATEWAY=api/gateway -REACT_APP_WS_GATEWAY=ws/gateway - EXTEND_ESLINT=true + +REACT_APP_API_GATEWAY=/api/gateway +REACT_APP_WS_GATEWAY=/ws/gateway diff --git a/.env.development b/.env.development deleted file mode 100644 index 1451cfd..0000000 --- a/.env.development +++ /dev/null @@ -1,3 +0,0 @@ -REACT_APP_USE_AUTHENTICATION=false - -REACT_APP_SRV_STUDY_URI=study-server diff --git a/src/components/app-top-bar.tsx b/src/components/app-top-bar.tsx index ce55984..5643785 100644 --- a/src/components/app-top-bar.tsx +++ b/src/components/app-top-bar.tsx @@ -10,8 +10,7 @@ import { LIGHT_THEME, logout, TopBar } from '@gridsuite/commons-ui'; import Parameters, { useParameterState } from './parameters'; import { APP_NAME, PARAM_LANGUAGE, PARAM_THEME } from '../utils/config-params'; import { useDispatch, useSelector } from 'react-redux'; -import { fetchAppsAndUrls, fetchVersion } from '../utils/rest-api'; -import { getServersInfos } from '../rest/study'; +import { AppsMetadataSrv, StudySrv } from '../services'; import { useNavigate } from 'react-router-dom'; import { ReactComponent as PowsyblLogo } from '../images/powsybl_logo.svg'; import AppPackage from '../../package.json'; @@ -31,7 +30,7 @@ const AppTopBar: FunctionComponent = (props) => { const dispatch = useDispatch(); const [appsAndUrls, setAppsAndUrls] = useState< - Awaited> + Awaited> >([]); const theme = useSelector((state: AppState) => state[PARAM_THEME]); @@ -45,7 +44,7 @@ const AppTopBar: FunctionComponent = (props) => { useEffect(() => { if (props.user !== null) { - fetchAppsAndUrls().then((res) => { + AppsMetadataSrv.fetchAppsAndUrls().then((res) => { setAppsAndUrls(res); }); } @@ -73,9 +72,11 @@ const AppTopBar: FunctionComponent = (props) => { user={props.user} appsAndUrls={appsAndUrls} globalVersionPromise={() => - fetchVersion().then((res) => res?.deployVersion) + AppsMetadataSrv.fetchVersion().then( + (res) => res?.deployVersion + ) } - additionalModulesPromise={getServersInfos} + additionalModulesPromise={StudySrv.getServersInfos} onThemeClick={handleChangeTheme} theme={themeLocal} onLanguageClick={handleChangeLanguage} diff --git a/src/components/app.tsx b/src/components/app.tsx index 256a967..9031113 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -26,7 +26,6 @@ import { AuthenticationRouter, CardErrorBoundary, getPreLoginPath, - initializeAuthenticationDev, initializeAuthenticationProd, useSnackMessage, } from '@gridsuite/commons-ui'; @@ -37,14 +36,13 @@ import { } from '../redux/actions'; import { AppState } from '../redux/reducer'; import { + ConfigSrv, ConfigParameter, ConfigParameters, - connectNotificationsWsUpdateConfig, - fetchAuthorizationCodeFlowFeatureFlag, - fetchConfigParameter, - fetchConfigParameters, - fetchValidateUser, -} from '../utils/rest-api'; + UserAdminSrv, + AppsMetadataSrv, +} from '../services'; +import { connectNotificationsWsUpdateConfig } from '../utils/rest-api'; import { UserManager } from 'oidc-client'; import { APP_NAME, @@ -109,8 +107,10 @@ const App: FunctionComponent = () => { const ws = connectNotificationsWsUpdateConfig(); ws.onmessage = function (event) { let eventData = JSON.parse(event.data); - if (eventData.headers && eventData.headers['parameterName']) { - fetchConfigParameter(eventData.headers['parameterName']) + if (eventData?.headers?.parameterName) { + ConfigSrv.fetchConfigParameter( + eventData.headers.parameterName + ) .then((param) => updateParams([param])) .catch((error) => snackError({ @@ -132,58 +132,40 @@ const App: FunctionComponent = () => { path: '/silent-renew-callback', }) ); - - const [initialMatchSigninCallbackUrl] = useState( + const [initialMatchSignInCallbackUrl] = useState( useMatch({ path: '/sign-in-callback', }) ); - const initialize: () => Promise = useCallback(() => { - if (process.env.REACT_APP_USE_AUTHENTICATION === 'true') { - return fetchAuthorizationCodeFlowFeatureFlag().then( - (authorizationCodeFlowEnabled) => - initializeAuthenticationProd( - dispatch, - initialMatchSilentRenewCallbackUrl != null, - fetch('idpSettings.json'), - fetchValidateUser, - authorizationCodeFlowEnabled, - initialMatchSigninCallbackUrl != null - ) - ); - } else { - return initializeAuthenticationDev( - dispatch, - initialMatchSilentRenewCallbackUrl != null, - () => - new Promise((resolve) => - window.setTimeout(() => resolve(true), 500) - ), - initialMatchSigninCallbackUrl != null - ); - } - // Note: initialMatchSilentRenewCallbackUrl and dispatch don't change - }, [ - initialMatchSilentRenewCallbackUrl, - dispatch, - initialMatchSigninCallbackUrl, - ]); - useEffect(() => { - initialize() + AppsMetadataSrv.fetchAuthorizationCodeFlowFeatureFlag() + .then((authorizationCodeFlowEnabled) => + initializeAuthenticationProd( + dispatch, + initialMatchSilentRenewCallbackUrl != null, + fetch('idpSettings.json'), + UserAdminSrv.fetchValidateUser, + authorizationCodeFlowEnabled, + initialMatchSignInCallbackUrl != null + ) + ) .then((userManager: UserManager | undefined) => { setUserManager({ instance: userManager || null, error: null }); }) .catch((error: any) => { setUserManager({ instance: null, error: error.message }); }); - // Note: initialize and initialMatchSilentRenewCallbackUrl won't change - }, [initialize, initialMatchSilentRenewCallbackUrl, dispatch]); + // Note: initialize and initialMatchSilentRenewCallbackUrl & initialMatchSignInCallbackUrl won't change + }, [ + dispatch, + initialMatchSilentRenewCallbackUrl, + initialMatchSignInCallbackUrl, + ]); useEffect(() => { if (user !== null) { - fetchConfigParameters(COMMON_APP_NAME) + ConfigSrv.fetchConfigParameters(COMMON_APP_NAME) .then((params) => updateParams(params)) .catch((error) => snackError({ @@ -192,7 +174,7 @@ const App: FunctionComponent = () => { }) ); - fetchConfigParameters(APP_NAME) + ConfigSrv.fetchConfigParameters(APP_NAME) .then((params) => updateParams(params)) .catch((error) => snackError({ diff --git a/src/components/parameters.tsx b/src/components/parameters.tsx index 2401ccf..f5f5b68 100644 --- a/src/components/parameters.tsx +++ b/src/components/parameters.tsx @@ -28,7 +28,7 @@ import { Typography, } from '@mui/material'; import { CSSObject, Theme } from '@emotion/react'; -import { updateConfigParameter } from '../utils/rest-api'; +import { ConfigSrv } from '../services'; import { useSnackMessage } from '@gridsuite/commons-ui'; import { AppState } from '../redux/reducer'; import { TypographyTypeMap } from '@mui/material/Typography/Typography'; @@ -65,7 +65,7 @@ export function useParameterState< const handleChangeParamLocalState = useCallback( (value: any) => { setParamLocalState(value); - updateConfigParameter(paramName, value).catch((error) => { + ConfigSrv.updateConfigParameter(paramName, value).catch((error) => { setParamLocalState(paramGlobalState); snackError({ messageTxt: error.message, diff --git a/src/services/apps-metadata.ts b/src/services/apps-metadata.ts new file mode 100644 index 0000000..ba87af7 --- /dev/null +++ b/src/services/apps-metadata.ts @@ -0,0 +1,55 @@ +import { ReqResponse } from '../utils/rest-api'; + +export type EnvJson = typeof import('../../public/env.json'); + +function fetchEnv(): Promise { + return fetch('/env.json').then((res: ReqResponse) => res.json()); +} + +export function fetchAuthorizationCodeFlowFeatureFlag(): Promise { + console.info(`Fetching authorization code flow feature flag...`); + return fetchEnv() + .then((env: EnvJson) => + fetch(`${env.appsMetadataServerUrl}/authentication.json`) + ) + .then((res: ReqResponse) => res.json()) + .then((res: Record) => { + console.log( + `Authorization code flow is ${ + res.authorizationCodeFlowFeatureFlag + ? 'enabled' + : 'disabled' + }` + ); + return res.authorizationCodeFlowFeatureFlag; + }) + .catch((error) => { + console.error(error); + console.warn( + `Something wrong happened when retrieving authentication.json: authorization code flow will be disabled` + ); + return false; + }); +} + +export function fetchVersion(): Promise> { + console.info(`Fetching global metadata...`); + return fetchEnv() + .then((env: EnvJson) => + fetch(`${env.appsMetadataServerUrl}/version.json`) + ) + .then((response: ReqResponse) => response.json()) + .catch((reason) => { + console.error(`Error while fetching the version : ${reason}`); + return reason; + }); +} + +export function fetchAppsAndUrls(): Promise>> { + console.info(`Fetching apps and urls...`); + return fetchEnv() + .then((env: EnvJson) => + fetch(`${env.appsMetadataServerUrl}/apps-metadata.json`) + ) + .then((response: ReqResponse) => response.json()); +} diff --git a/src/services/config.ts b/src/services/config.ts new file mode 100644 index 0000000..f120d7d --- /dev/null +++ b/src/services/config.ts @@ -0,0 +1,42 @@ +import { getAppName } from '../utils/config-params'; +import { backendFetch, backendFetchJson } from '../utils/rest-api'; + +const PREFIX_CONFIG_QUERIES = `${process.env.REACT_APP_API_GATEWAY}/config`; + +export type ConfigParameter = { + //TODO check with config-server swagger + name: string; + value: any; + [propertiesName: string]: unknown; //temporary +}; +export type ConfigParameters = Array; +export function fetchConfigParameters( + appName: string +): Promise { + console.info(`Fetching UI configuration params for app : ${appName}`); + const fetchParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters`; + return backendFetchJson(fetchParams); +} + +export function fetchConfigParameter( + name: string +): ReturnType { + const appName = getAppName(name); + console.info(`Fetching UI config parameter '${name}' for app '${appName}'`); + const fetchParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters/${name}`; + return backendFetchJson(fetchParams); +} + +export function updateConfigParameter( + name: string, + value: Parameters[0] +): ReturnType { + const appName = getAppName(name); + console.info( + `Updating config parameter '${name}=${value}' for app '${appName}'` + ); + const updateParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters/${name}?value=${encodeURIComponent( + value + )}`; + return backendFetch(updateParams, { method: 'put' }); +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..a953828 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,24 @@ +import * as Config from './config'; +import * as AppsMetadata from './apps-metadata'; +import * as Study from './study'; +import * as UserAdmin from './user-admin'; + +const _ = { + ...Config, + ...AppsMetadata, + ...Study, + ...UserAdmin, +}; +export default _; + +export * as ConfigSrv from './config'; +export type * from './config'; + +export * as AppsMetadataSrv from './apps-metadata'; +export type * from './apps-metadata'; + +export * as StudySrv from './study'; +export type * from './study'; + +export * as UserAdminSrv from './user-admin'; +export type * from './user-admin'; diff --git a/src/rest/study.ts b/src/services/study.ts similarity index 80% rename from src/rest/study.ts rename to src/services/study.ts index d1143af..240333d 100644 --- a/src/rest/study.ts +++ b/src/services/study.ts @@ -6,11 +6,7 @@ */ import { backendFetchJson, Token } from '../utils/rest-api'; -const API_URL = - '/api/' + - (process.env.REACT_APP_USE_AUTHENTICATION === 'true' - ? `${process.env.REACT_APP_API_GATEWAY}/study/v1` - : `${process.env.REACT_APP_SRV_STUDY_URI}/v1`); +const STUDY_URL = `${process.env.REACT_APP_API_GATEWAY}/study/v1`; //TODO delete when commons-ui will be in typescript export type ServerAbout = { @@ -22,7 +18,7 @@ export type ServerAbout = { export function getServersInfos(token: Token): Promise { return backendFetchJson( - `${API_URL}/servers/about`, + `${STUDY_URL}/servers/about`, { headers: { Accept: 'application/json', diff --git a/src/services/user-admin.ts b/src/services/user-admin.ts new file mode 100644 index 0000000..2067823 --- /dev/null +++ b/src/services/user-admin.ts @@ -0,0 +1,33 @@ +import { backendFetch, ReqResponse } from '../utils/rest-api'; + +const USER_ADMIN_URL = `${process.env.REACT_APP_API_GATEWAY}/user-admin`; + +export function fetchValidateUser(user: Record): Promise { + const sub = user?.profile?.sub; + if (!sub) { + return Promise.reject( + new Error( + `Fetching access for missing user.profile.sub : ${JSON.stringify( + user + )}` + ) + ); + } + + console.info(`Fetching access for user...`); + const CheckAccessUrl = `${USER_ADMIN_URL}/v1/users/${sub}`; + console.debug(CheckAccessUrl); + + return backendFetch(CheckAccessUrl, { method: 'head' }, user?.id_token) + .then((response: ReqResponse) => { + //if the response is ok, the responseCode will be either 200 or 204 otherwise it's an HTTP error and it will be caught + return response.status === 200; + }) + .catch((error) => { + if (error.status === 403) { + return false; + } else { + throw error; + } + }); +} diff --git a/src/setupProxy.js b/src/setupProxy.js index e074a4a..3711a13 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.js @@ -2,18 +2,13 @@ const { createProxyMiddleware } = require('http-proxy-middleware'); module.exports = function (app) { app.use( createProxyMiddleware('http://localhost:9000/api/gateway', { - pathRewrite: { '^/api/gateway/': '/' }, + pathRewrite: { [`^${process.env.REACT_APP_API_GATEWAY}`]: '/' }, }) ); app.use( createProxyMiddleware('http://localhost:9000/ws/gateway', { - pathRewrite: { '^/ws/gateway/': '/' }, + pathRewrite: { [`^${process.env.REACT_APP_WS_GATEWAY}`]: '/' }, ws: true, }) ); - app.use( - createProxyMiddleware('http://localhost:5001/api/study-server', { - pathRewrite: { '^/api/study-server/': '/' }, - }) - ); }; diff --git a/src/utils/rest-api.ts b/src/utils/rest-api.ts index 6fbe7c2..b8acb73 100644 --- a/src/utils/rest-api.ts +++ b/src/utils/rest-api.ts @@ -5,13 +5,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { APP_NAME, getAppName } from './config-params'; +import { APP_NAME } from './config-params'; import { store } from '../redux/store'; import ReconnectingWebSocket, { Event } from 'reconnecting-websocket'; import { AppState } from '../redux/reducer'; -export type EnvJson = typeof import('../../public/env.json'); - export interface ErrorWithStatus extends Error { status?: number; } @@ -21,24 +19,8 @@ export type InitRequest = Partial[1]>; //Partial>; -const PREFIX_USER_ADMIN_SERVER_QUERIES = `${process.env.REACT_APP_API_GATEWAY}/user-admin`; - -// If you want to use user-admin-server in dev mode you must avoid passing through gateway -// and use the user-admin-server directly. SetupProxy should allow this. -// const PREFIX_USER_ADMIN_SERVER_QUERIES = -// process.env.REACT_APP_API_PREFIX + -// (process.env.REACT_APP_USE_AUTHENTICATION === 'true' -// ? `${process.env.REACT_APP_API_GATEWAY}/user-admin` -// : process.env.REACT_APP_USER_ADMIN_URI); - -const PREFIX_CONFIG_QUERIES = `${process.env.REACT_APP_API_GATEWAY}/config`; const PREFIX_CONFIG_NOTIFICATION_WS = `${process.env.REACT_APP_WS_GATEWAY}/config-notification`; -function getToken(): Token { - const state: AppState = store.getState(); - return state.user?.id_token; -} - export function connectNotificationsWsUpdateConfig(): ReconnectingWebSocket { const webSocketBaseUrl = document.baseURI .replace(/^http:\/\//, 'ws://') @@ -56,6 +38,11 @@ export function connectNotificationsWsUpdateConfig(): ReconnectingWebSocket { return reconnectingWebSocket; } +function getToken(): Token { + const state: AppState = store.getState(); + return state.user?.id_token; +} + function parseError(text: string): any { try { return JSON.parse(text); @@ -85,7 +72,7 @@ function handleError(response: ReqResponse): Promise { } function prepareRequest(init?: InitRequest, token?: Token): RequestInit { - if (!(typeof init == 'undefined' || typeof init == 'object')) { + if (!(typeof init === 'undefined' || typeof init === 'object')) { throw new TypeError( `Argument 2 of backendFetch is not an object ${typeof init}` ); @@ -130,121 +117,3 @@ export function backendFetchJson( safeResponse.json() ); } - -export function fetchValidateUser(user: Record): Promise { - const sub = user?.profile?.sub; - if (!sub) { - return Promise.reject( - new Error( - `Error : Fetching access for missing user.profile.sub : ${user}` - ) - ); - } - - console.info(`Fetching access for user...`); - const CheckAccessUrl = `${PREFIX_USER_ADMIN_SERVER_QUERIES}/v1/users/${sub}`; - console.debug(CheckAccessUrl); - - return backendFetch(CheckAccessUrl, { method: 'head' }, user?.id_token) - .then((response: ReqResponse) => { - //if the response is ok, the responseCode will be either 200 or 204 otherwise it's a Http error and it will be caught - return response.status === 200; - }) - .catch((error) => { - if (error.status === 403) { - return false; - } else { - throw error; - } - }); -} - -function fetchEnv(): Promise { - return fetch('env.json').then((res: ReqResponse) => res.json()); -} - -export function fetchAuthorizationCodeFlowFeatureFlag(): Promise { - console.info(`Fetching authorization code flow feature flag...`); - return fetchEnv() - .then((env: EnvJson) => - fetch(`${env.appsMetadataServerUrl}/authentication.json`) - ) - .then((res: ReqResponse) => res.json()) - .then((res: Record) => { - console.log( - `Authorization code flow is ${ - res.authorizationCodeFlowFeatureFlag - ? 'enabled' - : 'disabled' - }` - ); - return res.authorizationCodeFlowFeatureFlag; - }) - .catch((error) => { - console.error(error); - console.warn( - `Something wrong happened when retrieving authentication.json: authorization code flow will be disabled` - ); - return false; - }); -} - -export function fetchVersion(): Promise> { - console.info(`Fetching global metadata...`); - return fetchEnv() - .then((env: EnvJson) => - fetch(`${env.appsMetadataServerUrl}/version.json`) - ) - .then((response: ReqResponse) => response.json()) - .catch((reason) => { - console.error(`Error while fetching the version : ${reason}`); - return reason; - }); -} - -export function fetchAppsAndUrls(): Promise>> { - console.info(`Fetching apps and urls...`); - return fetchEnv() - .then((env: EnvJson) => - fetch(`${env.appsMetadataServerUrl}/apps-metadata.json`) - ) - .then((response: ReqResponse) => response.json()); -} - -export type ConfigParameter = { - //TODO check with config-server swagger - name: string; - value: any; - [propertiesName: string]: unknown; //temporary -}; -export type ConfigParameters = Array; -export function fetchConfigParameters( - appName: string -): Promise { - console.info(`Fetching UI configuration params for app : ${appName}`); - const fetchParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters`; - return backendFetchJson(fetchParams); -} - -export function fetchConfigParameter( - name: string -): ReturnType { - const appName = getAppName(name); - console.info(`Fetching UI config parameter '${name}' for app '${appName}'`); - const fetchParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters/${name}`; - return backendFetchJson(fetchParams); -} - -export function updateConfigParameter( - name: string, - value: Parameters[0] -): ReturnType { - const appName = getAppName(name); - console.info( - `Updating config parameter '${name}=${value}' for app '${appName}'` - ); - const updateParams = `${PREFIX_CONFIG_QUERIES}/v1/applications/${appName}/parameters/${name}?value=${encodeURIComponent( - value - )}`; - return backendFetch(updateParams, { method: 'put' }); -}