mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
SDA-4737 - Add retry logic for browser login (#2232)
* SDA-4737 - Add retry logic for browser login * SDA-4725 - Update SDA title bar branding * SDA-4737 - Add abort & handle network changed use case
This commit is contained in:
parent
4c366bcf2f
commit
b18bb12028
@ -8,6 +8,7 @@
|
||||
"autoUpdateCheckInterval": "30",
|
||||
"enableBrowserLogin": false,
|
||||
"browserLoginAutoConnect": false,
|
||||
"browserLoginRetryTimeout": "5",
|
||||
"overrideUserAgent": false,
|
||||
"minimizeOnClose" : "ENABLED",
|
||||
"launchOnStartup" : "ENABLED",
|
||||
|
@ -33,6 +33,7 @@ bring_to_front=$(sed -n '6p' ${settingsFilePath});
|
||||
dev_tools_enabled=$(sed -n '7p' ${settingsFilePath});
|
||||
enable_browser_login=$(sed -n '8p' ${settingsFilePath});
|
||||
browser_login_autoconnect=$(sed -n '9p' ${settingsFilePath});
|
||||
browser_login_retry_timeout=$(sed -n '10p' ${settingsFilePath});
|
||||
|
||||
## If any of the above values turn out to be empty, set default values ##
|
||||
if [ "$pod_url" = "" ]; then pod_url="https://my.symphony.com"; fi
|
||||
@ -44,6 +45,7 @@ if [ "$bring_to_front" = "" ] || [ "$bring_to_front" = 'false' ]; then bring_to_
|
||||
if [ "$dev_tools_enabled" = "" ]; then dev_tools_enabled=true; fi
|
||||
if [ "$enable_browser_login" = "" ]; then enable_browser_login=false; fi
|
||||
if [ "$browser_login_autoconnect" = "" ]; then browser_login_autoconnect=false; fi
|
||||
if [ "$browser_login_retry_timeout" = "" ]; then browser_login_retry_timeout='5'; fi
|
||||
|
||||
|
||||
## Add settings force auto update
|
||||
@ -86,6 +88,7 @@ if [ "$EUID" -ne 0 ]; then
|
||||
defaults write "$plistFilePath" autoUpdateCheckInterval -string "30"
|
||||
defaults write "$plistFilePath" enableBrowserLogin -bool "$enable_browser_login"
|
||||
defaults write "$plistFilePath" browserLoginAutoConnect -bool "$browser_login_autoconnect"
|
||||
defaults write "$plistFilePath" browserLoginRetryTimeout -string "$browser_login_retry_timeout"
|
||||
defaults write "$plistFilePath" overrideUserAgent -bool false
|
||||
defaults write "$plistFilePath" minimizeOnClose -string "$minimize_on_close"
|
||||
defaults write "$plistFilePath" launchOnStartup -string "$launch_on_startup"
|
||||
@ -130,6 +133,7 @@ else
|
||||
sudo -u "$userName" defaults write "$plistFilePath" autoUpdateCheckInterval -string "30"
|
||||
sudo -u "$userName" defaults write "$plistFilePath" enableBrowserLogin -bool "$enable_browser_login"
|
||||
sudo -u "$userName" defaults write "$plistFilePath" browserLoginAutoConnect -bool "$browser_login_autoconnect"
|
||||
sudo -u "$userName" defaults write "$plistFilePath" browserLoginRetryTimeout -string "$browser_login_retry_timeout"
|
||||
sudo -u "$userName" defaults write "$plistFilePath" overrideUserAgent -bool false
|
||||
sudo -u "$userName" defaults write "$plistFilePath" minimizeOnClose -string "$minimize_on_close"
|
||||
sudo -u "$userName" defaults write "$plistFilePath" launchOnStartup -string "$launch_on_startup"
|
||||
|
@ -100,6 +100,7 @@ describe('config', () => {
|
||||
'browserLoginAutoConnect',
|
||||
'latestAutoUpdateChannelEnabled',
|
||||
'betaAutoUpdateChannelEnabled',
|
||||
'browserLoginRetryTimeout',
|
||||
];
|
||||
const globalConfig: object = { url: 'test' };
|
||||
const userConfig: object = { configVersion: '4.0.1' };
|
||||
|
@ -50,6 +50,7 @@ describe('Plist Handler', () => {
|
||||
betaAutoUpdateChannelEnabled: undefined,
|
||||
bringToFront: undefined,
|
||||
browserLoginAutoConnect: undefined,
|
||||
browserLoginRetryTimeout: undefined,
|
||||
customFlags: {
|
||||
authNegotiateDelegateWhitelist: undefined,
|
||||
authServerWhitelist: undefined,
|
||||
|
@ -39,6 +39,7 @@ export const ConfigFieldsDefaultValues: Partial<IConfig> = {
|
||||
browserLoginAutoConnect: false,
|
||||
latestAutoUpdateChannelEnabled: true,
|
||||
betaAutoUpdateChannelEnabled: true,
|
||||
browserLoginRetryTimeout: '5',
|
||||
};
|
||||
|
||||
export const ConfigFieldsToRestart = new Set([
|
||||
@ -85,6 +86,7 @@ export interface IConfig {
|
||||
startedAfterAutoUpdate?: boolean;
|
||||
enableBrowserLogin?: boolean;
|
||||
browserLoginAutoConnect?: boolean;
|
||||
browserLoginRetryTimeout?: string;
|
||||
betaAutoUpdateChannelEnabled?: boolean;
|
||||
latestAutoUpdateChannelEnabled?: boolean;
|
||||
forceAutoUpdate?: boolean;
|
||||
|
@ -101,6 +101,8 @@ let loginUrl = '';
|
||||
let formattedPodUrl = '';
|
||||
let credentialsPromise;
|
||||
const credentialsPromiseRefHolder: { [key: string]: any } = {};
|
||||
const BROWSER_LOGIN_RETRY = 15 * 1000; // 15sec
|
||||
const BROWSER_LOGIN_ABORT_TIMEOUT = 10 * 1000; // 10sec
|
||||
|
||||
/**
|
||||
* Handle API related ipc messages from renderers. Only messages from windows
|
||||
@ -467,7 +469,10 @@ ipcMain.on(
|
||||
? userConfigURL
|
||||
: globalConfigURL;
|
||||
const { subdomain, domain, tld } = whitelistHandler.parseDomain(podUrl);
|
||||
const localConfig = config.getConfigFields(['enableBrowserLogin']);
|
||||
const localConfig = config.getConfigFields([
|
||||
'enableBrowserLogin',
|
||||
'browserLoginRetryTimeout',
|
||||
]);
|
||||
|
||||
formattedPodUrl = `https://${subdomain}.${domain}${tld}`;
|
||||
loginUrl = getBrowserLoginUrl(formattedPodUrl);
|
||||
@ -483,7 +488,10 @@ ipcMain.on(
|
||||
'check if sso is enabled for the pod',
|
||||
formattedPodUrl,
|
||||
);
|
||||
loadPodUrl(false);
|
||||
const timeout = localConfig.browserLoginRetryTimeout
|
||||
? parseInt(localConfig.browserLoginRetryTimeout, 10)
|
||||
: 0;
|
||||
loadPodUrl(false, timeout);
|
||||
} else {
|
||||
logger.info(
|
||||
'main-api-handler:',
|
||||
@ -739,72 +747,162 @@ const logApiCallParams = (arg: any) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadPodUrl = (proxyLogin = false) => {
|
||||
logger.info('loading pod URL. Proxy: ', proxyLogin);
|
||||
let onLogin = {};
|
||||
if (proxyLogin) {
|
||||
onLogin = {
|
||||
async onLogin(authInfo) {
|
||||
// this 'authInfo' is the one received by the 'login' event. See https://www.electronjs.org/docs/latest/api/client-request#event-login
|
||||
proxyDetails.hostname = authInfo.host || authInfo.realm;
|
||||
await credentialsPromise;
|
||||
return Promise.resolve({
|
||||
username: proxyDetails.username,
|
||||
password: proxyDetails.password,
|
||||
/**
|
||||
* Loads the Pod URL and handles potential authentication challenges.
|
||||
*
|
||||
* This function attempts to fetch the Pod URL and handles various authentication scenarios:
|
||||
* - Standard login (no proxy)
|
||||
* - Proxy login with authentication window
|
||||
* - Login retry logic for failed attempts
|
||||
*
|
||||
* @param {boolean} [proxyLogin=false] - Whether to use a proxy for the request. Defaults to false.
|
||||
* @param {number} [retryDurationInMinutes=0] - The duration (in minutes) for the retry logic. Defaults to 0 (no retries).
|
||||
*/
|
||||
const loadPodUrl = (() => {
|
||||
let isRetryInProgress: boolean = false;
|
||||
let retryTimeoutId: NodeJS.Timeout | null = null;
|
||||
|
||||
return (proxyLogin = false, retryDurationInMinutes = 0) => {
|
||||
logger.info('main-api-handler: loading pod URL. Proxy: ', proxyLogin);
|
||||
|
||||
const maxRetries = Math.floor(
|
||||
(retryDurationInMinutes * 60 * 1000) / BROWSER_LOGIN_RETRY,
|
||||
);
|
||||
let retryCount = 0;
|
||||
|
||||
// Function to attempt fetching the endpoint
|
||||
const attemptFetch = async () => {
|
||||
if (retryTimeoutId) {
|
||||
clearTimeout(retryTimeoutId); // Clear any existing timeout to avoid overlaps
|
||||
}
|
||||
|
||||
logger.info(
|
||||
'main-api-handler: Attempting to fetch the pod URL. Attempt:',
|
||||
retryCount + 1,
|
||||
);
|
||||
|
||||
let onLogin = {};
|
||||
if (proxyLogin) {
|
||||
onLogin = {
|
||||
async onLogin(authInfo) {
|
||||
// this 'authInfo' is the one received by the 'login' event. See https://www.electronjs.org/docs/latest/api/client-request#event-login
|
||||
proxyDetails.hostname = authInfo.host || authInfo.realm;
|
||||
await credentialsPromise;
|
||||
return Promise.resolve({
|
||||
username: proxyDetails.username,
|
||||
password: proxyDetails.password,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(
|
||||
() => controller.abort(),
|
||||
BROWSER_LOGIN_ABORT_TIMEOUT,
|
||||
);
|
||||
try {
|
||||
const response = await fetch(`${formattedPodUrl}${AUTH_STATUS_PATH}`, {
|
||||
...onLogin,
|
||||
signal: controller.signal,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
fetch(`${formattedPodUrl}${AUTH_STATUS_PATH}`, onLogin)
|
||||
.then(async (response) => {
|
||||
const authResponse = (await response.json()) as IAuthResponse;
|
||||
logger.info('main-api-handler:', 'check auth response', authResponse);
|
||||
if (authResponse.authenticationType === 'sso') {
|
||||
logger.info(
|
||||
'main-api-handler: browser login is enabled - logging in',
|
||||
loginUrl,
|
||||
);
|
||||
await shell.openExternal(loginUrl);
|
||||
} else {
|
||||
logger.info(
|
||||
'main-api-handler: no SSO - loading main window with',
|
||||
formattedPodUrl,
|
||||
);
|
||||
const mainWebContents = windowHandler.getMainWebContents();
|
||||
if (mainWebContents && !mainWebContents.isDestroyed()) {
|
||||
windowHandler.setMainWindowOrigin(formattedPodUrl);
|
||||
mainWebContents.loadURL(formattedPodUrl);
|
||||
const authResponse = (await response.json()) as IAuthResponse;
|
||||
logger.info('main-api-handler: check auth response', authResponse);
|
||||
|
||||
if (authResponse.authenticationType === 'sso') {
|
||||
logger.info(
|
||||
'main-api-handler: browser login is enabled - logging in',
|
||||
loginUrl,
|
||||
);
|
||||
await shell.openExternal(loginUrl);
|
||||
} else {
|
||||
logger.info(
|
||||
'main-api-handler: no SSO - loading main window with',
|
||||
formattedPodUrl,
|
||||
);
|
||||
const mainWebContents = windowHandler.getMainWebContents();
|
||||
if (mainWebContents && !mainWebContents.isDestroyed()) {
|
||||
windowHandler.setMainWindowOrigin(formattedPodUrl);
|
||||
mainWebContents.loadURL(formattedPodUrl);
|
||||
}
|
||||
}
|
||||
|
||||
isRetryInProgress = false;
|
||||
setLoginRetryState(isRetryInProgress);
|
||||
retryTimeoutId = null;
|
||||
} catch (error: any) {
|
||||
if (
|
||||
(error.type === 'proxy' && error.code === 'PROXY_AUTH_FAILED') ||
|
||||
(error.code === 'ERR_TOO_MANY_RETRIES' && proxyLogin)
|
||||
) {
|
||||
credentialsPromise = new Promise((res, _rej) => {
|
||||
credentialsPromiseRefHolder.resolutionCallback = res;
|
||||
});
|
||||
const welcomeWindow =
|
||||
windowHandler.getMainWindow() as ICustomBrowserWindow;
|
||||
windowHandler.createBasicAuthWindow(
|
||||
welcomeWindow,
|
||||
proxyDetails.hostname,
|
||||
proxyDetails.retries === 0,
|
||||
undefined,
|
||||
(username, password) => {
|
||||
proxyDetails.username = username;
|
||||
proxyDetails.password = password;
|
||||
credentialsPromiseRefHolder.resolutionCallback(true);
|
||||
loadPodUrl(true);
|
||||
},
|
||||
);
|
||||
proxyDetails.retries += 1;
|
||||
} else {
|
||||
logger.error(
|
||||
'main-api-handler: browser login error. Details: ',
|
||||
error.type,
|
||||
error.code,
|
||||
);
|
||||
retryCount++;
|
||||
if (retryCount < maxRetries || error.code === 'ERR_NETWORK_CHANGED') {
|
||||
retryTimeoutId = setTimeout(attemptFetch, BROWSER_LOGIN_RETRY);
|
||||
} else {
|
||||
logger.error(
|
||||
'main-api-handler: Retry attempts exhausted. Endpoint unreachable.',
|
||||
);
|
||||
isRetryInProgress = false;
|
||||
setLoginRetryState(isRetryInProgress);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(async (error) => {
|
||||
if (
|
||||
(error.type === 'proxy' && error.code === 'PROXY_AUTH_FAILED') ||
|
||||
(error.code === 'ERR_TOO_MANY_RETRIES' && proxyLogin)
|
||||
) {
|
||||
credentialsPromise = new Promise((res, _rej) => {
|
||||
credentialsPromiseRefHolder.resolutionCallback = res;
|
||||
});
|
||||
const welcomeWindow =
|
||||
windowHandler.getMainWindow() as ICustomBrowserWindow;
|
||||
windowHandler.createBasicAuthWindow(
|
||||
welcomeWindow,
|
||||
proxyDetails.hostname,
|
||||
proxyDetails.retries === 0,
|
||||
undefined,
|
||||
(username, password) => {
|
||||
proxyDetails.username = username;
|
||||
proxyDetails.password = password;
|
||||
credentialsPromiseRefHolder.resolutionCallback(true);
|
||||
loadPodUrl(true);
|
||||
},
|
||||
);
|
||||
proxyDetails.retries += 1;
|
||||
}
|
||||
logger.error(
|
||||
'main-api-handler: browser login error. Details: ',
|
||||
error.type,
|
||||
error.code,
|
||||
};
|
||||
|
||||
// Start the retry logic only if it's not already in progress
|
||||
if (!isRetryInProgress) {
|
||||
isRetryInProgress = true;
|
||||
setLoginRetryState(isRetryInProgress);
|
||||
attemptFetch();
|
||||
} else {
|
||||
logger.info(
|
||||
'main-api-handler: Retry logic already in progress. Ignoring duplicate call.',
|
||||
);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Updates the login retry state in the main web content.
|
||||
*
|
||||
* Sends a message to the main web content indicating whether a login retry is in progress.
|
||||
* This message is used to update the UI accordingly.
|
||||
*
|
||||
* @param {boolean} isRetryInProgress - A boolean indicating whether a login retry is in progress.
|
||||
*/
|
||||
const setLoginRetryState = (isRetryInProgress: boolean) => {
|
||||
const mainWebContents = windowHandler.getMainWebContents();
|
||||
if (mainWebContents && !mainWebContents.isDestroyed()) {
|
||||
mainWebContents.send('welcome', {
|
||||
isRetryInProgress,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -19,6 +19,7 @@ const GENERAL_SETTINGS = {
|
||||
autoUpdateCheckInterval: 'string',
|
||||
enableBrowserLogin: 'boolean',
|
||||
browserLoginAutoConnect: 'boolean',
|
||||
browserLoginRetryTimeout: 'string',
|
||||
overrideUserAgent: 'boolean',
|
||||
minimizeOnClose: 'string',
|
||||
launchOnStartup: 'string',
|
||||
|
@ -12,6 +12,7 @@ interface IState {
|
||||
isBrowserLoginEnabled: boolean;
|
||||
browserLoginAutoConnect: boolean;
|
||||
isLoading: boolean;
|
||||
isRetryInProgress: boolean;
|
||||
}
|
||||
|
||||
const WELCOME_NAMESPACE = 'Welcome';
|
||||
@ -37,6 +38,7 @@ export default class Welcome extends React.Component<{}, IState> {
|
||||
isBrowserLoginEnabled: true,
|
||||
browserLoginAutoConnect: false,
|
||||
isLoading: false,
|
||||
isRetryInProgress: false,
|
||||
};
|
||||
this.updateState = this.updateState.bind(this);
|
||||
}
|
||||
@ -52,6 +54,7 @@ export default class Welcome extends React.Component<{}, IState> {
|
||||
isLoading,
|
||||
isBrowserLoginEnabled,
|
||||
isFirstTimeLaunch,
|
||||
isRetryInProgress,
|
||||
} = this.state;
|
||||
return (
|
||||
<div className='Welcome' lang={i18n.getLocale()}>
|
||||
@ -126,6 +129,7 @@ export default class Welcome extends React.Component<{}, IState> {
|
||||
<button
|
||||
className='Welcome-retry-button'
|
||||
onClick={this.eventHandlers.onLogin}
|
||||
disabled={isRetryInProgress}
|
||||
>
|
||||
{i18n.t('Retry', WELCOME_NAMESPACE)()}
|
||||
</button>
|
||||
|
@ -242,6 +242,7 @@ body {
|
||||
|
||||
&-continue-button-loading:disabled {
|
||||
background-color: @electricity-ui-50;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&-redirect-info-text-container {
|
||||
@ -271,6 +272,11 @@ body {
|
||||
color: @electricity-ui-30;
|
||||
}
|
||||
|
||||
&-retry-button:disabled {
|
||||
cursor: not-allowed;
|
||||
color: @graphite-60;
|
||||
}
|
||||
|
||||
&-auto-connect-wrapper {
|
||||
display: flex;
|
||||
margin-top: 18px;
|
||||
|
Loading…
Reference in New Issue
Block a user