Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add keycloak js #409

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"express": "^4.17.1",
"graphql": "^15.5.2",
"isnumber": "^1.0.0",
"keycloak-js": "^26.1.0",
"qs": "^6.10.1",
"react-apexcharts": "^1.4.0",
"react-collapse": "^5.1.0",
Expand Down
7 changes: 7 additions & 0 deletions public/silent-check-sso.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
2 changes: 2 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
grafanaUrl,
grafanaDashboardUrl,
dataSourceIsEnable,
keycloakEnable,
indexHtml,
baseUrl,
} = require('./setupConfig');
Expand All @@ -35,6 +36,7 @@ app.get('*/dashboard-config.json', (req, res) => {
grafanaUrl,
grafanaDashboardUrl,
dataSourceIsEnable,
keycloakEnable,
baseUrl,
monitorBackend,
board,
Expand Down
2 changes: 2 additions & 0 deletions server/setupConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const grafanaUrl = process.env.GRAFANA_URL;
const grafanaDashboardUrl = process.env.GRAFANA_URL;

const dataSourceIsEnable = process.env.DATA_SOURCE_IS_ENABLE === 'true';
const keycloakEnable = process.env.KEYCLOAK_ENABLE === 'true';

const baseUrl = process.env.HKUBE_BASE_URL
? process.env.HKUBE_BASE_URL.replace(/^\//, '')
Expand Down Expand Up @@ -54,6 +55,7 @@ module.exports = {
grafanaUrl,
grafanaDashboardUrl,
dataSourceIsEnable,
keycloakEnable,
baseUrl,
board,
BOARD_HOST,
Expand Down
18 changes: 16 additions & 2 deletions src/Routes/Base/Header/HelpBar.react.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React from 'react';
import { COLOR } from 'styles';
import KeycloakServices from 'keycloak/keycloakServices';
import { MenuOutlined } from '@ant-design/icons';
import { Popover } from 'antd';
import { Popover, Avatar, Tooltip } from 'antd';
import { USER_GUIDE } from 'const';
import { FlexBox, Icons } from 'components/common';
import styled from 'styled-components';
Expand All @@ -17,7 +19,19 @@ const HelpBar = () => (
<InactiveModeTag />

<ExperimentPicker />

<Tooltip
title={`You are logged in as the user ${KeycloakServices.getUsername()}.`}
placement="top">
<Avatar
style={{
backgroundColor: COLOR.greenLight,
verticalAlign: 'middle',
textTransform: 'uppercase',
}}>
{KeycloakServices.getUsername() &&
KeycloakServices.getUsername().toString()[0]}
</Avatar>
</Tooltip>
<Popover content={<Settings />} placement="bottomRight" trigger="click">
<Icons.Hover type={<MenuOutlined title="Settings" />} />
</Popover>
Expand Down
13 changes: 13 additions & 0 deletions src/Routes/Base/Header/Settings/Settings.react.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useCallback } from 'react';
import KeycloakServices from 'keycloak/keycloakServices';
import { FlexBox, Icons } from 'components/common';
import { useNavigate } from 'react-router-dom';
import {
GlobalOutlined,
GithubOutlined,
QuestionCircleOutlined,
LogoutOutlined,
} from '@ant-design/icons';
import styled from 'styled-components';
import { useSelector } from 'react-redux';
Expand Down Expand Up @@ -36,6 +38,7 @@ const Settings = () => {
const { triggerUserGuide } = useActions();
const { hkubeSystemVersion } = useSelector(selectors.connection);
const { grafanaUrl } = useSelector(selectors.connection);
// const { keycloakEnable } = useSelector(selectors.connection);

const onGuideClick = useCallback(() => {
triggerUserGuide();
Expand All @@ -44,6 +47,8 @@ const Settings = () => {

const openUrl = url => () => window.open(url);

const logout = () => KeycloakServices.doLogout();

const DarkText = styled.div`
cursor: pointer;
font-size: 14px;
Expand Down Expand Up @@ -146,6 +151,14 @@ const Settings = () => {
<TextLink onClick={onGuideClick}>Help</TextLink>
</FlexBox.Auto>

<FlexBox.Auto>
<Icons.Hover
type={<LogoutOutlined title="logout" />}
onClick={logout}
/>
<TextLink onClick={logout}>logout</TextLink>
</FlexBox.Auto>

<DarkText as="span">{hkubeSystemVersion}</DarkText>

{/* <GraphDirection />
Expand Down
2 changes: 2 additions & 0 deletions src/actions/connection.action.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const connectionSetup = ({
grafanaUrl,
grafanaDashboardUrl,
dataSourceIsEnable,
keycloakEnable,
}) => ({
type: actions.CONNECTION_SETUP,
payload: {
Expand All @@ -41,6 +42,7 @@ export const connectionSetup = ({
grafanaUrl,
grafanaDashboardUrl,
dataSourceIsEnable,
keycloakEnable,
},
});

Expand Down
44 changes: 44 additions & 0 deletions src/client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
import axios from 'axios';
import KeycloakServices from 'keycloak/keycloakServices';

const client = axios.create();

client.interceptors.request.use(
config => {
const token = KeycloakServices.getToken();
if (token) {
return {
...config,
headers: {
...config.headers,
Authorization: `Bearer ${token}`,
},
};
}
return config;
},
error => Promise.reject(error)
);

client.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;

if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
await KeycloakServices.updateToken(30, () => {
const newToken = KeycloakServices.getToken();
originalRequest.headers.Authorization = `Bearer ${newToken}`;
});

return client(originalRequest);
} catch (refreshError) {
console.error('Failed to refresh token', refreshError);
KeycloakServices.doLogout();
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
}
);

export default client;
125 changes: 87 additions & 38 deletions src/graphql/useApolloClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
ApolloLink,
useReactiveVar,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
import { selectors } from 'reducers';
import { useSelector } from 'react-redux';
import cache, { numberErrorGraphQLVar } from 'cache';
import { Modal } from 'antd';
import { GrafanaLink } from 'components';
import KeycloakServices from 'keycloak/keycloakServices';

const MAX_ERRORS_THRESHOLD = 5; // Maximum number of errors allowed within the time interval
const TIME_INTERVAL = 60000; // 60 seconds in milliseconds
Expand All @@ -23,54 +25,20 @@ const useApolloClient = () => {
const numberErrorGraphQL = useReactiveVar(numberErrorGraphQLVar);

const [modal, contextHolder] = Modal.useModal();
const errorLink = onError(({ graphQLErrors, networkError }) => {
clearTimeout(timer);
errorCount += 1;

timer = setTimeout(() => {
errorCount = 0;
numberErrorGraphQLVar({ error: 0 });
}, TIME_INTERVAL);

if (
errorCount >= MAX_ERRORS_THRESHOLD &&
numberErrorGraphQL.error < MAX_ERRORS_THRESHOLD
) {
console.log(`Too many errors within the time interval`);
numberErrorGraphQLVar({ error: numberErrorGraphQL.error + 1 });
}

if (graphQLErrors) {
// Handle GraphQL errors
}

if (networkError) {
// Handle network errors
}
});

const httpLink = createHttpLink({
uri: `${backendApiUrl.replace('/api/v1', '')}/graphql`,
});

const apolloClient = new ApolloClient({
link: ApolloLink.from([errorLink, httpLink]),
cache,
});

// Function to open the error notification modal
const openNotification = () => {
modal.error({
title: `Oops... Something went wrong `,
title: `Oops... Something went wrong`,
content: (
<>
To see more details about the system status you can access
grafana,please click on <GrafanaLink />
To see more details about the system status you can access Grafana,
please click on <GrafanaLink />
</>
),
width: 500,
okText: 'Close',
okType: 'default',

onOk() {
numberErrorGraphQLVar({ error: 0 });
setTimeout(() => {
Expand All @@ -80,6 +48,86 @@ const useApolloClient = () => {
});
};

// Link to handle errors
const errorLink = onError(
({ graphQLErrors, networkError, response, operation }) => {
clearTimeout(timer);
errorCount += 1;

timer = setTimeout(() => {
errorCount = 0;
numberErrorGraphQLVar({ error: 0 });
}, TIME_INTERVAL);

if (
errorCount >= MAX_ERRORS_THRESHOLD &&
numberErrorGraphQL.error < MAX_ERRORS_THRESHOLD
) {
console.log(`Too many errors within the time interval`);
numberErrorGraphQLVar({ error: numberErrorGraphQL.error + 1 });
}

if (graphQLErrors) {
// Handle GraphQL errors
console.error('GraphQL Errors:', graphQLErrors);
}

if (networkError) {
// Handle network errors
console.error('Network Error:', networkError);
}

// Handle 401 Unauthorized (Token Expired)
if (
response &&
response.errors &&
response.errors.some(error => error.message === 'Unauthorized')
) {
// Try to refresh token
KeycloakServices.updateToken(30, () => {
const newToken = KeycloakServices.getToken();
operation.setContext({
headers: {
...operation.getContext().headers,
Authorization: `Bearer ${newToken}`,
},
});
})
.then(() =>
// Retry the operation after token refresh
operation.retry()
)
.catch(error => {
console.error('Failed to refresh token', error);
KeycloakServices.doLogout(); // Log the user out if refreshing the token fails
openNotification();
});
}
}
);

// HTTP link for GraphQL queries
const httpLink = createHttpLink({
uri: `${backendApiUrl.replace('/api/v1', '')}/graphql`,
});

// Authentication link to add the token to the headers
const authLink = setContext((_, { headers }) => {
const token = KeycloakServices.getToken();
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : '',
},
};
});

// Apollo Client setup
const apolloClient = new ApolloClient({
link: ApolloLink.from([authLink, errorLink, httpLink]),
cache,
});

return {
apolloClient,
openNotification,
Expand All @@ -88,4 +136,5 @@ const useApolloClient = () => {
setIsNotificationErrorShow,
};
};

export default useApolloClient;
Loading
Loading