snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
108
react/features/authentication/actionTypes.ts
Normal file
108
react/features/authentication/actionTypes.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* The type of (redux) action which signals that {@link LoginDialog} has been
|
||||
* canceled.
|
||||
*
|
||||
* {
|
||||
* type: CANCEL_LOGIN
|
||||
* }
|
||||
*/
|
||||
export const CANCEL_LOGIN = 'CANCEL_LOGIN';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals to login.
|
||||
*
|
||||
* {
|
||||
* type: LOGOUT
|
||||
* }
|
||||
*/
|
||||
export const LOGIN = 'LOGIN';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals to logout.
|
||||
*
|
||||
* {
|
||||
* type: LOGOUT
|
||||
* }
|
||||
*/
|
||||
export const LOGOUT = 'LOGOUT';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that an external token
|
||||
* authentication (started by opening {@code tokenAuthUrl} in the system
|
||||
* browser) is pending or has finished. Native only.
|
||||
*
|
||||
* {
|
||||
* type: SET_TOKEN_AUTH_PENDING,
|
||||
* pending: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_TOKEN_AUTH_PENDING = 'SET_TOKEN_AUTH_PENDING';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the cyclic operation of waiting
|
||||
* for conference owner has been aborted.
|
||||
*
|
||||
* {
|
||||
* type: STOP_WAIT_FOR_OWNER
|
||||
* }
|
||||
*/
|
||||
export const STOP_WAIT_FOR_OWNER = 'STOP_WAIT_FOR_OWNER';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which disables moderator login.
|
||||
*
|
||||
* {
|
||||
* type: DISABLE_MODERATOR_LOGIN
|
||||
* }
|
||||
*/
|
||||
export const DISABLE_MODERATOR_LOGIN = 'DISABLE_MODERATOR_LOGIN';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which informs that the authentication and role
|
||||
* upgrade process has finished either with success or with a specific error.
|
||||
* If {@code error} is {@code undefined}, then the process succeeded;
|
||||
* otherwise, it failed. Refer to
|
||||
* {@link JitsiConference#authenticateAndUpgradeRole} in lib-jitsi-meet for the
|
||||
* error details.
|
||||
*
|
||||
* {
|
||||
* type: UPGRADE_ROLE_FINISHED,
|
||||
* error: Object,
|
||||
* progress: number,
|
||||
* thenableWithCancel: Object
|
||||
* }
|
||||
*/
|
||||
export const UPGRADE_ROLE_FINISHED = 'UPGRADE_ROLE_FINISHED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that the process of authenticating
|
||||
* and upgrading the local participant's role has been started.
|
||||
*
|
||||
* {
|
||||
* type: UPGRADE_ROLE_STARTED,
|
||||
* thenableWithCancel: Object
|
||||
* }
|
||||
*/
|
||||
export const UPGRADE_ROLE_STARTED = 'UPGRADE_ROLE_STARTED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which enables moderator login.
|
||||
*
|
||||
* {
|
||||
* type: ENABLE_MODERATOR_LOGIN
|
||||
* }
|
||||
*/
|
||||
export const ENABLE_MODERATOR_LOGIN = 'ENABLE_MODERATOR_LOGIN';
|
||||
|
||||
/**
|
||||
* The type of (redux) action that sets delayed handler which will check if
|
||||
* the conference has been created and it's now possible to join from anonymous
|
||||
* connection.
|
||||
*
|
||||
* {
|
||||
* type: WAIT_FOR_OWNER,
|
||||
* handler: Function,
|
||||
* timeoutMs: number
|
||||
* }
|
||||
*/
|
||||
export const WAIT_FOR_OWNER = 'WAIT_FOR_OWNER';
|
||||
243
react/features/authentication/actions.any.ts
Normal file
243
react/features/authentication/actions.any.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { checkIfCanJoin } from '../base/conference/actions';
|
||||
import { IJitsiConference } from '../base/conference/reducer';
|
||||
import { hideDialog, openDialog } from '../base/dialog/actions';
|
||||
|
||||
import {
|
||||
DISABLE_MODERATOR_LOGIN,
|
||||
ENABLE_MODERATOR_LOGIN,
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
STOP_WAIT_FOR_OWNER,
|
||||
UPGRADE_ROLE_FINISHED,
|
||||
UPGRADE_ROLE_STARTED,
|
||||
WAIT_FOR_OWNER
|
||||
} from './actionTypes';
|
||||
import { LoginDialog, WaitForOwnerDialog } from './components';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Initiates authenticating and upgrading the role of the local participant to
|
||||
* moderator which will allow to create and join a new conference on an XMPP
|
||||
* password + guest access configuration. Refer to {@link LoginDialog} for more
|
||||
* info.
|
||||
*
|
||||
* @param {string} id - The XMPP user's ID (e.g. {@code user@domain.com}).
|
||||
* @param {string} password - The XMPP user's password.
|
||||
* @param {JitsiConference} conference - The conference for which the local
|
||||
* participant's role will be upgraded.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function authenticateAndUpgradeRole(
|
||||
id: string,
|
||||
password: string,
|
||||
conference: IJitsiConference) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
const process
|
||||
= conference.authenticateAndUpgradeRole({
|
||||
id,
|
||||
password,
|
||||
|
||||
onLoginSuccessful() {
|
||||
// When the login succeeds, the process has completed half
|
||||
// of its job (i.e. 0.5).
|
||||
return dispatch(_upgradeRoleFinished(process, 0.5));
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(_upgradeRoleStarted(process));
|
||||
process.then(
|
||||
/* onFulfilled */ () => dispatch(_upgradeRoleFinished(process, 1)),
|
||||
/* onRejected */ (error: any) => {
|
||||
// The lack of an error signals a cancellation.
|
||||
if (error.authenticationError || error.connectionError) {
|
||||
logger.error('authenticateAndUpgradeRole failed', error);
|
||||
}
|
||||
|
||||
dispatch(_upgradeRoleFinished(process, error));
|
||||
});
|
||||
|
||||
return process;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Signals that the process of authenticating and upgrading the local
|
||||
* participant's role has finished either with success or with a specific error.
|
||||
*
|
||||
* @param {Object} thenableWithCancel - The process of authenticating and
|
||||
* upgrading the local participant's role.
|
||||
* @param {Object} progressOrError - If the value is a {@code number}, then the
|
||||
* process of authenticating and upgrading the local participant's role has
|
||||
* succeeded in one of its two/multiple steps; otherwise, it has failed with the
|
||||
* specified error. Refer to {@link JitsiConference#authenticateAndUpgradeRole}
|
||||
* in lib-jitsi-meet for the error details.
|
||||
* @private
|
||||
* @returns {{
|
||||
* type: UPGRADE_ROLE_FINISHED,
|
||||
* error: ?Object,
|
||||
* progress: number
|
||||
* }}
|
||||
*/
|
||||
function _upgradeRoleFinished(
|
||||
thenableWithCancel: Object,
|
||||
progressOrError: number | any) {
|
||||
let error;
|
||||
let progress;
|
||||
|
||||
if (typeof progressOrError === 'number') {
|
||||
progress = progressOrError;
|
||||
} else {
|
||||
// Make the specified error object resemble an Error instance (to the
|
||||
// extent that jitsi-meet needs it).
|
||||
const {
|
||||
authenticationError,
|
||||
connectionError,
|
||||
...other
|
||||
} = progressOrError;
|
||||
|
||||
error = {
|
||||
name: authenticationError || connectionError,
|
||||
...other
|
||||
};
|
||||
progress = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
type: UPGRADE_ROLE_FINISHED,
|
||||
error,
|
||||
progress,
|
||||
thenableWithCancel
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a process of authenticating and upgrading the local
|
||||
* participant's role has started.
|
||||
*
|
||||
* @param {Object} thenableWithCancel - The process of authenticating and
|
||||
* upgrading the local participant's role.
|
||||
* @private
|
||||
* @returns {{
|
||||
* type: UPGRADE_ROLE_STARTED,
|
||||
* thenableWithCancel: Object
|
||||
* }}
|
||||
*/
|
||||
function _upgradeRoleStarted(thenableWithCancel: Object) {
|
||||
return {
|
||||
type: UPGRADE_ROLE_STARTED,
|
||||
thenableWithCancel
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides an authentication dialog where the local participant
|
||||
* should authenticate.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hideLoginDialog() {
|
||||
return hideDialog('LoginDialog', LoginDialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login.
|
||||
*
|
||||
* @returns {{
|
||||
* type: LOGIN
|
||||
* }}
|
||||
*/
|
||||
export function login() {
|
||||
return {
|
||||
type: LOGIN
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout.
|
||||
*
|
||||
* @returns {{
|
||||
* type: LOGOUT
|
||||
* }}
|
||||
*/
|
||||
export function logout() {
|
||||
return {
|
||||
type: LOGOUT
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables moderator login.
|
||||
*
|
||||
* @returns {{
|
||||
* type: DISABLE_MODERATOR_LOGIN
|
||||
* }}
|
||||
*/
|
||||
export function disableModeratorLogin() {
|
||||
return {
|
||||
type: DISABLE_MODERATOR_LOGIN
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables moderator login.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function enableModeratorLogin() {
|
||||
return {
|
||||
type: ENABLE_MODERATOR_LOGIN
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@link WaitForOnwerDialog}.
|
||||
*
|
||||
* @protected
|
||||
* @returns {Action}
|
||||
*/
|
||||
export function openWaitForOwnerDialog() {
|
||||
return openDialog('WaitForOwnerDialog', WaitForOwnerDialog);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stops waiting for the conference owner.
|
||||
*
|
||||
* @returns {{
|
||||
* type: STOP_WAIT_FOR_OWNER
|
||||
* }}
|
||||
*/
|
||||
export function stopWaitForOwner() {
|
||||
return {
|
||||
type: STOP_WAIT_FOR_OWNER
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when Jicofo rejects to create the room for anonymous user. Will
|
||||
* start the process of "waiting for the owner" by periodically trying to join
|
||||
* the room every five seconds.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function waitForOwner() {
|
||||
return (dispatch: IStore['dispatch']) =>
|
||||
dispatch({
|
||||
type: WAIT_FOR_OWNER,
|
||||
handler: () => dispatch(checkIfCanJoin()),
|
||||
timeoutMs: 5000
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@link LoginDialog} which will ask to enter username and password
|
||||
* for the current conference.
|
||||
*
|
||||
* @protected
|
||||
* @returns {Action}
|
||||
*/
|
||||
export function openLoginDialog() {
|
||||
return openDialog('LoginDialog', LoginDialog);
|
||||
}
|
||||
161
react/features/authentication/actions.native.ts
Normal file
161
react/features/authentication/actions.native.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { Linking } from 'react-native';
|
||||
|
||||
import { appNavigate } from '../app/actions.native';
|
||||
import { IStore } from '../app/types';
|
||||
import { conferenceLeft } from '../base/conference/actions';
|
||||
import { connectionFailed } from '../base/connection/actions.native';
|
||||
import { hideDialog, openDialog } from '../base/dialog/actions';
|
||||
import { set } from '../base/redux/functions';
|
||||
|
||||
import { CANCEL_LOGIN, SET_TOKEN_AUTH_PENDING } from './actionTypes';
|
||||
import { stopWaitForOwner } from './actions.any';
|
||||
import LoginRetryDialog from './components/native/LoginRetryDialog';
|
||||
import logger from './logger';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Cancels {@ink LoginDialog}.
|
||||
*
|
||||
* @returns {{
|
||||
* type: CANCEL_LOGIN
|
||||
* }}
|
||||
*/
|
||||
export function cancelLogin() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
dispatch({ type: CANCEL_LOGIN });
|
||||
|
||||
// XXX The error associated with CONNECTION_FAILED was marked as
|
||||
// recoverable by the authentication feature and, consequently,
|
||||
// recoverable-aware features such as mobile's external-api did not
|
||||
// deliver the CONFERENCE_FAILED to the SDK clients/consumers (as
|
||||
// a reaction to CONNECTION_FAILED). Since the
|
||||
// app/user is going to navigate to WelcomePage, the SDK
|
||||
// clients/consumers need an event.
|
||||
const { error = { recoverable: undefined }, passwordRequired }
|
||||
= getState()['features/base/connection'];
|
||||
|
||||
passwordRequired
|
||||
&& dispatch(
|
||||
connectionFailed(
|
||||
passwordRequired,
|
||||
set(error, 'recoverable', false) as any));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels {@link WaitForOwnerDialog}. Will navigate back to the welcome page.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function cancelWaitForOwner() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
dispatch(stopWaitForOwner());
|
||||
|
||||
// XXX The error associated with CONFERENCE_FAILED was marked as
|
||||
// recoverable by the feature room-lock and, consequently,
|
||||
// recoverable-aware features such as mobile's external-api did not
|
||||
// deliver the CONFERENCE_FAILED to the SDK clients/consumers. Since the
|
||||
// app/user is going to navigate to WelcomePage, the SDK
|
||||
// clients/consumers need an event.
|
||||
const { authRequired } = getState()['features/base/conference'];
|
||||
|
||||
if (authRequired) {
|
||||
dispatch(conferenceLeft(authRequired));
|
||||
|
||||
// in case we are showing lobby and on top of it wait for owner
|
||||
// we do not want to navigate away from the conference
|
||||
dispatch(appNavigate(undefined));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides {@link LoginRetryDialog}.
|
||||
*
|
||||
* @returns {Action}
|
||||
*/
|
||||
export function hideLoginRetryDialog() {
|
||||
return hideDialog('LoginRetryDialog', LoginRetryDialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens {@link LoginRetryDialog} which warns the user that they are not
|
||||
* connected to the meeting because the external login was not completed.
|
||||
*
|
||||
* @returns {Action}
|
||||
*/
|
||||
export function openLoginRetryDialog() {
|
||||
return openDialog('LoginRetryDialog', LoginRetryDialog);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the default location (e.g. Welcome page).
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function redirectToDefaultLocation() {
|
||||
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether an external token authentication (started by opening
|
||||
* {@code tokenAuthUrl} in the system browser) is pending. While the flag is
|
||||
* set, returning to the foreground without a connection means the login was
|
||||
* abandoned and {@link LoginRetryDialog} is shown.
|
||||
*
|
||||
* @param {boolean} pending - Whether the external login is pending.
|
||||
* @returns {{
|
||||
* type: SET_TOKEN_AUTH_PENDING,
|
||||
* pending: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setTokenAuthPending(pending: boolean) {
|
||||
return {
|
||||
type: SET_TOKEN_AUTH_PENDING,
|
||||
pending
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens token auth URL page.
|
||||
*
|
||||
* @param {string} tokenAuthServiceUrl - Authentication service URL.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openTokenAuthUrl(tokenAuthServiceUrl: string) {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
dispatch(setTokenAuthPending(true));
|
||||
|
||||
// Show the dialog right away, behind the browser: if the login is
|
||||
// completed, the deep link triggers a new connection attempt which
|
||||
// dismisses it; if the login is abandoned, it is what the user finds
|
||||
// when they return. The app may be put into picture-in-picture while
|
||||
// the browser is up, in which case no AppState change is emitted on
|
||||
// return, so the dialog cannot be reliably shown at that point.
|
||||
dispatch(openLoginRetryDialog());
|
||||
|
||||
Linking.openURL(tokenAuthServiceUrl)
|
||||
.catch(error => logger.error('Failed to open token auth URL', error));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Not used.
|
||||
*
|
||||
* @param {string} tokenAuthServiceUrl - Authentication service URL.
|
||||
* @returns {Promise<any>} Resolves.
|
||||
*/
|
||||
export function loginWithPopup(tokenAuthServiceUrl: string): Promise<any> {
|
||||
return Promise.resolve(tokenAuthServiceUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Not used. There is no inline login popup on native.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function closeLoginPopup() {
|
||||
// No-op on native.
|
||||
}
|
||||
359
react/features/authentication/actions.web.ts
Normal file
359
react/features/authentication/actions.web.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom error thrown when a pending inline login popup is closed
|
||||
* programmatically (e.g. the participant is admitted to the conference while
|
||||
* the popup is still open), as opposed to failing to authenticate.
|
||||
*/
|
||||
class LoginCancelledError extends Error {
|
||||
constructor(message: string = 'Login cancelled') {
|
||||
super(message);
|
||||
this.name = 'LoginCancelledError';
|
||||
Error.captureStackTrace(this, LoginCancelledError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds a function that closes the currently open inline login popup (if any)
|
||||
* and settles its pending promise. It is set while a popup is open and cleared
|
||||
* once the popup is closed, so it can be used to dismiss the popup when it is no
|
||||
* longer needed (e.g. when the participant is admitted to the conference).
|
||||
*/
|
||||
let closeActiveLoginPopup: (() => void) | undefined;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
closeActiveLoginPopup = undefined;
|
||||
|
||||
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);
|
||||
|
||||
// Expose a way to close this popup externally, so it can be dismissed
|
||||
// when it is no longer needed (e.g. the participant was admitted to the
|
||||
// conference while the popup was still open).
|
||||
closeActiveLoginPopup = () => {
|
||||
cleanup(handler);
|
||||
reject(new LoginCancelledError());
|
||||
};
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the currently open inline login popup (if any) and settles its pending
|
||||
* promise. Used to dismiss the popup when it is no longer needed, e.g. when the
|
||||
* participant is admitted to the conference while the popup is still open.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
export function closeLoginPopup() {
|
||||
closeActiveLoginPopup?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
logger.error(err);
|
||||
} else if (err instanceof LoginCancelledError) {
|
||||
// The popup was closed programmatically (e.g. we were
|
||||
// admitted to the conference while it was still open); this
|
||||
// is not an authentication failure, so nothing to report.
|
||||
logger.info('Inline login popup closed:', err.message);
|
||||
} 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
2
react/features/authentication/components/index.native.ts
Normal file
2
react/features/authentication/components/index.native.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as LoginDialog } from './native/LoginDialog';
|
||||
export { default as WaitForOwnerDialog } from './native/WaitForOwnerDialog';
|
||||
2
react/features/authentication/components/index.web.ts
Normal file
2
react/features/authentication/components/index.web.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as LoginDialog } from './web/LoginDialog';
|
||||
export { default as WaitForOwnerDialog } from './web/WaitForOwnerDialog';
|
||||
318
react/features/authentication/components/native/LoginDialog.tsx
Normal file
318
react/features/authentication/components/native/LoginDialog.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import React, { Component } from 'react';
|
||||
import Dialog from 'react-native-dialog';
|
||||
import { connect as reduxConnect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { IJitsiConference } from '../../../base/conference/reducer';
|
||||
import { connect } from '../../../base/connection/actions.native';
|
||||
import { toJid } from '../../../base/connection/functions';
|
||||
import { _abstractMapStateToProps } from '../../../base/dialog/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet';
|
||||
import { authenticateAndUpgradeRole, cancelLogin } from '../../actions.native';
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link LoginDialog}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* {@link JitsiConference} That needs authentication - will hold a valid
|
||||
* value in XMPP login + guest access mode.
|
||||
*/
|
||||
_conference?: IJitsiConference;
|
||||
|
||||
/**
|
||||
* The server hosts specified in the global config.
|
||||
*/
|
||||
_configHosts?: {
|
||||
anonymousdomain?: string;
|
||||
authdomain?: string;
|
||||
domain: string;
|
||||
focus?: string;
|
||||
muc: string;
|
||||
visitorFocus?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates if the dialog should display "connecting" status message.
|
||||
*/
|
||||
_connecting: boolean;
|
||||
|
||||
/**
|
||||
* The error which occurred during login/authentication.
|
||||
*/
|
||||
_error: any;
|
||||
|
||||
/**
|
||||
* The progress in the floating range between 0 and 1 of the authenticating
|
||||
* and upgrading the role of the local participant/user.
|
||||
*/
|
||||
_progress?: number;
|
||||
|
||||
/**
|
||||
* Redux store dispatch method.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} state of {@link LoginDialog}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* The user entered password for the conference.
|
||||
*/
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* The user entered local participant name.
|
||||
*/
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog asks user for username and password.
|
||||
*
|
||||
* First authentication configuration that it will deal with is the main XMPP
|
||||
* domain (config.hosts.domain) with password authentication. A LoginDialog
|
||||
* will be opened after 'CONNECTION_FAILED' action with
|
||||
* 'JitsiConnectionErrors.PASSWORD_REQUIRED' error. After username and password
|
||||
* are entered a new 'connect' action from 'features/base/connection' will be
|
||||
* triggered which will result in new XMPP connection. The conference will start
|
||||
* if the credentials are correct.
|
||||
*
|
||||
* The second setup is the main XMPP domain with password plus guest domain with
|
||||
* anonymous access configured under 'config.hosts.anonymousdomain'. In such
|
||||
* case user connects from the anonymous domain, but if the room does not exist
|
||||
* yet, Jicofo will not allow to start new conference. This will trigger
|
||||
* 'CONFERENCE_FAILED' action with JitsiConferenceErrors.AUTHENTICATION_REQUIRED
|
||||
* error and 'authRequired' value of 'features/base/conference' will hold
|
||||
* the {@link JitsiConference} instance. If user decides to authenticate, a
|
||||
* new/separate XMPP connection is established and authentication is performed.
|
||||
* In case it succeeds, Jicofo will assign new session ID which then can be used
|
||||
* from the anonymous domain connection to create and join the room. This part
|
||||
* is done by {@link JitsiConference#authenticateAndUpgradeRole} in
|
||||
* lib-jitsi-meet.
|
||||
*
|
||||
* See {@link https://github.com/jitsi/jicofo#secure-domain} for a description
|
||||
* of the configuration parameters.
|
||||
*/
|
||||
class LoginDialog extends Component<IProps, IState> {
|
||||
/**
|
||||
* Initializes a new LoginDialog instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
username: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onLogin = this._onLogin.bind(this);
|
||||
this._onPasswordChange = this._onPasswordChange.bind(this);
|
||||
this._onUsernameChange = this._onUsernameChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_connecting: connecting,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Dialog.Container
|
||||
coverScreen = { false }
|
||||
visible = { true }>
|
||||
<Dialog.Title>
|
||||
{ t('dialog.login') }
|
||||
</Dialog.Title>
|
||||
<Dialog.Input
|
||||
autoCapitalize = { 'none' }
|
||||
autoCorrect = { false }
|
||||
onChangeText = { this._onUsernameChange }
|
||||
placeholder = { 'user@domain.com' }
|
||||
spellCheck = { false }
|
||||
value = { this.state.username } />
|
||||
<Dialog.Input
|
||||
autoCapitalize = { 'none' }
|
||||
onChangeText = { this._onPasswordChange }
|
||||
placeholder = { t('dialog.userPassword') }
|
||||
secureTextEntry = { true }
|
||||
value = { this.state.password } />
|
||||
<Dialog.Description>
|
||||
{ this._renderMessage() }
|
||||
</Dialog.Description>
|
||||
<Dialog.Button
|
||||
label = { t('dialog.Cancel') }
|
||||
onPress = { this._onCancel } />
|
||||
<Dialog.Button
|
||||
disabled = { connecting }
|
||||
label = { t('dialog.Ok') }
|
||||
onPress = { this._onLogin } />
|
||||
</Dialog.Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an optional message, if applicable.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
_renderMessage() {
|
||||
const {
|
||||
_connecting: connecting,
|
||||
_error: error,
|
||||
_progress: progress,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
let messageKey;
|
||||
const messageOptions = { msg: '' };
|
||||
|
||||
if (progress && progress < 1) {
|
||||
messageKey = 'connection.FETCH_SESSION_ID';
|
||||
} else if (error) {
|
||||
const { name } = error;
|
||||
|
||||
if (name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
|
||||
// Show a message that the credentials are incorrect only if the
|
||||
// credentials which have caused the connection to fail are the
|
||||
// ones which the user sees.
|
||||
const { credentials } = error;
|
||||
|
||||
if (credentials
|
||||
&& credentials.jid
|
||||
=== toJid(
|
||||
this.state.username,
|
||||
this.props._configHosts ?? {})
|
||||
&& credentials.password === this.state.password) {
|
||||
messageKey = 'dialog.incorrectPassword';
|
||||
}
|
||||
} else if (name) {
|
||||
messageKey = 'dialog.connectErrorWithMsg';
|
||||
messageOptions.msg = `${name} ${error.message}`;
|
||||
}
|
||||
} else if (connecting) {
|
||||
messageKey = 'connection.CONNECTING';
|
||||
}
|
||||
|
||||
if (messageKey) {
|
||||
return t(messageKey, messageOptions);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user edits the username.
|
||||
*
|
||||
* @param {string} text - A new username value entered by user.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
_onUsernameChange(text: string) {
|
||||
this.setState({
|
||||
username: text.trim()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user edits the password.
|
||||
*
|
||||
* @param {string} text - A new password value entered by user.
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
_onPasswordChange(text: string) {
|
||||
this.setState({
|
||||
password: text
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this LoginDialog that it has been dismissed by cancel.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
this.props.dispatch(cancelLogin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this LoginDialog that the login button (OK) has been pressed by
|
||||
* the user.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLogin() {
|
||||
const { _conference: conference, dispatch } = this.props;
|
||||
const { password, username } = this.state;
|
||||
const jid = toJid(username, this.props._configHosts ?? {});
|
||||
let r;
|
||||
|
||||
// If there's a conference it means that the connection has succeeded,
|
||||
// but authentication is required in order to join the room.
|
||||
if (conference) {
|
||||
r = dispatch(authenticateAndUpgradeRole(jid, password, conference));
|
||||
} else {
|
||||
r = dispatch(connect(jid, password));
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code LoginDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
const {
|
||||
error: authenticateAndUpgradeRoleError,
|
||||
progress,
|
||||
thenableWithCancel
|
||||
} = state['features/authentication'];
|
||||
const { authRequired, conference } = state['features/base/conference'];
|
||||
const { hosts: configHosts } = state['features/base/config'];
|
||||
const {
|
||||
connecting,
|
||||
error: connectionError
|
||||
} = state['features/base/connection'];
|
||||
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
_conference: authRequired || conference,
|
||||
_configHosts: configHosts,
|
||||
_connecting: Boolean(connecting) || Boolean(thenableWithCancel),
|
||||
_error: connectionError || authenticateAndUpgradeRoleError,
|
||||
_progress: progress
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(reduxConnect(_mapStateToProps)(LoginDialog));
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
|
||||
import { cancelLogin, login, redirectToDefaultLocation } from '../../actions.native';
|
||||
|
||||
/**
|
||||
* Dialog shown when the app returns to the foreground while an external token
|
||||
* authentication (started by opening {@code tokenAuthUrl} in the system
|
||||
* browser) was not completed. It warns the user that they are not connected
|
||||
* to the meeting and offers to either retry the login or leave.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function LoginRetryDialog(): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
const handleSubmit = useCallback(() => {
|
||||
dispatch(login());
|
||||
|
||||
return true; // close dialog
|
||||
}, [ dispatch ]);
|
||||
const handleCancel = useCallback(() => {
|
||||
dispatch(cancelLogin());
|
||||
|
||||
// cancelLogin() only navigates away when the connection is still
|
||||
// parked on passwordRequired; on WebSocket deployments that state is
|
||||
// reset by the subsequent connection-dropped failure, so leave the
|
||||
// dead conference screen explicitly.
|
||||
dispatch(redirectToDefaultLocation());
|
||||
|
||||
return true; // close dialog
|
||||
}, [ dispatch ]);
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelLabel = 'toolbar.leaveConference'
|
||||
confirmLabel = 'dialog.login'
|
||||
descriptionKey = 'dialog.loginNotCompleted'
|
||||
onCancel = { handleCancel }
|
||||
onSubmit = { handleSubmit } />
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { cancelWaitForOwner, login } from '../../actions.native';
|
||||
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link WaitForOwnerDialog}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Whether to show alternative cancel button text.
|
||||
*/
|
||||
_alternativeCancelText?: boolean;
|
||||
|
||||
/**
|
||||
* Is confirm button hidden?
|
||||
*/
|
||||
_isConfirmHidden?: boolean;
|
||||
|
||||
/**
|
||||
* Redux store dispatch function.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* The dialog is display in XMPP password + guest access configuration, after
|
||||
* user connects from anonymous domain and the conference does not exist yet.
|
||||
*
|
||||
* See {@link LoginDialog} description for more details.
|
||||
*/
|
||||
class WaitForOwnerDialog extends Component<IProps> {
|
||||
/**
|
||||
* Initializes a new WaitForWonderDialog instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onLogin = this._onLogin.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { _isConfirmHidden } = this.props;
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelLabel = { this.props._alternativeCancelText ? 'dialog.WaitingForHostButton' : 'dialog.Cancel' }
|
||||
confirmLabel = 'dialog.IamHost'
|
||||
descriptionKey = 'lobby.waitForModerator'
|
||||
isConfirmHidden = { _isConfirmHidden }
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onLogin } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the cancel button is clicked.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancel() {
|
||||
this.props.dispatch(cancelWaitForOwner());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the OK button is clicked.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLogin() {
|
||||
this.props.dispatch(login());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated
|
||||
* {@code WaitForOwnerDialog}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
|
||||
return {
|
||||
_alternativeCancelText: membersOnly && lobbyWaitingForHost,
|
||||
_isConfirmHidden: locationURL?.hostname?.includes('8x8.vc')
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(WaitForOwnerDialog));
|
||||
299
react/features/authentication/components/web/LoginDialog.tsx
Normal file
299
react/features/authentication/components/web/LoginDialog.tsx
Normal file
@@ -0,0 +1,299 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect as reduxConnect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { IJitsiConference } from '../../../base/conference/reducer';
|
||||
import { IConfig } from '../../../base/config/configType';
|
||||
import { connect } from '../../../base/connection/actions.web';
|
||||
import { toJid } from '../../../base/connection/functions';
|
||||
import { translate, translateToHTML } from '../../../base/i18n/functions';
|
||||
import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import Input from '../../../base/ui/components/web/Input';
|
||||
import {
|
||||
authenticateAndUpgradeRole,
|
||||
cancelLogin
|
||||
} from '../../actions.web';
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link LoginDialog}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* {@link JitsiConference} That needs authentication - will hold a valid
|
||||
* value in XMPP login + guest access mode.
|
||||
*/
|
||||
_conference?: IJitsiConference;
|
||||
|
||||
/**
|
||||
* The server hosts specified in the global config.
|
||||
*/
|
||||
_configHosts: IConfig['hosts'];
|
||||
|
||||
/**
|
||||
* Indicates if the dialog should display "connecting" status message.
|
||||
*/
|
||||
_connecting: boolean;
|
||||
|
||||
/**
|
||||
* The error which occurred during login/authentication.
|
||||
*/
|
||||
_error: any;
|
||||
|
||||
/**
|
||||
* The progress in the floating range between 0 and 1 of the authenticating
|
||||
* and upgrading the role of the local participant/user.
|
||||
*/
|
||||
_progress?: number;
|
||||
|
||||
/**
|
||||
* Redux store dispatch method.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Conference room name.
|
||||
*/
|
||||
roomName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of {@link LoginDialog}.
|
||||
*/
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* The user entered password for the conference.
|
||||
*/
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* The user entered local participant name.
|
||||
*/
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders the login in conference dialog.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
class LoginDialog extends Component<IProps, IState> {
|
||||
/**
|
||||
* Initializes a new {@code LoginDialog} instance.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
username: '',
|
||||
password: ''
|
||||
};
|
||||
|
||||
this._onCancelLogin = this._onCancelLogin.bind(this);
|
||||
this._onLogin = this._onLogin.bind(this);
|
||||
this._onUsernameChange = this._onUsernameChange.bind(this);
|
||||
this._onPasswordChange = this._onPasswordChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the cancel button is clicked.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancelLogin() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(cancelLogin());
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies this LoginDialog that the login button (OK) has been pressed by
|
||||
* the user.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onLogin() {
|
||||
const {
|
||||
_conference: conference,
|
||||
_configHosts: configHosts,
|
||||
dispatch
|
||||
} = this.props;
|
||||
const { password, username } = this.state;
|
||||
const jid = toJid(username, configHosts ?? {
|
||||
authdomain: '',
|
||||
domain: ''
|
||||
});
|
||||
|
||||
if (conference) {
|
||||
dispatch(authenticateAndUpgradeRole(jid, password, conference));
|
||||
} else {
|
||||
logger.info('Dispatching connect from LoginDialog.');
|
||||
dispatch(connect(jid, password));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the onChange event of the field.
|
||||
*
|
||||
* @param {string} value - The static event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPasswordChange(value: string) {
|
||||
this.setState({
|
||||
password: value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for the onChange event of the username input.
|
||||
*
|
||||
* @param {string} value - The new value.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onUsernameChange(value: string) {
|
||||
this.setState({
|
||||
username: value
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an optional message, if applicable.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
* @private
|
||||
*/
|
||||
renderMessage() {
|
||||
const {
|
||||
_configHosts: configHosts,
|
||||
_connecting: connecting,
|
||||
_error: error,
|
||||
_progress: progress,
|
||||
t
|
||||
} = this.props;
|
||||
const { username, password } = this.state;
|
||||
const messageOptions: { msg?: string; } = {};
|
||||
let messageKey;
|
||||
|
||||
if (progress && progress < 1) {
|
||||
messageKey = 'connection.FETCH_SESSION_ID';
|
||||
} else if (error) {
|
||||
const { name } = error;
|
||||
|
||||
if (name === JitsiConnectionErrors.PASSWORD_REQUIRED) {
|
||||
const { credentials } = error;
|
||||
|
||||
if (credentials
|
||||
&& credentials.jid === toJid(username, configHosts ?? { authdomain: '',
|
||||
domain: '' })
|
||||
&& credentials.password === password) {
|
||||
messageKey = 'dialog.incorrectPassword';
|
||||
}
|
||||
} else if (name) {
|
||||
messageKey = 'dialog.connectErrorWithMsg';
|
||||
messageOptions.msg = `${name} ${error.message}`;
|
||||
}
|
||||
} else if (connecting) {
|
||||
messageKey = 'connection.CONNECTING';
|
||||
}
|
||||
|
||||
if (messageKey) {
|
||||
return (
|
||||
<span>
|
||||
{ translateToHTML(t, messageKey, messageOptions) }
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@Component#render}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_connecting: connecting,
|
||||
t
|
||||
} = this.props;
|
||||
const { password, username } = this.state;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
disableAutoHideOnSubmit = { true }
|
||||
disableBackdropClose = { true }
|
||||
hideCloseButton = { true }
|
||||
ok = {{
|
||||
disabled: connecting
|
||||
|| !password
|
||||
|| !username,
|
||||
translationKey: 'dialog.login'
|
||||
}}
|
||||
onCancel = { this._onCancelLogin }
|
||||
onSubmit = { this._onLogin }
|
||||
titleKey = { t('dialog.authenticationRequired') }>
|
||||
<Input
|
||||
autoFocus = { true }
|
||||
id = 'login-dialog-username'
|
||||
label = { t('dialog.user') }
|
||||
name = 'username'
|
||||
onChange = { this._onUsernameChange }
|
||||
placeholder = { t('dialog.userIdentifier') }
|
||||
type = 'text'
|
||||
value = { username } />
|
||||
<br />
|
||||
<Input
|
||||
className = 'dialog-bottom-margin'
|
||||
id = 'login-dialog-password'
|
||||
label = { t('dialog.userPassword') }
|
||||
name = 'password'
|
||||
onChange = { this._onPasswordChange }
|
||||
placeholder = { t('dialog.password') }
|
||||
type = 'password'
|
||||
value = { password } />
|
||||
{ this.renderMessage() }
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the Redux state to the associated props for the
|
||||
* {@code LoginDialog} component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const {
|
||||
error: authenticateAndUpgradeRoleError,
|
||||
progress,
|
||||
thenableWithCancel
|
||||
} = state['features/authentication'];
|
||||
const { authRequired, conference } = state['features/base/conference'];
|
||||
const { hosts: configHosts } = state['features/base/config'];
|
||||
const {
|
||||
connecting,
|
||||
error: connectionError
|
||||
} = state['features/base/connection'];
|
||||
|
||||
return {
|
||||
_conference: authRequired || conference,
|
||||
_configHosts: configHosts,
|
||||
_connecting: Boolean(connecting) || Boolean(thenableWithCancel),
|
||||
_error: connectionError || authenticateAndUpgradeRoleError,
|
||||
_progress: progress
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(reduxConnect(mapStateToProps)(LoginDialog));
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
|
||||
/**
|
||||
* The type of {@link LoginQuestionDialog}'s React {@code Component} props.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The handler.
|
||||
*/
|
||||
handler: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the dialog that warns the user that the login will leave the conference.
|
||||
*
|
||||
* @param {Object} props - The props of the component.
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
const LoginQuestionDialog = ({ handler }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
ok = {{ translationKey: 'dialog.Yes' }}
|
||||
onSubmit = { handler }
|
||||
titleKey = { t('dialog.login') }>
|
||||
<div>
|
||||
{ t('dialog.loginQuestion') }
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginQuestionDialog;
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import { cancelWaitForOwner, login } from '../../actions.web';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link WaitForOwnerDialog}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Whether to show alternative cancel button text.
|
||||
*/
|
||||
_alternativeCancelText?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to hide the login button.
|
||||
*/
|
||||
_hideLoginButton?: boolean;
|
||||
|
||||
/**
|
||||
* Redux store dispatch method.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication message dialog for host confirmation.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
class WaitForOwnerDialog extends PureComponent<IProps> {
|
||||
/**
|
||||
* Instantiates a new component.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onCancelWaitForOwner = this._onCancelWaitForOwner.bind(this);
|
||||
this._onIAmHost = this._onIAmHost.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the cancel button is clicked.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onCancelWaitForOwner() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(cancelWaitForOwner());
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the OK button is clicked.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onIAmHost() {
|
||||
this.props.dispatch(login());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ translationKey:
|
||||
this.props._alternativeCancelText ? 'dialog.WaitingForHostButton' : 'dialog.Cancel' }}
|
||||
disableBackdropClose = { true }
|
||||
hideCloseButton = { true }
|
||||
ok = { this.props._hideLoginButton ? { hidden: true,
|
||||
disabled: true } : { translationKey: 'dialog.IamHost' } }
|
||||
onCancel = { this._onCancelWaitForOwner }
|
||||
onSubmit = { this._onIAmHost }
|
||||
titleKey = { t('dialog.WaitingForHostTitle') }>
|
||||
<span>
|
||||
{ this.props._hideLoginButton ? t('dialog.WaitForHostNoAuthMsg') : t('lobby.waitForModerator') }
|
||||
</span>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated
|
||||
* {@code WaitForOwnerDialog}'s props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
const { membersOnly, lobbyWaitingForHost } = state['features/base/conference'];
|
||||
const { hideLoginButton } = state['features/base/config'];
|
||||
|
||||
return {
|
||||
_alternativeCancelText: membersOnly && lobbyWaitingForHost,
|
||||
_hideLoginButton: hideLoginButton
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(WaitForOwnerDialog));
|
||||
104
react/features/authentication/functions.any.ts
Normal file
104
react/features/authentication/functions.any.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IConfig } from '../base/config/configType';
|
||||
import { parseURLParams } from '../base/util/parseURLParams';
|
||||
import { getBackendSafeRoomName } from '../base/util/uri';
|
||||
import { isVpaasMeeting } from '../jaas/functions';
|
||||
|
||||
/**
|
||||
* Checks if the token for authentication URL is available and the meeting is not jaas.
|
||||
*
|
||||
* @param {IReduxState} state - The state of the app.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isTokenAuthEnabled = (state: IReduxState): boolean => {
|
||||
const config = state['features/base/config'];
|
||||
|
||||
return typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length > 0
|
||||
&& !isVpaasMeeting(state);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the token authentication should be done inline.
|
||||
*
|
||||
* @param {Object} config - Configuration state object from store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isTokenAuthInline = (config: IConfig): boolean =>
|
||||
config.tokenAuthInline === true;
|
||||
|
||||
/**
|
||||
* Returns the state that we can add as a parameter to the tokenAuthUrl.
|
||||
*
|
||||
* @param {URL} locationURL - The location URL.
|
||||
* @param {Object} options: - Config options {
|
||||
* audioMuted: boolean | undefined
|
||||
* lowBandwidthModeEnabled: boolean | undefined,
|
||||
* skipPrejoin: boolean | undefined,
|
||||
* videoMuted: boolean | undefined
|
||||
* }.
|
||||
* @param {string?} roomName - The room name.
|
||||
* @param {string?} tenant - The tenant name if any.
|
||||
* @param {string?} refreshToken - The refresh token if available.
|
||||
*
|
||||
* @returns {Object} The state object.
|
||||
*/
|
||||
export const _getTokenAuthState = (
|
||||
locationURL: URL,
|
||||
options: {
|
||||
audioMuted: boolean | undefined;
|
||||
lowBandwidthModeEnabled: boolean | undefined;
|
||||
skipPrejoin: boolean | undefined;
|
||||
videoMuted: boolean | undefined;
|
||||
},
|
||||
roomName: string | undefined,
|
||||
tenant: string | undefined,
|
||||
refreshToken?: string): object => {
|
||||
const state = {
|
||||
refreshToken,
|
||||
room: roomName,
|
||||
roomSafe: getBackendSafeRoomName(roomName),
|
||||
tenant
|
||||
};
|
||||
|
||||
const {
|
||||
audioMuted = false,
|
||||
lowBandwidthModeEnabled = false,
|
||||
skipPrejoin = false,
|
||||
videoMuted = false
|
||||
} = options;
|
||||
|
||||
if (audioMuted) {
|
||||
|
||||
// @ts-ignore
|
||||
state['config.startWithAudioMuted'] = true;
|
||||
}
|
||||
|
||||
if (lowBandwidthModeEnabled) {
|
||||
|
||||
// @ts-ignore
|
||||
state['config.startLowBandwidthMode'] = true;
|
||||
}
|
||||
|
||||
if (skipPrejoin) {
|
||||
// We have already shown the prejoin screen, no need to show it again after obtaining the token.
|
||||
// @ts-ignore
|
||||
state['config.prejoinConfig.enabled'] = false;
|
||||
}
|
||||
|
||||
if (videoMuted) {
|
||||
|
||||
// @ts-ignore
|
||||
state['config.startWithVideoMuted'] = true;
|
||||
}
|
||||
const params = parseURLParams(locationURL);
|
||||
|
||||
for (const key of Object.keys(params)) {
|
||||
// we allow only config, interfaceConfig and iceServers overrides in the state
|
||||
if (key.startsWith('config.') || key.startsWith('interfaceConfig.') || key.startsWith('iceServers.')) {
|
||||
// @ts-ignore
|
||||
state[key] = params[key];
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
82
react/features/authentication/functions.native.ts
Normal file
82
react/features/authentication/functions.native.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
import { IConfig } from '../base/config/configType';
|
||||
|
||||
import { _getTokenAuthState } from './functions.any';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Creates the URL pointing to JWT token authentication service. It is
|
||||
* formatted from the 'urlPattern' argument which can contain the following
|
||||
* constants:
|
||||
* '{room}' - name of the conference room passed as <tt>roomName</tt>
|
||||
* argument to this method.
|
||||
*
|
||||
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
|
||||
* @param {URL} locationURL - The location URL.
|
||||
* @param {Object} options: - Config options {
|
||||
* audioMuted: boolean | undefined
|
||||
* lowBandwidthModeEnabled: boolean | undefined,
|
||||
* skipPrejoin: boolean | undefined,
|
||||
* videoMuted: boolean | undefined
|
||||
* }.
|
||||
* @param {string?} roomName - The room name.
|
||||
* @param {string?} tenant - The tenant name if any.
|
||||
* @param {string?} refreshToken - The refreshToken if any.
|
||||
*
|
||||
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
|
||||
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
|
||||
* constructed.
|
||||
*/
|
||||
export const getTokenAuthUrl = (
|
||||
config: IConfig,
|
||||
locationURL: URL,
|
||||
options: {
|
||||
audioMuted: boolean | undefined;
|
||||
lowBandwidthModeEnabled: boolean | undefined;
|
||||
skipPrejoin: boolean | undefined;
|
||||
videoMuted: boolean | undefined;
|
||||
},
|
||||
roomName: string | undefined,
|
||||
// eslint-disable-next-line max-params
|
||||
tenant: string | undefined,
|
||||
// eslint-disable-next-line max-params
|
||||
refreshToken?: string | undefined): Promise<string | undefined> => {
|
||||
|
||||
const {
|
||||
audioMuted = false,
|
||||
lowBandwidthModeEnabled = false,
|
||||
skipPrejoin = false,
|
||||
videoMuted = false
|
||||
} = options;
|
||||
|
||||
let url = config.tokenAuthUrl;
|
||||
|
||||
if (!url || !roomName) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
if (url.indexOf('{state}')) {
|
||||
const state = _getTokenAuthState(
|
||||
locationURL,
|
||||
{
|
||||
audioMuted,
|
||||
lowBandwidthModeEnabled,
|
||||
skipPrejoin,
|
||||
videoMuted
|
||||
},
|
||||
roomName,
|
||||
tenant,
|
||||
refreshToken
|
||||
);
|
||||
|
||||
// Append ios=true or android=true to the token URL.
|
||||
// @ts-ignore
|
||||
state[Platform.OS] = true;
|
||||
|
||||
url = url.replace('{state}', encodeURIComponent(JSON.stringify(state)));
|
||||
}
|
||||
|
||||
return Promise.resolve(url.replace('{room}', roomName));
|
||||
};
|
||||
136
react/features/authentication/functions.web.ts
Normal file
136
react/features/authentication/functions.web.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import base64js from 'base64-js';
|
||||
|
||||
import { IConfig } from '../base/config/configType';
|
||||
import { browser } from '../base/lib-jitsi-meet';
|
||||
|
||||
import { _getTokenAuthState } from './functions.any';
|
||||
import logger from './logger';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Based on rfc7636 we need a random string for a code verifier.
|
||||
*/
|
||||
const POSSIBLE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
/**
|
||||
* Crypto random, alternative of Math.random.
|
||||
*
|
||||
* @returns {float} A random value.
|
||||
*/
|
||||
function _cryptoRandom() {
|
||||
const typedArray = new Uint8Array(1);
|
||||
const randomValue = crypto.getRandomValues(typedArray)[0];
|
||||
|
||||
return randomValue / Math.pow(2, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the URL pointing to JWT token authentication service. It is
|
||||
* formatted from the 'urlPattern' argument which can contain the following
|
||||
* constants:
|
||||
* '{room}' - name of the conference room passed as <tt>roomName</tt>
|
||||
* argument to this method.
|
||||
*
|
||||
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
|
||||
* @param {URL} locationURL - The location URL.
|
||||
* @param {Object} options: - Config options {
|
||||
* audioMuted: boolean | undefined
|
||||
* lowBandwidthModeEnabled: boolean | undefined,
|
||||
* skipPrejoin: boolean | undefined,
|
||||
* videoMuted: boolean | undefined
|
||||
* }.
|
||||
* @param {string?} roomName - The room name.
|
||||
* @param {string?} tenant - The tenant name if any.
|
||||
* @param {string?} refreshToken - The refresh token if available.
|
||||
*
|
||||
* @returns {Promise<string|undefined>} - The URL pointing to JWT login service or
|
||||
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
|
||||
* constructed.
|
||||
*/
|
||||
export const getTokenAuthUrl = (
|
||||
config: IConfig,
|
||||
locationURL: URL,
|
||||
options: {
|
||||
audioMuted: boolean | undefined;
|
||||
lowBandwidthModeEnabled: boolean | undefined;
|
||||
skipPrejoin: boolean | undefined;
|
||||
videoMuted: boolean | undefined;
|
||||
},
|
||||
roomName: string | undefined,
|
||||
// eslint-disable max-params
|
||||
tenant: string | undefined,
|
||||
refreshToken?: string): Promise<string | undefined> => {
|
||||
// eslint-enable max-params
|
||||
const {
|
||||
audioMuted = false,
|
||||
lowBandwidthModeEnabled = false,
|
||||
skipPrejoin = false,
|
||||
videoMuted = false
|
||||
} = options;
|
||||
|
||||
let url = config.tokenAuthUrl;
|
||||
|
||||
if (!url || !roomName) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
if (url.indexOf('{state}')) {
|
||||
const state = _getTokenAuthState(
|
||||
locationURL,
|
||||
{
|
||||
audioMuted,
|
||||
lowBandwidthModeEnabled,
|
||||
skipPrejoin,
|
||||
videoMuted
|
||||
},
|
||||
roomName,
|
||||
tenant,
|
||||
refreshToken
|
||||
);
|
||||
|
||||
if (browser.isElectron()) {
|
||||
// @ts-ignore
|
||||
state.electron = true;
|
||||
}
|
||||
|
||||
url = url.replace('{state}', encodeURIComponent(JSON.stringify(state)));
|
||||
}
|
||||
|
||||
url = url.replace('{room}', roomName);
|
||||
|
||||
if (url.indexOf('{code_challenge}')) {
|
||||
let codeVerifier = '';
|
||||
|
||||
// random string
|
||||
for (let i = 0; i < 64; i++) {
|
||||
codeVerifier += POSSIBLE_CHARS.charAt(Math.floor(_cryptoRandom() * POSSIBLE_CHARS.length));
|
||||
}
|
||||
|
||||
try {
|
||||
window.sessionStorage.setItem('code_verifier', codeVerifier);
|
||||
} 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 code verifier in session storage', e);
|
||||
}
|
||||
}
|
||||
|
||||
return window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
|
||||
.then(digest => {
|
||||
// prepare code challenge - base64 encoding without padding as described in:
|
||||
// https://datatracker.ietf.org/doc/html/rfc7636#appendix-A
|
||||
const codeChallenge = base64js.fromByteArray(new Uint8Array(digest))
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
return url ? url.replace('{code_challenge}', codeChallenge) : undefined;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(url);
|
||||
};
|
||||
3
react/features/authentication/logger.ts
Normal file
3
react/features/authentication/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('app:authentication');
|
||||
320
react/features/authentication/middleware.any.ts
Normal file
320
react/features/authentication/middleware.any.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
import { IStore } from '../app/types';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT
|
||||
} from '../base/conference/actionTypes';
|
||||
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 {
|
||||
closeLoginPopup,
|
||||
disableModeratorLogin,
|
||||
enableModeratorLogin,
|
||||
hideLoginDialog,
|
||||
openLoginDialog,
|
||||
openTokenAuthUrl,
|
||||
openWaitForOwnerDialog,
|
||||
redirectToDefaultLocation,
|
||||
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
|
||||
* moderator login availability 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 (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR && lobbyWaitingForHost) {
|
||||
if (recoverable) {
|
||||
store.dispatch(enableModeratorLogin());
|
||||
} else {
|
||||
store.dispatch(disableModeratorLogin());
|
||||
}
|
||||
} else if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) {
|
||||
if (recoverable) {
|
||||
store.dispatch(waitForOwner());
|
||||
} else {
|
||||
store.dispatch(stopWaitForOwner());
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED: {
|
||||
const { dispatch } = store;
|
||||
|
||||
if (_isWaitingForModerator(store)) {
|
||||
dispatch(disableModeratorLogin());
|
||||
}
|
||||
if (_isWaitingForOwner(store)) {
|
||||
dispatch(stopWaitForOwner());
|
||||
}
|
||||
dispatch(hideLoginDialog());
|
||||
|
||||
// We may have been admitted to the conference while an inline token
|
||||
// auth popup was still open (e.g. an authenticated participant joined
|
||||
// and the backend let us in automatically). Dismiss the popup so it
|
||||
// doesn't linger on top of the conference.
|
||||
closeLoginPopup();
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_LEFT:
|
||||
store.dispatch(disableModeratorLogin());
|
||||
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 STOP_WAIT_FOR_OWNER:
|
||||
_clearExistingWaitForOwnerTimeout(store);
|
||||
store.dispatch(hideDialog('WaitForOwnerDialog', 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cyclic "wait for moderator" task is currently scheduled.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function _isWaitingForModerator({ getState }: IStore) {
|
||||
return getState()['features/authentication'].showModeratorLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: lowBandwidthModeEnabled } = state['features/base/low-bandwidth-mode'];
|
||||
const audioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
|
||||
const videoMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.VIDEO);
|
||||
const refreshToken = state['features/base/jwt'].refreshToken;
|
||||
|
||||
if (!room) {
|
||||
logger.warn('Cannot handle login, room is undefined!');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTokenAuthEnabled(state)) {
|
||||
dispatch(openLoginDialog());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
getTokenAuthUrl(
|
||||
config,
|
||||
locationURL,
|
||||
{
|
||||
audioMuted,
|
||||
lowBandwidthModeEnabled,
|
||||
skipPrejoin: true,
|
||||
videoMuted
|
||||
},
|
||||
room,
|
||||
tenant,
|
||||
refreshToken
|
||||
)
|
||||
.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());
|
||||
}
|
||||
84
react/features/authentication/middleware.native.ts
Normal file
84
react/features/authentication/middleware.native.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_WILL_CONNECT } from '../base/connection/actionTypes';
|
||||
import { isDialogOpen } from '../base/dialog/functions';
|
||||
import { SET_JWT } from '../base/jwt/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { APP_STATE_CHANGED } from '../mobile/background/actionTypes';
|
||||
|
||||
import { hideLoginRetryDialog, openLoginRetryDialog, setTokenAuthPending } from './actions.native';
|
||||
import LoginRetryDialog from './components/native/LoginRetryDialog';
|
||||
import { isTokenAuthEnabled } from './functions.native';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
/**
|
||||
* Middleware that detects the app returning to the foreground while an
|
||||
* external token authentication is still pending and warns the user that they
|
||||
* are not connected to the meeting.
|
||||
*
|
||||
* Opening {@code tokenAuthUrl} in the system browser is fire-and-forget: if
|
||||
* the user abandons the login page and switches back to the app, no deep link
|
||||
* is ever delivered and the app keeps showing the conference screen with no
|
||||
* connection and no indication of failure. The pending state is tracked with
|
||||
* an explicit flag (set when the browser is opened, cleared when a new
|
||||
* connection attempt starts) because the connection-level error state cannot
|
||||
* be relied upon: on WebSocket deployments the not-authorized failure is
|
||||
* immediately followed by a connection-dropped failure which resets it.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_STATE_CHANGED: {
|
||||
if (action.appState !== 'active') {
|
||||
break;
|
||||
}
|
||||
|
||||
const state = store.getState();
|
||||
const { tokenAuthPending } = state['features/authentication'];
|
||||
const { conference, room } = state['features/base/conference'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
|
||||
// The app is back in the foreground on the conference screen with the
|
||||
// external login still pending: no conference was joined and no JWT
|
||||
// has been delivered via a deep link, so the login was abandoned and
|
||||
// the app is not connected.
|
||||
if (tokenAuthPending
|
||||
&& room
|
||||
&& !conference
|
||||
&& !jwt
|
||||
&& isTokenAuthEnabled(state)
|
||||
&& !isDialogOpen(store, LoginRetryDialog)) {
|
||||
store.dispatch(openLoginRetryDialog());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_JWT:
|
||||
// SET_JWT with no token is the periodic clearing that happens on
|
||||
// every navigation; only a delivered token completes the login.
|
||||
if (!action.jwt) {
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
case CONNECTION_ESTABLISHED:
|
||||
case CONNECTION_WILL_CONNECT: {
|
||||
// The external login is no longer pending once the deep link delivers
|
||||
// a JWT (the room may first land on the prejoin screen, where no
|
||||
// connection attempt is started yet) or a new connection attempt
|
||||
// starts.
|
||||
const { dispatch, getState } = store;
|
||||
|
||||
if (getState()['features/authentication'].tokenAuthPending) {
|
||||
dispatch(setTokenAuthPending(false));
|
||||
}
|
||||
if (isDialogOpen(store, LoginRetryDialog)) {
|
||||
dispatch(hideLoginRetryDialog());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
1
react/features/authentication/middleware.web.ts
Normal file
1
react/features/authentication/middleware.web.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './middleware.any';
|
||||
103
react/features/authentication/reducer.ts
Normal file
103
react/features/authentication/reducer.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import { assign } from '../base/redux/functions';
|
||||
|
||||
import {
|
||||
CANCEL_LOGIN,
|
||||
DISABLE_MODERATOR_LOGIN,
|
||||
ENABLE_MODERATOR_LOGIN,
|
||||
SET_TOKEN_AUTH_PENDING,
|
||||
STOP_WAIT_FOR_OWNER,
|
||||
UPGRADE_ROLE_FINISHED,
|
||||
UPGRADE_ROLE_STARTED,
|
||||
WAIT_FOR_OWNER
|
||||
} from './actionTypes';
|
||||
|
||||
export interface IAuthenticationState {
|
||||
error?: Object | undefined;
|
||||
progress?: number | undefined;
|
||||
showModeratorLogin?: boolean;
|
||||
thenableWithCancel?: {
|
||||
cancel: Function;
|
||||
};
|
||||
tokenAuthPending?: boolean;
|
||||
waitForOwnerTimeoutID?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for actions which change the state of the authentication feature.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the authentication feature.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register<IAuthenticationState>('features/authentication',
|
||||
(state = {}, action): IAuthenticationState => {
|
||||
switch (action.type) {
|
||||
case CANCEL_LOGIN:
|
||||
return assign(state, {
|
||||
error: undefined,
|
||||
progress: undefined,
|
||||
thenableWithCancel: undefined,
|
||||
tokenAuthPending: undefined
|
||||
});
|
||||
|
||||
case SET_TOKEN_AUTH_PENDING:
|
||||
return assign(state, {
|
||||
tokenAuthPending: action.pending || undefined
|
||||
});
|
||||
case ENABLE_MODERATOR_LOGIN:
|
||||
return assign(state, {
|
||||
showModeratorLogin: true
|
||||
});
|
||||
|
||||
case STOP_WAIT_FOR_OWNER:
|
||||
return assign(state, {
|
||||
error: undefined,
|
||||
waitForOwnerTimeoutID: undefined
|
||||
});
|
||||
|
||||
case DISABLE_MODERATOR_LOGIN:
|
||||
return assign(state, {
|
||||
error: undefined,
|
||||
showModeratorLogin: false
|
||||
});
|
||||
|
||||
case UPGRADE_ROLE_FINISHED: {
|
||||
let { thenableWithCancel } = action;
|
||||
|
||||
if (state.thenableWithCancel === thenableWithCancel) {
|
||||
const { error, progress } = action;
|
||||
|
||||
// An error interrupts the process of authenticating and upgrading
|
||||
// the role of the local participant/user i.e. the process is no
|
||||
// more. Obviously, the process seizes to exist also when it does
|
||||
// its whole job.
|
||||
if (error || progress === 1) {
|
||||
thenableWithCancel = undefined;
|
||||
}
|
||||
|
||||
return assign(state, {
|
||||
error,
|
||||
progress: progress || undefined,
|
||||
thenableWithCancel
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UPGRADE_ROLE_STARTED:
|
||||
return assign(state, {
|
||||
error: undefined,
|
||||
progress: undefined,
|
||||
thenableWithCancel: action.thenableWithCancel
|
||||
});
|
||||
|
||||
case WAIT_FOR_OWNER:
|
||||
return assign(state, {
|
||||
waitForOwnerTimeoutID: action.waitForOwnerTimeoutID
|
||||
});
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
Reference in New Issue
Block a user