mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-20 07:47:46 +00:00
Adds a retry logic with a button. The problem is that some browsers (Safari) when window.open is not executed directly in the onClick handler, but on some underlying Promise will block the popup.
314 lines
10 KiB
TypeScript
314 lines
10 KiB
TypeScript
import { maybeRedirectToWelcomePage } from '../app/actions.web';
|
|
import { IStore } from '../app/types';
|
|
import { connect } from '../base/connection/actions';
|
|
import { openDialog } from '../base/dialog/actions';
|
|
import { setJWT } from '../base/jwt/actions';
|
|
import { browser } from '../base/lib-jitsi-meet';
|
|
import { hideNotification, showErrorNotification, showNotification } from '../notifications/actions';
|
|
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
|
|
|
import { CANCEL_LOGIN } from './actionTypes';
|
|
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
|
|
import { isTokenAuthInline } from './functions.any';
|
|
import logger from './logger';
|
|
|
|
export * from './actions.any';
|
|
|
|
const PROMPT_POPUP_NOTIFICATION_ID = 'PROMPT_POPUP_NOTIFICATION_ID';
|
|
|
|
/**
|
|
* Custom error that includes a recovery callback.
|
|
*/
|
|
class PopupBlockedError extends Error {
|
|
constructor(
|
|
message: string = 'Popup was blocked by browser',
|
|
public readonly retry?: () => Window | null
|
|
) {
|
|
super(message);
|
|
this.name = 'PopupBlockedError';
|
|
Error.captureStackTrace(this, PopupBlockedError);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels {@ink LoginDialog}.
|
|
*
|
|
* @returns {{
|
|
* type: CANCEL_LOGIN
|
|
* }}
|
|
*/
|
|
export function cancelLogin() {
|
|
return {
|
|
type: CANCEL_LOGIN
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Cancels authentication, closes {@link WaitForOwnerDialog}
|
|
* and navigates back to the welcome page only in the case of authentication required error.
|
|
* We can be showing the dialog while lobby is enabled and participant is still waiting there and hiding this dialog
|
|
* should do nothing.
|
|
*
|
|
* @returns {Function}
|
|
*/
|
|
export function cancelWaitForOwner() {
|
|
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
|
const { authRequired } = getState()['features/base/conference'];
|
|
|
|
authRequired && dispatch(maybeRedirectToWelcomePage());
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Redirect to the default location (e.g. Welcome page).
|
|
*
|
|
* @returns {Function}
|
|
*/
|
|
export function redirectToDefaultLocation() {
|
|
return (dispatch: IStore['dispatch']) => dispatch(maybeRedirectToWelcomePage());
|
|
}
|
|
|
|
/**
|
|
* Generates a cryptographic nonce.
|
|
*
|
|
* @returns {string} The generated nonce.
|
|
*/
|
|
function generateNonce(): string {
|
|
const array = new Uint8Array(32);
|
|
|
|
crypto.getRandomValues(array);
|
|
|
|
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
/**
|
|
* Performs login with a popup window.
|
|
*
|
|
* @param {string} tokenAuthServiceUrl - Authentication service URL.
|
|
* @param {Window|undefined} popup - Authentication service URL.
|
|
* @returns {Promise<any>} A promise that resolves with the authentication
|
|
* result or rejects with an error.
|
|
*/
|
|
export function loginWithPopup(tokenAuthServiceUrl: string, popup?: Window | null): Promise<any> {
|
|
return new Promise<any>((resolve, reject) => {
|
|
let nonceParam = '';
|
|
|
|
try {
|
|
const nonce = generateNonce();
|
|
|
|
sessionStorage.setItem('oauth_nonce', nonce);
|
|
|
|
nonceParam = `&nonce=${nonce}`;
|
|
} catch (e) {
|
|
if (e instanceof DOMException && e.name === 'SecurityError') {
|
|
logger.warn(
|
|
'sessionStorage access denied (cross-origin or restricted context) enable it to improve security',
|
|
e);
|
|
} else {
|
|
logger.error('Unable to save nonce in session storage', e);
|
|
}
|
|
}
|
|
|
|
const openPopup = () => {
|
|
// Open popup
|
|
const width = 500;
|
|
const height = 600;
|
|
const left = window.screen.width / 2 - width / 2;
|
|
const top = window.screen.height / 2 - height / 2;
|
|
|
|
return window.open(
|
|
`${tokenAuthServiceUrl}${nonceParam}`,
|
|
`Auth-${Date.now()}`,
|
|
`width=${width},height=${height},left=${left},top=${top}`
|
|
);
|
|
};
|
|
|
|
if (!popup) {
|
|
popup = openPopup();
|
|
}
|
|
|
|
if (!popup) {
|
|
// adds a callback to the error that can be used to directly retry where window.open will be executed
|
|
// on the user click without any promises(dispatches) incoved
|
|
reject(new PopupBlockedError('Popup blocked', () => {
|
|
return openPopup();
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
let closedPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
|
|
const cleanup = (handler: (event: MessageEvent) => void) => {
|
|
window.removeEventListener('message', handler);
|
|
clearInterval(closedPollInterval);
|
|
popup?.close();
|
|
|
|
try {
|
|
sessionStorage.removeItem('oauth_nonce');
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
try {
|
|
sessionStorage.removeItem('code_verifier');
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
};
|
|
|
|
const handler = (event: MessageEvent) => {
|
|
// Verify origin
|
|
if (event.origin !== window.location.origin) {
|
|
return;
|
|
}
|
|
|
|
if (event.data.type === 'oauth-success') {
|
|
cleanup(handler);
|
|
|
|
resolve({
|
|
accessToken: event.data.accessToken,
|
|
idToken: event.data.idToken,
|
|
refreshToken: event.data.refreshToken
|
|
});
|
|
} else if (event.data.type === 'oauth-error') {
|
|
cleanup(handler);
|
|
|
|
reject(new Error(event.data.error));
|
|
}
|
|
};
|
|
|
|
// Listen for messages from the popup
|
|
window.addEventListener('message', handler);
|
|
|
|
// Detect manual popup close before authentication completes
|
|
closedPollInterval = setInterval(() => {
|
|
if (popup?.closed) {
|
|
cleanup(handler);
|
|
reject(new Error('Login cancelled'));
|
|
}
|
|
}, 500);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Performs silent logout by loading the token authentication logout service URL in an
|
|
* invisible iframe.
|
|
*
|
|
* @param {string} tokenAuthLogoutServiceUrl - Logout service URL.
|
|
* @returns {Promise<any>} A promise that resolves when logout is complete.
|
|
*/
|
|
export function silentLogout(tokenAuthLogoutServiceUrl: string): any {
|
|
return new Promise<void>(resolve => {
|
|
const iframe = document.createElement('iframe');
|
|
|
|
iframe.style.display = 'none';
|
|
iframe.src = tokenAuthLogoutServiceUrl;
|
|
document.body.appendChild(iframe);
|
|
|
|
// Listen for logout completion
|
|
const handler = (event: any) => {
|
|
if (event.origin !== window.location.origin) return;
|
|
|
|
if (event.data.type === 'logout-success') {
|
|
window.removeEventListener('message', handler);
|
|
document.body.removeChild(iframe);
|
|
|
|
resolve();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('message', handler);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Opens token auth URL page.
|
|
*
|
|
* @param {string} tokenAuthServiceUrl - Authentication service URL.
|
|
*
|
|
* @returns {Function}
|
|
*/
|
|
export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
|
|
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
|
const redirect = () => {
|
|
if (browser.isElectron()) {
|
|
window.open(tokenAuthServiceUrl, '_blank');
|
|
} else {
|
|
window.location.href = tokenAuthServiceUrl;
|
|
}
|
|
};
|
|
|
|
if (!browser.isElectron() && isTokenAuthInline(getState()['features/base/config'])) {
|
|
const login = (popup?: Window | null) => {
|
|
return loginWithPopup(tokenAuthServiceUrl, popup)
|
|
.then((result: { accessToken: string; idToken: string; refreshToken?: string; }) => {
|
|
// @ts-ignore
|
|
const token: string = result.accessToken;
|
|
const idToken: string = result.idToken;
|
|
const refreshToken: string | undefined = result.refreshToken;
|
|
|
|
// @ts-ignore
|
|
dispatch(setJWT(token, idToken, refreshToken));
|
|
|
|
logger.info('Reconnecting to conference with new token.');
|
|
|
|
const { connection } = getState()['features/base/connection'];
|
|
|
|
if (connection) {
|
|
connection.refreshToken(token).then(
|
|
() => {
|
|
const { membersOnly } = getState()['features/base/conference'];
|
|
|
|
membersOnly?.join();
|
|
})
|
|
.catch((err: any) => {
|
|
dispatch(setJWT());
|
|
logger.error(err);
|
|
});
|
|
} else {
|
|
dispatch(connect());
|
|
}
|
|
});
|
|
};
|
|
|
|
login().catch(err => {
|
|
if (err instanceof PopupBlockedError) {
|
|
dispatch(showNotification({
|
|
titleKey: 'dialog.loginPopupBlocked',
|
|
uid: PROMPT_POPUP_NOTIFICATION_ID,
|
|
customActionNameKey: [ 'dialog.retry' ],
|
|
customActionHandler: [ () => {
|
|
dispatch(hideNotification(PROMPT_POPUP_NOTIFICATION_ID));
|
|
|
|
// the window.open will be executed directly from the onClick handler of the notification action button
|
|
login(err.retry ? err.retry() : undefined)
|
|
.catch(logger.error);
|
|
} ],
|
|
appearance: NOTIFICATION_TYPE.ERROR
|
|
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
|
|
|
} else {
|
|
// let's add expand that will show the error message in the notification
|
|
dispatch(showErrorNotification({
|
|
titleKey: 'dialog.loginFailed'
|
|
}));
|
|
}
|
|
logger.error(err);
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// Show warning for leaving conference only when in a conference.
|
|
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
|
|
dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, {
|
|
handler: () => {
|
|
// Give time for the dialog to close.
|
|
setTimeout(() => redirect(), 500);
|
|
}
|
|
}));
|
|
} else {
|
|
redirect();
|
|
}
|
|
};
|
|
}
|