import React, { Suspense, useEffect, useState } from 'react';
import './App.scss';
import { Route, Switch, Redirect, useLocation, useHistory } from 'react-router-dom';
import Policies from './access-policies/components/policies/Policies';
import { ThemeProvider, Theme, StyledEngineProvider } from '@mui/material/styles';
import { scaTheme } from './scaTheme';
import Wizard, { WizardMode } from './create-edit-policy/wizard/Wizard';
import { INavBarItem, updateSideNavigation, updateShellURL, GeneralMessageEnum } from '@cyberark/shell-sdk';
import { isInIframe } from './utils/shellUtils';
import IntroPage from './introducation-page/introPage';
import './interceptors/CsrfInterceptor';
import './interceptors/RefreshTokenInterceptor';
import { runMocks } from './mocks/mocker';
import { useDispatch, useSelector } from 'react-redux';
import { MessagesState } from './global-state/reducers/ShowMessagesReducer';
import { ConfigService } from './access-policies/services/ConfigService';
import {
setSocketConnectionId,
updateAccessMap,
updateApplicationConfig,
updateCemOnboarding,
updateCloudRequestObject,
updateOnboardingDone,
updatePoliciesTable,
} from './global-state/Actions';
import CircularProgress from '@mui/material/CircularProgress';
import { Box } from '@mui/material';
import { hideIntroduction, onHideIntroductionButton } from '@cyberark/shell-sdk/dist/shell-sdk';
import ErrorDialog from './error-handling/error-dialog/ErrorDialog';
import Roles from './cloud-roles/components/CloudRoles';
import ServiceUnavalable from './error-handling/service-unavalable/serviceUnavalable';
import { ErrorBoundary } from 'react-error-boundary';
import { shouldInitShellSDK } from './index';
import RequestForm from './request-form/RequestForm';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import { PermissionService } from './shared/PermissionService';
import { PoliciesService } from './access-policies/services/PoliciesService';
import { CemAzureOnBoardingData } from './onboarding/azure-onboarding/CemAzureOnBoardingData';
import { CemGcpOnBoardingData } from './onboarding/gcp-onboarding/CemGcpOnBoardingData';
import AzureOnboarding from './onboarding/azure-onboarding/AzureOnboarding';
import SelfService from './self-service/SelfService';
import GcpOnboarding from './onboarding/gcp-onboarding/GcpOnboarding';
import { ApplicationConfig } from './global-state/reducers/ApplicationConfigReducer';
import AccessPolicies from './access-policies/AccessPolicies';
import { SocketService } from 'src/cloud-roles/services/SocketService';
import { UserService } from './access-policies/services/UserService';
import { setUserPermissionsAction } from './global-state/actions/user.actions';
import ProtectedRoute from './shared/hoc/ProtectedRoute';
declare module '@mui/styles/defaultTheme' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
interface DefaultTheme extends Theme {
config?: string;
}
}
export const navBarItems = [
{
label: `Introduction`,
url: 'introduction',
iconClass: 'cyb-ic-welcome',
isRoot: true,
} as INavBarItem,
{
label: `Policies`,
url: 'policies',
iconClass: 'cyb-ic-policy-list',
isRoot: true,
} as INavBarItem,
{
label: `Settings`,
url: 'settings',
iconClass: 'cyb-ic-settings',
isRoot: true,
shouldHide: true,
} as INavBarItem,
{
label: `gcp_onboarding`,
url: 'gcp_onboarding',
iconClass: 'cyb-ic-recuring-access',
isRoot: true,
shouldHide: true,
} as INavBarItem,
];
export const navBarItemsWithoutIntro = [
{
label: `Policies`,
url: 'policies',
iconClass: 'cyb-ic-policy-list',
isRoot: true,
} as INavBarItem,
{
label: `Settings`,
url: 'settings',
iconClass: 'cyb-ic-settings',
isRoot: true,
shouldHide: true,
} as INavBarItem,
{
label: `gcp_onboarding`,
url: 'gcp_onboarding',
iconClass: 'cyb-ic-recuring-access',
isRoot: true,
shouldHide: true,
} as INavBarItem,
];
const socketService = new SocketService();
const queryClient = new QueryClient();
const userService = new UserService();
function App() {
const location = useLocation();
if (process.env.REACT_APP_MOCK === 'mock') {
runMocks();
}
const appConfig: ApplicationConfig = useSelector<ApplicationConfig>(
(state: any) => state.appConfig.applicationConfig
) as ApplicationConfig;
const webSocketFlagEnabled = appConfig.wizardConfig.enableWebSocket;
const permissionService = new PermissionService();
const errorMessage = useSelector<MessagesState>((state: any) => state.messages) as MessagesState;
const dispatch = useDispatch();
const [loading, setLoading] = React.useState(true);
const [fetchUserPermissionsLoading, setFetchUserPermissionsLoading] = useState(true);
const history = useHistory();
const [defaultRoute, setDefaultRoute] = useState<string>('/Introduction');
const [showHide, setShowHide] = useState(true);
let socket: WebSocket;
let pingInterval: any;
console.log('----- APP .tsx ---- ', { fetchUserPermissionsLoading });
useEffect(() => {
getUserPermissions();
}, []);
const getUserPermissions = async () => {
try {
console.log('getUserPermissions');
const res = await userService.getUserPermissions();
console.log({ res });
dispatch(setUserPermissionsAction(res.data.actions));
setFetchUserPermissionsLoading(false);
} catch (e) {
console.error(e);
setFetchUserPermissionsLoading(false);
}
};
const updateNavBarItems = (items: INavBarItem[], appConfigData: ApplicationConfig) => {
return items.map((item) => {
if (item.url === 'settings') {
item.shouldHide = !appConfigData.navBarItemsShow.selfService;
}
return item;
});
};
const startPingSending = () => {
pingInterval = setInterval(() => {
if (!socket) {
return;
}
if (socket.readyState === socket.OPEN) {
socket.send('heartbeat');
}
}, 1000 * 30);
};
window.addEventListener(
'message',
(e) => {
console.log('event listener was invoked');
try {
if (e.origin !== window.location.href) {
const data = e.data;
if (data.SCARequestPayload) {
const esacped = data.SCARequestPayload.replace(/"/g, '"');
console.log('esacped' + esacped);
const reqObj = JSON.parse(esacped);
dispatch(updateCloudRequestObject(reqObj));
}
if (e.data.type === GeneralMessageEnum.GENERAL) {
const cemData = e.data.data;
switch (cemData.type) {
case 'CEM_ONBORADING_DATA':
if (window.location.hash.includes('gcp_onboarding')) {
dispatch(updateCemOnboarding(new CemGcpOnBoardingData(cemData)));
break;
}
if (window.location.hash.includes('azure_onboarding')) {
dispatch(updateCemOnboarding(new CemAzureOnBoardingData(cemData)));
break;
}
break;
case 'CEM_ONBOARIDNG_PROCESS_DONE':
dispatch(updateOnboardingDone({ onboarding: cemData }));
break;
case 'ACCESS_MAP_LOADED':
dispatch(updateAccessMap({ accessMap: true }));
break;
}
}
}
} catch (ex) {
console.error('error from identity listener ' + ex);
}
},
false
);
const connectWebSocket = (url: string) => {
socket = new WebSocket(url);
socket.onopen = (event: any) => {
console.log('Websocket is connected');
startPingSending();
};
socket.onerror = (error: any) => {
console.error('Socket encountered error: ', error.message, 'Closing socket');
socket.close();
};
socket.onmessage = (event: any) => {
const dataFromServer = JSON.parse(event.data.toString());
if (dataFromServer.policy_id) {
console.log('Message received:', event.data);
dispatch(updatePoliciesTable(dataFromServer));
}
if (dataFromServer.connection_id) {
console.log('Message received connection id:', event.data);
dispatch(setSocketConnectionId(dataFromServer));
}
};
socket.onclose = (event: any) => {
console.error('Socket is closed. Reconnect will be attempted in 1 second.', event.reason);
if (pingInterval) {
clearInterval(pingInterval);
}
setTimeout(() => {
initWebsocket();
}, 1000);
};
};
const initWebsocket = () => {
socketService
.allocateWebSocket()
.then((data) => {
try {
connectWebSocket(data.data);
} catch (error: any) {
console.error('Connection to websocket failed, Reconnect will be attempted in 3 second.', error);
setTimeout(() => {
initWebsocket();
}, 3000);
}
})
.catch((error) => {
console.error('Connection to websocket failed, Reconnect will be attempted in 3 second.', error);
setTimeout(() => {
initWebsocket();
}, 3000);
});
};
useEffect(() => {
const confService = new ConfigService();
if (!shouldInitShellSDK()) {
setLoading(false);
return;
}
confService
.getApplicationConfig()
.then((result) => {
const data = result.data;
if (permissionService.isNoAccess(data.applicationConfig.wizardConfig.scope)) {
console.log("Your role isn't allowed to use SCA");
setLoading(false);
history.push('/service-unavalable');
}
data.applicationConfig.timeoutConfig.default = data.applicationConfig.timeoutConfig.default
? data.applicationConfig.timeoutConfig.default * 1000
: 15000;
dispatch(updateApplicationConfig(result.data));
setLoading(false);
if (isInIframe()) {
updateSideNavigation(updateNavBarItems(navBarItems, data.applicationConfig));
onHideIntroductionButton()
.then(() => {
updateSideNavigation(updateNavBarItems(navBarItemsWithoutIntro, data.applicationConfig));
setDefaultRoute('/policies');
return history.push('/policies');
})
.catch((error) => {
console.error(error);
});
}
if (data.applicationConfig.wizardConfig.enableWebSocket) {
initWebsocket();
}
})
.catch((error) => {
console.log(error);
setLoading(false);
history.push('/service-unavalable');
});
}, []);
useEffect(() => {
if (isInIframe()) {
updateShellURL({
pathname: location.pathname,
});
}
}, [location]);
const onHideIntroPage = (appConfigData: ApplicationConfig) => {
setShowHide(false);
updateSideNavigation(updateNavBarItems(navBarItemsWithoutIntro, appConfigData));
hideIntroduction();
history.push('/policies');
};
useEffect(() => {
onHideIntroductionButton()
.then(() => {
setShowHide(false);
})
.catch((error) => {
console.error(error);
});
}, []);
const sharedRotes = () => {
return (
<>
<Route path="/policies">
<AccessPolicies />
</Route>
<Route path="/Introduction">
<IntroPage onHideIntroPage={() => onHideIntroPage(appConfig)} showHide={showHide} />
</Route>
<Route path="/settings">
<SelfService />
</Route>
<Route path="/gcp_onboarding">
<GcpOnboarding />
</Route>
<Route path="/azure_onboarding">
<AzureOnboarding />
</Route>
<Route path="/service-unavalable">
<ServiceUnavalable />
</Route>
<ProtectedRoute
path="/create_policy/:provider"
render={(props) => <Wizard {...props.match.params.provider} wizardMode={WizardMode.Create} />}
permissions={(match) => [`sca.mgmt.${match.params.provider}.create`]}
/>
<Route
path="/view_policy/:provider/:id"
render={(props) => <Wizard {...props.match.params.id} wizardMode={WizardMode.View} />}
/>
<Route
path="/edit_policy/:provider/:id"
render={(props) => <Wizard {...props.match.params.id} wizardMode={WizardMode.Edit} />}
/>
<Route path="/cloud_roles">
<Roles />
</Route>
<Route path="/request_form">
<RequestForm />
</Route>
<Route exact={true} path="/">
<Redirect to={defaultRoute} />
</Route>
</>
);
};
const getRoutesForApp = () => {
if (isInIframe()) {
return <Switch>{sharedRotes()}</Switch>;
} else {
return (
<Switch>
<Route path="/policies">
<AccessPolicies />
</Route>
{sharedRotes()}
<Redirect from="/" to="policies" />
</Switch>
);
}
};
const showLoader = loading || fetchUserPermissionsLoading;
console.log({ loading, fetchUserPermissionsLoading });
console.log({ showLoader });
if (showLoader) {
return (
<Box data-testid="app" sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '800px' }}>
<CircularProgress size={150} />
</Box>
);
}
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<StyledEngineProvider injectFirst={true}>
<ThemeProvider theme={scaTheme}>
<ErrorBoundary
FallbackComponent={ServiceUnavalable}
onError={(error) => {
console.error(error.message);
}}
>
{errorMessage.showErrorMessage && errorMessage.errorDetails ? (
<ErrorDialog open={errorMessage.showErrorMessage} errorDetails={errorMessage.errorDetails} />
) : null}
<Suspense fallback="loading">
<div className="App" data-testid="app">
<div>{getRoutesForApp()}</div>
</div>
</Suspense>
</ErrorBoundary>
</ThemeProvider>
</StyledEngineProvider>
</QueryClientProvider>
);
}
export default App;