Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled
320 lines
9.6 KiB
TypeScript
320 lines
9.6 KiB
TypeScript
import { IStore } from '../app/types';
|
|
import { APP_WILL_NAVIGATE } from '../base/app/actionTypes';
|
|
import {
|
|
CONFERENCE_FAILED,
|
|
CONFERENCE_JOINED,
|
|
CONFERENCE_LEFT
|
|
} from '../base/conference/actionTypes';
|
|
import { isRoomValid } from '../base/conference/functions';
|
|
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes';
|
|
import { hideDialog } from '../base/dialog/actions';
|
|
import { isDialogOpen } from '../base/dialog/functions';
|
|
import {
|
|
JitsiConferenceErrors,
|
|
JitsiConnectionErrors
|
|
} from '../base/lib-jitsi-meet';
|
|
import { MEDIA_TYPE } from '../base/media/constants';
|
|
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
|
import { isLocalTrackMuted } from '../base/tracks/functions.any';
|
|
import { parseURIString } from '../base/util/uri';
|
|
import { openLogoutDialog } from '../settings/actions';
|
|
|
|
import {
|
|
CANCEL_LOGIN,
|
|
LOGIN,
|
|
LOGOUT,
|
|
STOP_WAIT_FOR_OWNER,
|
|
UPGRADE_ROLE_FINISHED,
|
|
WAIT_FOR_OWNER
|
|
} from './actionTypes';
|
|
import {
|
|
hideLoginDialog,
|
|
openLoginDialog,
|
|
openTokenAuthUrl,
|
|
openWaitForOwnerDialog,
|
|
redirectToDefaultLocation,
|
|
setTokenAuthUrlSuccess,
|
|
stopWaitForOwner,
|
|
waitForOwner
|
|
} from './actions';
|
|
import { LoginDialog, WaitForOwnerDialog } from './components';
|
|
import { getTokenAuthUrl, isTokenAuthEnabled } from './functions';
|
|
import logger from './logger';
|
|
|
|
|
|
/**
|
|
* Middleware that captures connection or conference failed errors and controls
|
|
* {@link WaitForOwnerDialog} and {@link LoginDialog}.
|
|
*
|
|
* FIXME Some of the complexity was introduced by the lack of dialog stacking.
|
|
*
|
|
* @param {Store} store - Redux store.
|
|
* @returns {Function}
|
|
*/
|
|
MiddlewareRegistry.register(store => next => action => {
|
|
switch (action.type) {
|
|
case CANCEL_LOGIN: {
|
|
const { dispatch, getState } = store;
|
|
const state = getState();
|
|
const { thenableWithCancel } = state['features/authentication'];
|
|
|
|
thenableWithCancel?.cancel();
|
|
|
|
// The LoginDialog can be opened on top of "wait for owner". The app
|
|
// should navigate only if LoginDialog was open without the
|
|
// WaitForOwnerDialog.
|
|
if (!isDialogOpen(store, WaitForOwnerDialog)) {
|
|
if (_isWaitingForOwner(store)) {
|
|
// Instead of hiding show the new one.
|
|
const result = next(action);
|
|
|
|
dispatch(openWaitForOwnerDialog());
|
|
|
|
return result;
|
|
}
|
|
|
|
dispatch(hideLoginDialog());
|
|
|
|
const { authRequired, conference } = state['features/base/conference'];
|
|
const { passwordRequired } = state['features/base/connection'];
|
|
|
|
// Only end the meeting if we are not already inside and trying to upgrade.
|
|
// NOTE: Despite it's confusing name, `passwordRequired` implies an XMPP
|
|
// connection auth error.
|
|
if ((passwordRequired || authRequired) && !conference) {
|
|
dispatch(redirectToDefaultLocation());
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CONFERENCE_FAILED: {
|
|
const { error } = action;
|
|
|
|
// XXX The feature authentication affords recovery from
|
|
// CONFERENCE_FAILED caused by
|
|
// JitsiConferenceErrors.AUTHENTICATION_REQUIRED.
|
|
let recoverable;
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const [ _lobbyJid, lobbyWaitingForHost ] = error.params;
|
|
|
|
if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED
|
|
|| (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR && lobbyWaitingForHost)) {
|
|
if (typeof error.recoverable === 'undefined') {
|
|
error.recoverable = true;
|
|
}
|
|
recoverable = error.recoverable;
|
|
}
|
|
if (recoverable) {
|
|
store.dispatch(waitForOwner());
|
|
} else {
|
|
store.dispatch(stopWaitForOwner());
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CONFERENCE_JOINED: {
|
|
const { dispatch, getState } = store;
|
|
const state = getState();
|
|
const config = state['features/base/config'];
|
|
|
|
if (isTokenAuthEnabled(config)
|
|
&& config.tokenAuthUrlAutoRedirect
|
|
&& state['features/base/jwt'].jwt) {
|
|
// auto redirect is turned on and we have successfully logged in
|
|
// let's mark that
|
|
dispatch(setTokenAuthUrlSuccess(true));
|
|
}
|
|
|
|
if (_isWaitingForOwner(store)) {
|
|
store.dispatch(stopWaitForOwner());
|
|
}
|
|
store.dispatch(hideLoginDialog());
|
|
break;
|
|
}
|
|
|
|
case CONFERENCE_LEFT:
|
|
store.dispatch(stopWaitForOwner());
|
|
break;
|
|
|
|
case CONNECTION_ESTABLISHED:
|
|
store.dispatch(hideLoginDialog());
|
|
break;
|
|
|
|
case CONNECTION_FAILED: {
|
|
const { error } = action;
|
|
const { getState } = store;
|
|
const state = getState();
|
|
const { jwt } = state['features/base/jwt'];
|
|
|
|
if (error
|
|
&& error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
|
|
&& typeof error.recoverable === 'undefined'
|
|
&& !jwt) {
|
|
error.recoverable = true;
|
|
|
|
_handleLogin(store);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case LOGIN: {
|
|
_handleLogin(store);
|
|
|
|
break;
|
|
}
|
|
|
|
case LOGOUT: {
|
|
_handleLogout(store);
|
|
|
|
break;
|
|
}
|
|
|
|
case APP_WILL_NAVIGATE: {
|
|
const { dispatch, getState } = store;
|
|
const state = getState();
|
|
const config = state['features/base/config'];
|
|
const room = state['features/base/conference'].room;
|
|
|
|
if (isRoomValid(room)
|
|
&& config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
|
|
&& state['features/authentication'].tokenAuthUrlSuccessful
|
|
&& !state['features/base/jwt'].jwt) {
|
|
// if we have auto redirect enabled, and we have previously logged in successfully
|
|
// we will redirect to the auth url to get the token and login again
|
|
// we want to mark token auth success to false as if login is unsuccessful
|
|
// the participant can join anonymously and not go in login loop
|
|
dispatch(setTokenAuthUrlSuccess(false));
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case STOP_WAIT_FOR_OWNER:
|
|
_clearExistingWaitForOwnerTimeout(store);
|
|
store.dispatch(hideDialog(WaitForOwnerDialog));
|
|
break;
|
|
|
|
case UPGRADE_ROLE_FINISHED: {
|
|
const { error, progress } = action;
|
|
|
|
if (!error && progress === 1) {
|
|
store.dispatch(hideLoginDialog());
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WAIT_FOR_OWNER: {
|
|
_clearExistingWaitForOwnerTimeout(store);
|
|
|
|
const { handler, timeoutMs }: { handler: () => void; timeoutMs: number; } = action;
|
|
|
|
action.waitForOwnerTimeoutID = setTimeout(handler, timeoutMs);
|
|
|
|
// The WAIT_FOR_OWNER action is cyclic, and we don't want to hide the
|
|
// login dialog every few seconds.
|
|
isDialogOpen(store, LoginDialog)
|
|
|| store.dispatch(openWaitForOwnerDialog());
|
|
break;
|
|
}
|
|
}
|
|
|
|
return next(action);
|
|
});
|
|
|
|
/**
|
|
* Will clear the wait for conference owner timeout handler if any is currently
|
|
* set.
|
|
*
|
|
* @param {Object} store - The redux store.
|
|
* @returns {void}
|
|
*/
|
|
function _clearExistingWaitForOwnerTimeout({ getState }: IStore) {
|
|
const { waitForOwnerTimeoutID } = getState()['features/authentication'];
|
|
|
|
waitForOwnerTimeoutID && clearTimeout(waitForOwnerTimeoutID);
|
|
}
|
|
|
|
|
|
/**
|
|
* Checks if the cyclic "wait for conference owner" task is currently scheduled.
|
|
*
|
|
* @param {Object} store - The redux store.
|
|
* @returns {boolean}
|
|
*/
|
|
function _isWaitingForOwner({ getState }: IStore) {
|
|
return Boolean(getState()['features/authentication'].waitForOwnerTimeoutID);
|
|
}
|
|
|
|
/**
|
|
* Handles login challenge. Opens login dialog or redirects to token auth URL.
|
|
*
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
* is being dispatched.
|
|
* @returns {void}
|
|
*/
|
|
function _handleLogin({ dispatch, getState }: IStore) {
|
|
const state = getState();
|
|
const config = state['features/base/config'];
|
|
const room = state['features/base/conference'].room;
|
|
const { locationURL = { href: '' } as URL } = state['features/base/connection'];
|
|
const { tenant } = parseURIString(locationURL.href) || {};
|
|
const { enabled: audioOnlyEnabled } = state['features/base/audio-only'];
|
|
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
|
|
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
|
|
|
if (!room) {
|
|
logger.warn('Cannot handle login, room is undefined!');
|
|
|
|
return;
|
|
}
|
|
|
|
if (!isTokenAuthEnabled(config)) {
|
|
dispatch(openLoginDialog());
|
|
|
|
return;
|
|
}
|
|
|
|
getTokenAuthUrl(
|
|
config,
|
|
locationURL,
|
|
{
|
|
audioMuted,
|
|
audioOnlyEnabled,
|
|
skipPrejoin: true,
|
|
videoMuted
|
|
},
|
|
room,
|
|
tenant
|
|
)
|
|
.then((tokenAuthServiceUrl: string | undefined) => {
|
|
if (!tokenAuthServiceUrl) {
|
|
logger.warn('Cannot handle login, token service URL is not set');
|
|
|
|
return;
|
|
}
|
|
|
|
return dispatch(openTokenAuthUrl(tokenAuthServiceUrl));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handles logout challenge. Opens logout dialog and hangs up the conference.
|
|
*
|
|
* @param {Store} store - The redux store in which the specified {@code action}
|
|
* is being dispatched.
|
|
* @param {string} logoutUrl - The url for logging out.
|
|
* @returns {void}
|
|
*/
|
|
function _handleLogout({ dispatch, getState }: IStore) {
|
|
const state = getState();
|
|
const { conference } = state['features/base/conference'];
|
|
|
|
if (!conference) {
|
|
return;
|
|
}
|
|
|
|
dispatch(openLogoutDialog());
|
|
}
|