diff --git a/src/fireedge/src/client/components/DebugLog/ansiHtml.js b/src/fireedge/src/client/components/DebugLog/ansiHtml.js index 8b3cc9856a..1a72f8855d 100644 --- a/src/fireedge/src/client/components/DebugLog/ansiHtml.js +++ b/src/fireedge/src/client/components/DebugLog/ansiHtml.js @@ -80,7 +80,6 @@ export default function ansiHTML(text) { if (ot) { // If current sequence has been opened, close it. if (~ansiCodes.indexOf(seq)) { - // eslint-disable-line no-extra-boolean-cast ansiCodes.pop() return '' diff --git a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/index.js b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/index.js index 3c1e80e385..2de22e3a98 100644 --- a/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/index.js +++ b/src/fireedge/src/client/components/Forms/VmTemplate/CreateForm/Steps/ExtraConfiguration/index.js @@ -87,7 +87,6 @@ const Content = ({ data, setFormData }) => { ...section, name, label: , - // eslint-disable-next-line react/display-name renderContent: () => ( ), diff --git a/src/fireedge/src/client/components/HOC/AuthLayout.js b/src/fireedge/src/client/components/HOC/AuthLayout.js index 4d8464bc7c..f84ee88ac8 100644 --- a/src/fireedge/src/client/components/HOC/AuthLayout.js +++ b/src/fireedge/src/client/components/HOC/AuthLayout.js @@ -51,6 +51,7 @@ const AuthLayout = ({ subscriptions = [], children }) => { return () => { endpoints.forEach((endpoint) => { endpoint.unsubscribe() + endpoint.abort() }) } }, [dispatch, jwt]) diff --git a/src/fireedge/src/client/components/Header/index.js b/src/fireedge/src/client/components/Header/index.js index 3b7cb51dc1..82c97119ed 100644 --- a/src/fireedge/src/client/components/Header/index.js +++ b/src/fireedge/src/client/components/Header/index.js @@ -39,7 +39,7 @@ import { sentenceCase } from 'client/utils' const Header = () => { const { isOneAdmin } = useAuth() const { fixMenu } = useGeneralApi() - const { appTitle, title, isBeta } = useGeneral() + const { appTitle, title, isBeta, withGroupSwitcher } = useGeneral() const appAsSentence = useMemo(() => sentenceCase(appTitle), [appTitle]) return ( @@ -104,7 +104,7 @@ const Header = () => { > - {!isOneAdmin && } + {!isOneAdmin && withGroupSwitcher && } diff --git a/src/fireedge/src/client/constants/index.js b/src/fireedge/src/client/constants/index.js index 3a674cbe3a..0e696b9879 100644 --- a/src/fireedge/src/client/constants/index.js +++ b/src/fireedge/src/client/constants/index.js @@ -30,6 +30,7 @@ export const BY = { export const _APPS = defaultApps export const APPS = Object.keys(defaultApps) export const APPS_IN_BETA = [_APPS.sunstone.name] +export const APPS_WITH_SWITCHER = [_APPS.sunstone.name] export const APP_URL = defaultAppName ? `/${defaultAppName}` : '' export const WEBSOCKET_URL = `${APP_URL}/websockets` export const STATIC_FILES_URL = `${APP_URL}/client/assets` @@ -52,6 +53,7 @@ export const LANGUAGES_URL = `${STATIC_FILES_URL}/languages` export const ONEADMIN_ID = '0' export const SERVERADMIN_ID = '1' +export const ONEADMIN_GROUP_ID = '0' export const FILTER_POOL = { PRIMARY_GROUP_RESOURCES: '-4', diff --git a/src/fireedge/src/client/constants/translates.js b/src/fireedge/src/client/constants/translates.js index adf6d03f55..7ae685c3f3 100644 --- a/src/fireedge/src/client/constants/translates.js +++ b/src/fireedge/src/client/constants/translates.js @@ -197,6 +197,8 @@ module.exports = { /* errors */ SessionExpired: 'Sorry, your session has expired', + OnlyForOneadminGroup: + 'Only members of the oneadmin group can access OneProvision functionality', SomethingWrong: 'Something go wrong', CannotConnectOneFlow: 'Cannot connect to OneFlow server', CannotConnectOneProvision: 'Cannot connect to OneProvision server', diff --git a/src/fireedge/src/client/containers/Login/index.js b/src/fireedge/src/client/containers/Login/index.js index 22ed1bd469..309357725b 100644 --- a/src/fireedge/src/client/containers/Login/index.js +++ b/src/fireedge/src/client/containers/Login/index.js @@ -44,9 +44,9 @@ function Login() { const classes = loginStyles() const isMobile = useMediaQuery((theme) => theme.breakpoints.only('xs')) + const { logout } = useAuthApi() const { error: otherError, isLoginInProgress: needGroupToContinue } = useAuth() - const { logout } = useAuthApi() const [changeAuthGroup, changeAuthGroupState] = useChangeAuthGroupMutation() const [login, loginState] = useLoginMutation() diff --git a/src/fireedge/src/client/dev/_app.js b/src/fireedge/src/client/dev/_app.js deleted file mode 100644 index e42daeeb5c..0000000000 --- a/src/fireedge/src/client/dev/_app.js +++ /dev/null @@ -1,51 +0,0 @@ -/* ------------------------------------------------------------------------- * - * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); you may * - * not use this file except in compliance with the License. You may obtain * - * a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * ------------------------------------------------------------------------- */ -import { ReactElement } from 'react' - -import SunstoneApp from 'client/apps/sunstone' -import ProvisionApp from 'client/apps/provision' -import LoadingScreen from 'client/components/LoadingScreen' - -import { isDevelopment, isBackend } from 'client/utils' -import { _APPS, APPS } from 'client/constants' - -/** - * Render App by url: http:///fireedge/. - * - * @param {object} props - Props from server - * @returns {ReactElement} Returns App - */ -const DevelopmentApp = (props) => { - let appName = '' - - if (isDevelopment() && !isBackend()) { - appName = window.location.pathname - .split(/\//gi) - .filter((sub) => sub?.length > 0) - .find((resource) => APPS.includes(resource)) - } - - return ( - { - [_APPS.provision.name]: , - [_APPS.sunstone.name]: , - }[appName] ?? - ) -} - -DevelopmentApp.displayName = 'DevelopmentApp' - -export default DevelopmentApp diff --git a/src/fireedge/src/client/dev/index.js b/src/fireedge/src/client/dev/index.js deleted file mode 100644 index 0df11b1e93..0000000000 --- a/src/fireedge/src/client/dev/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* ------------------------------------------------------------------------- * - * Copyright 2002-2021, OpenNebula Project, OpenNebula Systems * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); you may * - * not use this file except in compliance with the License. You may obtain * - * a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - * ------------------------------------------------------------------------- */ -import { render } from 'react-dom' - -import { createStore } from 'client/store' -import App from 'client/dev/_app' - -const { store } = createStore({ initState: window.__PRELOADED_STATE__ }) - -render(, document.getElementById('root')) diff --git a/src/fireedge/src/client/features/AuthApi/index.js b/src/fireedge/src/client/features/AuthApi/index.js index 61d59013a5..f8482065cb 100644 --- a/src/fireedge/src/client/features/AuthApi/index.js +++ b/src/fireedge/src/client/features/AuthApi/index.js @@ -13,39 +13,45 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import { createApi } from '@reduxjs/toolkit/query/react' +import { Actions, Commands } from 'server/routes/api/auth/routes' import { dismissSnackbar } from 'client/features/General/actions' import { actions } from 'client/features/Auth/slice' import userApi from 'client/features/OneApi/user' -import { storage } from 'client/utils' -import { APP_URL, JWT_NAME, FILTER_POOL, ONEADMIN_ID } from 'client/constants' +import http from 'client/utils/rest' +import { requestConfig, storage } from 'client/utils' +import { JWT_NAME, FILTER_POOL, ONEADMIN_ID } from 'client/constants' const { ALL_RESOURCES, PRIMARY_GROUP_RESOURCES } = FILTER_POOL const authApi = createApi({ reducerPath: 'authApi', - baseQuery: fetchBaseQuery({ - baseUrl: `${APP_URL}/api/`, - prepareHeaders: (headers, { getState }) => { - const token = getState().auth.jwt + baseQuery: async ({ params, command, needState }, { getState, signal }) => { + try { + const config = requestConfig(params, command) + const response = await http.request({ ...config, signal }) + const state = needState ? getState() : {} - // If we have a token set in state, - // let's assume that we should be passing it. - token && headers.set('authorization', `Bearer ${token}`) + return { data: response.data ?? {}, meta: { state } } + } catch (axiosError) { + const { message, data = {}, status, statusText } = axiosError + const { message: messageFromServer, data: errorFromOned } = data - return headers - }, - }), + const error = message ?? errorFromOned ?? messageFromServer ?? statusText + + return { error: { status: status, data: error } } + } + }, endpoints: (builder) => ({ getAuthUser: builder.query({ /** * @returns {object} Information about authenticated user * @throws Fails when response isn't code 200 */ - query: () => ({ url: 'user/info' }), - transformResponse: (response) => response?.data?.USER, + query: () => ({ command: { path: '/user/info' } }), + transformResponse: (response) => response?.USER, async onQueryStarted(_, { queryFulfilled, dispatch }) { try { const { data: user } = await queryFulfilled @@ -55,23 +61,31 @@ const authApi = createApi({ }), login: builder.mutation({ /** - * @param {object} data - User credentials - * @param {string} data.user - Username - * @param {string} data.token - Password - * @param {boolean} [data.remember] - Remember session - * @param {string} [data.token2fa] - Token for Two factor authentication + * Login in the interface. + * + * @param {object} params - User credentials + * @param {string} params.user - Username + * @param {string} params.token - Password + * @param {boolean} [params.remember] - Remember session + * @param {string} [params.token2fa] - Token for Two factor authentication * @returns {object} Response data from request * @throws Fails when response isn't code 200 */ - query: (data) => ({ url: 'auth', method: 'POST', body: data }), - transformResponse: (response) => { - const { id, token } = response?.data + query: (params) => { + const name = Actions.AUTHENTICATION + const command = { name, ...Commands[name] } + + return { params, command, needState: true } + }, + transformResponse: (response, meta) => { + const { id, token } = response + const { withGroupSwitcher } = meta?.state?.general ?? {} const isOneAdmin = id === ONEADMIN_ID return { jwt: token, user: { ID: id }, - isLoginInProgress: !!token && !isOneAdmin, + isLoginInProgress: withGroupSwitcher && !!token && !isOneAdmin, } }, async onQueryStarted({ remember }, { queryFulfilled, dispatch }) { diff --git a/src/fireedge/src/client/features/General/slice.js b/src/fireedge/src/client/features/General/slice.js index f9c20023bb..bb15ef2bb4 100644 --- a/src/fireedge/src/client/features/General/slice.js +++ b/src/fireedge/src/client/features/General/slice.js @@ -18,13 +18,14 @@ import { createSlice } from '@reduxjs/toolkit' import { actions as authActions } from 'client/features/Auth/slice' import * as actions from 'client/features/General/actions' import { generateKey } from 'client/utils' -import { APPS_IN_BETA } from 'client/constants' +import { APPS_IN_BETA, APPS_WITH_SWITCHER } from 'client/constants' const initial = { zone: 0, title: null, appTitle: null, isBeta: false, + withGroupSwitcher: false, isLoading: false, isFixMenu: false, @@ -56,10 +57,12 @@ const { name, reducer } = createSlice({ ...state, title: payload, })) - .addCase(actions.changeAppTitle, (state, { payload }) => { - const isBeta = APPS_IN_BETA?.includes(String(payload).toLowerCase()) + .addCase(actions.changeAppTitle, (state, { payload: appTitle }) => { + const lowerAppTitle = String(appTitle).toLowerCase() + const isBeta = APPS_IN_BETA?.includes(lowerAppTitle) + const withGroupSwitcher = APPS_WITH_SWITCHER?.includes(lowerAppTitle) - return { ...state, appTitle: payload, isBeta } + return { ...state, appTitle, isBeta, withGroupSwitcher } }) .addCase(actions.changeZone, (state, { payload }) => ({ ...state, diff --git a/src/fireedge/src/client/features/OneApi/index.js b/src/fireedge/src/client/features/OneApi/index.js index 5ddaba709c..0e6c69e26d 100644 --- a/src/fireedge/src/client/features/OneApi/index.js +++ b/src/fireedge/src/client/features/OneApi/index.js @@ -99,12 +99,7 @@ const oneApi = createApi({ }) ) - return { - error: { - status: status, - data: message ?? data?.data ?? statusText, - }, - } + return { error: { status: status, data: error } } } }, refetchOnMountOrArgChange: 30, diff --git a/src/fireedge/src/client/features/middleware.js b/src/fireedge/src/client/features/middleware.js index d3b1cb330c..1c2fd202a5 100644 --- a/src/fireedge/src/client/features/middleware.js +++ b/src/fireedge/src/client/features/middleware.js @@ -14,8 +14,9 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ import { isRejectedWithValue, Middleware, Dispatch } from '@reduxjs/toolkit' -import { actions } from 'client/features/Auth/slice' -import { T } from 'client/constants' + +import * as Auth from 'client/features/Auth/slice' +import { T, ONEADMIN_GROUP_ID } from 'client/constants' /** * @param {{ dispatch: Dispatch }} params - Redux parameters @@ -26,7 +27,27 @@ export const unauthenticatedMiddleware = (next) => (action) => { if (isRejectedWithValue(action) && action.payload.status === 401) { - dispatch(actions.logout(T.SessionExpired)) + dispatch(Auth.actions.logout(T.SessionExpired)) + } + + return next(action) + } + +/** + * @param {{ dispatch: Dispatch, getState: function():object }} params - Redux parameters + * @returns {Middleware} - Middleware to logout when user isn't in oneadmin group + */ +export const onlyForOneadminMiddleware = + ({ dispatch, getState }) => + (next) => + (action) => { + const groups = getState()?.[Auth.name]?.user?.GROUPS?.ID + + if (!Auth.actions.logout.match(action) && groups) { + const ensuredGroups = [groups].flat() + + !ensuredGroups.includes(ONEADMIN_GROUP_ID) && + dispatch(Auth.actions.logout(T.OnlyForOneadminGroup)) } return next(action) diff --git a/src/fireedge/src/client/provision.js b/src/fireedge/src/client/provision.js index d62708eacd..7b188f2a16 100644 --- a/src/fireedge/src/client/provision.js +++ b/src/fireedge/src/client/provision.js @@ -17,8 +17,12 @@ import { hydrate, render } from 'react-dom' import { createStore } from 'client/store' import App from 'client/apps/provision' +import { onlyForOneadminMiddleware } from 'client/features/middleware' -const { store } = createStore({ initState: window.__PRELOADED_STATE__ }) +const { store } = createStore({ + initState: window.__PRELOADED_STATE__, + extraMiddleware: [onlyForOneadminMiddleware], +}) const rootHTML = document.getElementById('root')?.innerHTML const renderMethod = rootHTML !== '' ? hydrate : render diff --git a/src/fireedge/src/client/store/index.js b/src/fireedge/src/client/store/index.js index decb2bae74..becbe1df83 100644 --- a/src/fireedge/src/client/store/index.js +++ b/src/fireedge/src/client/store/index.js @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * * limitations under the License. * * ------------------------------------------------------------------------- */ -import { configureStore, EnhancedStore } from '@reduxjs/toolkit' +import { configureStore, Middleware, EnhancedStore } from '@reduxjs/toolkit' import { setupListeners } from '@reduxjs/toolkit/query/react' import { isDevelopment } from 'client/utils' @@ -27,9 +27,10 @@ import { unauthenticatedMiddleware } from 'client/features/middleware' /** * @param {object} props - Props * @param {object} props.initState - Initial state + * @param {Middleware[]} props.extraMiddleware - Extra middleware to apply on store * @returns {{ store: EnhancedStore }} Configured Redux Store */ -export const createStore = ({ initState = {} }) => { +export const createStore = ({ initState = {}, extraMiddleware = [] }) => { const store = configureStore({ reducer: { [Auth.name]: Auth.reducer, @@ -42,6 +43,7 @@ export const createStore = ({ initState = {} }) => { getDefaultMiddleware({ immutableCheck: true, }).concat([ + ...extraMiddleware, unauthenticatedMiddleware, authApi.middleware, oneApi.middleware, diff --git a/src/fireedge/src/server/routes/api/auth/routes.js b/src/fireedge/src/server/routes/api/auth/routes.js index bb5c9224ad..327ade9b68 100644 --- a/src/fireedge/src/server/routes/api/auth/routes.js +++ b/src/fireedge/src/server/routes/api/auth/routes.js @@ -17,10 +17,11 @@ const { httpMethod, from: fromData, -} = require('server/utils/constants/defaults') +} = require('../../../utils/constants/defaults') const { POST } = httpMethod const { postBody } = fromData + const basepath = '/auth' const AUTHENTICATION = 'authentication' diff --git a/src/fireedge/src/server/routes/entrypoints/App.js b/src/fireedge/src/server/routes/entrypoints/App.js index cae0d2b8b1..f5d6b6fdb9 100644 --- a/src/fireedge/src/server/routes/entrypoints/App.js +++ b/src/fireedge/src/server/routes/entrypoints/App.js @@ -45,32 +45,24 @@ languages.map((language) => const router = Router() router.get('*', (req, res) => { - let app = 'dev' - let title = 'FireEdge' + const defaultTitle = 'FireEdge' const context = {} let store = '' let component = '' let css = '' let storeRender = '' - // production - if ( - env && - (!env.NODE_ENV || (env.NODE_ENV && env.NODE_ENV !== defaultWebpackMode)) - ) { - const apps = Object.keys(defaultApps) - const parseUrl = req.url - .split(/\//gi) - .filter((sub) => sub && sub.length > 0) + const isProduction = + !env?.NODE_ENV || (env?.NODE_ENV && env?.NODE_ENV !== defaultWebpackMode) - parseUrl.forEach((element) => { - if (element && apps.includes(element)) { - app = element - title = element - } - }) + const apps = Object.keys(defaultApps) + const appName = req.url + .split(/\//gi) + .filter((sub) => sub?.length > 0) + .find((resource) => apps.includes(resource)) - const App = require(`../../../client/apps/${app}/index.js`).default + if (isProduction) { + const App = require(`../../../client/apps/${appName}/index.js`).default const sheets = new ServerStyleSheets() const composeEnhancer = (root && root.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose @@ -92,11 +84,11 @@ router.get('*', (req, res) => { - ${upperCaseFirst(title)} by OpenNebula - - - - + ${upperCaseFirst(appName ?? defaultTitle)} by OpenNebula + + + + @@ -106,7 +98,11 @@ router.get('*', (req, res) => {
${component}
${storeRender} - + ${ + isProduction + ? `` + : `` + } ` diff --git a/src/fireedge/webpack.config.dev.client.js b/src/fireedge/webpack.config.dev.client.js index 1c3ea720c4..99f40376bf 100644 --- a/src/fireedge/webpack.config.dev.client.js +++ b/src/fireedge/webpack.config.dev.client.js @@ -14,6 +14,12 @@ * limitations under the License. * * ------------------------------------------------------------------------- */ +const { + defaultWebpackMode, + defaultApps, + defaultAppName, +} = require('./src/server/utils/constants/defaults') + const getDevConfiguration = () => { try { const path = require('path') @@ -21,11 +27,6 @@ const getDevConfiguration = () => { const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin') const TimeFixPlugin = require('time-fix-plugin') - const { - defaultWebpackMode, - defaultAppName, - } = require('./src/server/utils/constants/defaults') - const appName = defaultAppName ? `/${defaultAppName}` : '' /** @type {webpack.Configuration} */ @@ -33,7 +34,9 @@ const getDevConfiguration = () => { mode: defaultWebpackMode, entry: [ 'webpack-hot-middleware/client', - path.resolve(__dirname, 'src/client/dev/index.js'), + ...Object.keys(defaultApps).map((app) => + path.resolve(__dirname, `src/client/${app}.js`) + ), ], output: { filename: 'bundle.dev.js',