diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50784ee089..532c067586 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -74,6 +74,8 @@
highly-available when we have multiple Prometheus instances
(PR[#3573](https://github.com/scality/metalk8s/pull/3573))
+- Handle 401 unauthorized error in MetalK8s UI
+ (PR[#3640](https://github.com/scality/metalk8s/pull/3640))
## Bug fixes
- [#3601](https://github.com/scality/metalk8s/issues/3601) - Marks
diff --git a/shell-ui/package-lock.json b/shell-ui/package-lock.json
index 11e421e420..f8dd28127d 100644
--- a/shell-ui/package-lock.json
+++ b/shell-ui/package-lock.json
@@ -1958,8 +1958,8 @@
"dev": true
},
"@scality/core-ui": {
- "version": "github:scality/core-ui#877d6440521a3a3fcd00fb88d0ce7039bf9b221f",
- "from": "github:scality/core-ui#v0.24.2"
+ "version": "github:scality/core-ui#9f1d3de45649ee0c420b1a804b77d15cb2d9c363",
+ "from": "github:scality/core-ui#v0.25.0"
},
"@scality/module-federation": {
"version": "github:scality/module-federation#a4f1cf882646c8d859b9081dc8b66e5647962456",
diff --git a/shell-ui/package.json b/shell-ui/package.json
index 4fd890d4d4..0c5db2f04b 100644
--- a/shell-ui/package.json
+++ b/shell-ui/package.json
@@ -47,7 +47,7 @@
"webpack-dev-server": "^3.11.2"
},
"dependencies": {
- "@scality/core-ui": "github:scality/core-ui.git#v0.24.2",
+ "@scality/core-ui": "github:scality/core-ui.git#v0.25.0",
"@scality/module-federation": "github:scality/module-federation.git#1.0.0",
"oidc-client": "^1.11.3",
"oidc-react": "^1.1.5",
diff --git a/shell-ui/src/FederatedApp.jsx b/shell-ui/src/FederatedApp.jsx
index 102ddb7306..9b20ba44d6 100644
--- a/shell-ui/src/FederatedApp.jsx
+++ b/shell-ui/src/FederatedApp.jsx
@@ -37,11 +37,7 @@ import {
useConfigRetriever,
useDiscoveredViews,
} from './initFederation/ConfigurationProviders';
-import {
- Route,
- Switch,
- Router,
-} from 'react-router-dom';
+import { Route, Switch, Router } from 'react-router-dom';
import {
ShellConfigProvider,
useShellConfig,
@@ -203,13 +199,17 @@ function WithInitFederationProviders({ children }: { children: Node }) {
export default function App(): Node {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
}
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 08890d8c66..aadcb4ea02 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -4708,8 +4708,8 @@
}
},
"@scality/core-ui": {
- "version": "github:scality/core-ui#877d6440521a3a3fcd00fb88d0ce7039bf9b221f",
- "from": "github:scality/core-ui#v0.24.2"
+ "version": "github:scality/core-ui#9f1d3de45649ee0c420b1a804b77d15cb2d9c363",
+ "from": "github:scality/core-ui#v0.25.0"
},
"@scality/module-federation": {
"version": "github:scality/module-federation#a4f1cf882646c8d859b9081dc8b66e5647962456",
diff --git a/ui/package.json b/ui/package.json
index 013bb79eb7..e85355d1e0 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -9,7 +9,7 @@
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/react-fontawesome": "^0.1.14",
"@kubernetes/client-node": "github:scality/kubernetes-client-javascript.git#browser-0.10.2-63-g579d066",
- "@scality/core-ui": "github:scality/core-ui.git#v0.24.2",
+ "@scality/core-ui": "github:scality/core-ui.git#v0.25.0",
"@scality/module-federation": "github:scality/module-federation.git#1.0.0",
"axios": "^0.21.1",
"formik": "2.2.5",
diff --git a/ui/src/FederableApp.js b/ui/src/FederableApp.js
index 8b44f9c9d5..1968f3c795 100644
--- a/ui/src/FederableApp.js
+++ b/ui/src/FederableApp.js
@@ -18,13 +18,21 @@ import { useTypedSelector } from './hooks';
import { setHistory as setReduxHistory } from './ducks/history';
import { setApiConfigAction } from './ducks/config';
import { initToggleSideBarAction } from './ducks/app/layout';
+import { authErrorAction } from './ducks/app/authError';
+import { AuthError } from './services/errorhandler';
const composeEnhancers =
typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
-const sagaMiddleware = createSagaMiddleware();
+const sagaMiddleware = createSagaMiddleware({
+ onError: (error) => {
+ if (error instanceof AuthError) {
+ store.dispatch(authErrorAction());
+ }
+ },
+});
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware));
export const store = createStore(reducer, enhancer);
diff --git a/ui/src/containers/PrivateRoute.js b/ui/src/containers/PrivateRoute.js
index e93c2b8e57..004c5c024a 100644
--- a/ui/src/containers/PrivateRoute.js
+++ b/ui/src/containers/PrivateRoute.js
@@ -1,6 +1,7 @@
import React, { useMemo } from 'react';
import { Route } from 'react-router-dom';
-import { ErrorPage500, ErrorPageAuth } from '@scality/core-ui';
+import { useHistory } from 'react-router';
+import { ErrorPage500, ErrorPageAuth, ErrorPage401 } from '@scality/core-ui';
import { useTypedSelector } from '../hooks';
import { ComponentWithFederatedImports } from '@scality/module-federation';
import { useDispatch } from 'react-redux';
@@ -13,16 +14,28 @@ const InternalPrivateRoute = ({
...rest
}) => {
const { language, api } = useTypedSelector((state) => state.config);
+ const { isAuthError } = useTypedSelector((state) => state.app.authError);
const url_support = api?.url_support;
const { userData } = moduleExports['./auth/AuthProvider'].useAuth();
const dispatch = useDispatch();
+ const history = useHistory();
useMemo(() => {
dispatch(updateAPIConfigAction(userData));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [!userData]);
- if (userData.token && userData.username) {
+ if (isAuthError) {
+ return (
+ {
+ history.push('/');
+ }}
+ />
+ );
+ } else if (userData.token && userData.username) {
return ;
} else {
return (
diff --git a/ui/src/ducks/app/authError.js b/ui/src/ducks/app/authError.js
new file mode 100644
index 0000000000..d9f543ba32
--- /dev/null
+++ b/ui/src/ducks/app/authError.js
@@ -0,0 +1,29 @@
+//@flow
+export type AuthErrorState = {
+ isAuthError: boolean,
+};
+
+type AuthErrorAction = {
+ type: 'AUTH_ERROR',
+};
+
+const defaultState: AuthErrorState = {
+ isAuthError: false,
+};
+
+export default function reducer(
+ state: AuthErrorState = defaultState,
+ action: AuthErrorAction,
+) {
+ switch (action.type) {
+ case 'AUTH_ERROR': {
+ return { ...state, isAuthError: true };
+ }
+ default:
+ return { ...state };
+ }
+}
+
+export function authErrorAction() {
+ return { type: 'AUTH_ERROR' };
+}
diff --git a/ui/src/ducks/reducer.js b/ui/src/ducks/reducer.js
index 7b7951a18f..ff4f961cbb 100644
--- a/ui/src/ducks/reducer.js
+++ b/ui/src/ducks/reducer.js
@@ -9,6 +9,8 @@ import pods from './app/pods';
import type { PodsState } from './app/pods';
import volumes from './app/volumes';
import type { VolumesState } from './app/volumes';
+import authError from './app/authError';
+import type { AuthErrorState } from './app/authError';
import login from './login';
import type { LoginState } from './login';
import layout from './app/layout';
@@ -31,6 +33,7 @@ const rootReducer = combineReducers({
salt,
monitoring,
volumes,
+ authError,
}),
oidc: oidcReducer,
history: historyReducer,
@@ -49,6 +52,7 @@ export type RootState = {
layout: LayoutState,
salt: SaltState,
monitoring: MonitoringState,
+ authError: AuthErrorState,
},
};
diff --git a/ui/src/services/errorhandler.js b/ui/src/services/errorhandler.js
new file mode 100644
index 0000000000..46cfde5b36
--- /dev/null
+++ b/ui/src/services/errorhandler.js
@@ -0,0 +1,13 @@
+export class AuthError extends Error {}
+
+export function handleUnAuthorizedError({ error }) {
+ if (
+ error?.response?.statusCode === 401 ||
+ error?.response?.statusCode === 403 ||
+ error?.response?.status === 401 ||
+ error?.response?.status === 403
+ ) {
+ throw new AuthError();
+ }
+ return { error };
+}
diff --git a/ui/src/services/k8s/core.js b/ui/src/services/k8s/core.js
index 93272ba56e..0b8d85e684 100644
--- a/ui/src/services/k8s/core.js
+++ b/ui/src/services/k8s/core.js
@@ -1,10 +1,11 @@
+import { handleUnAuthorizedError } from '../errorhandler';
import { coreV1, appsV1 } from './api';
export async function getNodes() {
try {
return await coreV1.listNode();
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -12,7 +13,7 @@ export async function getPods() {
try {
return await coreV1.listPodForAllNamespaces();
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -25,7 +26,7 @@ export async function getKubeSystemNamespace() {
'metadata.name=kube-system',
);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -33,7 +34,7 @@ export async function createNode(payload) {
try {
return await coreV1.createNode(payload);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -57,7 +58,7 @@ export async function listNamespaces({
options,
);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -72,7 +73,7 @@ export async function queryPodInNamespace(namespace, podLabel) {
`app=${podLabel}`,
);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -86,7 +87,7 @@ export async function createNamespacedConfigMap(name, namespace, restProps) {
try {
return await coreV1.createNamespacedConfigMap(namespace, body);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -115,7 +116,7 @@ export async function patchNamespacedConfigMap(
{ headers: { 'Content-Type': cTypeHeader } },
);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -123,7 +124,7 @@ export async function getNamespacedDeployment(name, namespace) {
try {
return await appsV1.readNamespacedDeployment(name, namespace);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -131,7 +132,7 @@ export async function readNode(name) {
try {
return await coreV1.readNode(name);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -139,6 +140,6 @@ export async function readNamespacedConfigMap(nameConfigMap, namespace) {
try {
return await coreV1.readNamespacedConfigMap(nameConfigMap, namespace);
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
diff --git a/ui/src/services/k8s/volumes.js b/ui/src/services/k8s/volumes.js
index 7ad9430df2..7e42ddfcd1 100644
--- a/ui/src/services/k8s/volumes.js
+++ b/ui/src/services/k8s/volumes.js
@@ -1,4 +1,5 @@
//@flow
+import { handleUnAuthorizedError } from '../errorhandler';
import { coreV1, storage } from './api';
export async function getPersistentVolumes() {
@@ -8,7 +9,7 @@ export async function getPersistentVolumes() {
try {
return await coreV1.listPersistentVolume();
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -19,7 +20,7 @@ export async function getStorageClass() {
try {
return await storage.listStorageClass();
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
@@ -30,6 +31,6 @@ export async function getPersistentVolumeClaims() {
try {
return await coreV1.listPersistentVolumeClaimForAllNamespaces();
} catch (error) {
- return { error };
+ return handleUnAuthorizedError({ error });
}
}
diff --git a/ui/src/services/salt/api.js b/ui/src/services/salt/api.js
index 6d0dbc4609..a7b482b84b 100644
--- a/ui/src/services/salt/api.js
+++ b/ui/src/services/salt/api.js
@@ -1,6 +1,7 @@
//@flow
import { User } from 'oidc-client';
import ApiClient from '../ApiClient';
+import { handleUnAuthorizedError } from '../errorhandler';
let saltApiClient = null;
@@ -25,7 +26,7 @@ export type SaltToken = {
],
};
-export async function authenticate(user: User): Promise {
+export async function authenticate(user: User) {
if (!saltApiClient) {
throw new Error('Salt api client should be defined.');
}
@@ -34,14 +35,20 @@ export async function authenticate(user: User): Promise {
username: `oidc:${user.email}`,
token: user.token,
};
- return saltApiClient.post('/login', payload);
+
+ const result = await saltApiClient.post('/login', payload);
+ if (result.error) {
+ return handleUnAuthorizedError({ error: result.error });
+ } else {
+ return result;
+ }
}
export async function deployNode(node: string, version: string) {
if (!saltApiClient) {
throw new Error('Salt api client should be defined.');
}
- return saltApiClient.post('/', {
+ const result = saltApiClient.post('/', {
client: 'runner_async',
fun: 'state.orchestrate',
arg: ['metalk8s.orchestrate.deploy_node'],
@@ -50,17 +57,27 @@ export async function deployNode(node: string, version: string) {
pillar: { orchestrate: { node_name: node } },
},
});
+ if (result.error) {
+ return handleUnAuthorizedError({ error: result.error });
+ } else {
+ return result;
+ }
}
export async function printJob(jid: string) {
if (!saltApiClient) {
throw new Error('Salt api client should be defined.');
}
- return saltApiClient.post('/', {
+ const result = saltApiClient.post('/', {
client: 'runner',
fun: 'jobs.print_job',
arg: [jid],
});
+ if (result.error) {
+ return handleUnAuthorizedError({ error: result.error });
+ } else {
+ return result;
+ }
}
export type IPInterfaces = {
@@ -94,15 +111,13 @@ We may get error message instead of IPInterfaces Object
}
*/
-export async function getNodesIPsInterfaces(
- nodeNames: string[],
-): Promise<{
+export async function getNodesIPsInterfaces(nodeNames: string[]): Promise<{
return: [{ [nodeName: string]: boolean | IPInterfaces | string }],
}> {
if (!saltApiClient) {
throw new Error('Salt api client should be defined.');
}
- return saltApiClient.post('/', {
+ const result = saltApiClient.post('/', {
client: 'local',
tgt: nodeNames.join(','),
tgt_type: 'list',
@@ -113,4 +128,9 @@ export async function getNodesIPsInterfaces(
'ip_interfaces',
],
});
+ if (result.error) {
+ return handleUnAuthorizedError({ error: result.error });
+ } else {
+ return result;
+ }
}