init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
/**
* 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 we have authenticated successful when
* tokenAuthUrl is set.
*
* {
* type: SET_TOKEN_AUTH_URL_SUCCESS
* }
*/
export const SET_TOKEN_AUTH_URL_SUCCESS = 'SET_TOKEN_AUTH_URL_SUCCESS';
/**
* 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 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 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';

View File

@@ -0,0 +1,229 @@
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 {
LOGIN,
LOGOUT,
SET_TOKEN_AUTH_URL_SUCCESS,
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);
}
/**
* Login.
*
* @returns {{
* type: LOGIN
* }}
*/
export function login() {
return {
type: LOGIN
};
}
/**
* Logout.
*
* @returns {{
* type: LOGOUT
* }}
*/
export function logout() {
return {
type: LOGOUT
};
}
/**
* Opens {@link WaitForOnwerDialog}.
*
* @protected
* @returns {Action}
*/
export function openWaitForOwnerDialog() {
return openDialog(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);
}
/**
* Updates the config with new options.
*
* @param {boolean} value - The new value.
* @returns {Function}
*/
export function setTokenAuthUrlSuccess(value: boolean) {
return {
type: SET_TOKEN_AUTH_URL_SUCCESS,
value
};
}

View File

@@ -0,0 +1,90 @@
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 { set } from '../base/redux/functions';
import { CANCEL_LOGIN } from './actionTypes';
import { stopWaitForOwner } from './actions.any';
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));
}
};
}
/**
* Redirect to the default location (e.g. Welcome page).
*
* @returns {Function}
*/
export function redirectToDefaultLocation() {
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
}
/**
* Opens token auth URL page.
*
* @param {string} tokenAuthServiceUrl - Authentication service URL.
*
* @returns {Function}
*/
export function openTokenAuthUrl(tokenAuthServiceUrl: string) {
return () => {
Linking.openURL(tokenAuthServiceUrl);
};
}

View File

@@ -0,0 +1,78 @@
import { maybeRedirectToWelcomePage } from '../app/actions.web';
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { browser } from '../base/lib-jitsi-meet';
import { CANCEL_LOGIN } from './actionTypes';
import LoginQuestionDialog from './components/web/LoginQuestionDialog';
export * from './actions.any';
/**
* 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());
}
/**
* 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;
}
};
// Show warning for leaving conference only when in a conference.
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
dispatch(openDialog(LoginQuestionDialog, {
handler: () => {
// Give time for the dialog to close.
setTimeout(() => redirect(), 500);
}
}));
} else {
redirect();
}
};
}

View File

@@ -0,0 +1,2 @@
export { default as LoginDialog } from './native/LoginDialog';
export { default as WaitForOwnerDialog } from './native/WaitForOwnerDialog';

View File

@@ -0,0 +1,2 @@
export { default as LoginDialog } from './web/LoginDialog';
export { default as WaitForOwnerDialog } from './web/WaitForOwnerDialog';

View 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));

View File

@@ -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 = 'dialog.WaitForHostMsg'
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));

View 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));

View File

@@ -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;

View File

@@ -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('dialog.WaitForHostMsg') }
</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));

View File

@@ -0,0 +1,86 @@
import { IConfig } from '../base/config/configType';
import { parseURLParams } from '../base/util/parseURLParams';
import { getBackendSafeRoomName } from '../base/util/uri';
/**
* Checks if the token for authentication is available.
*
* @param {Object} config - Configuration state object from store.
* @returns {boolean}
*/
export const isTokenAuthEnabled = (config: IConfig): boolean =>
typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length > 0;
/**
* 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
* audioOnlyEnabled: boolean | undefined,
* skipPrejoin: boolean | undefined,
* videoMuted: boolean | undefined
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name if any.
*
* @returns {Object} The state object.
*/
export const _getTokenAuthState = (
locationURL: URL,
options: {
audioMuted: boolean | undefined;
audioOnlyEnabled: boolean | undefined;
skipPrejoin: boolean | undefined;
videoMuted: boolean | undefined;
},
roomName: string | undefined,
tenant: string | undefined): object => {
const state = {
room: roomName,
roomSafe: getBackendSafeRoomName(roomName),
tenant
};
const {
audioMuted = false,
audioOnlyEnabled = false,
skipPrejoin = false,
videoMuted = false
} = options;
if (audioMuted) {
// @ts-ignore
state['config.startWithAudioMuted'] = true;
}
if (audioOnlyEnabled) {
// @ts-ignore
state['config.startAudioOnly'] = 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;
};

View File

@@ -0,0 +1,78 @@
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
* audioOnlyEnabled: boolean | undefined,
* skipPrejoin: boolean | undefined,
* videoMuted: boolean | undefined
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name 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;
audioOnlyEnabled: boolean | undefined;
skipPrejoin: boolean | undefined;
videoMuted: boolean | undefined;
},
roomName: string | undefined,
// eslint-disable-next-line max-params
tenant: string | undefined): Promise<string | undefined> => {
const {
audioMuted = false,
audioOnlyEnabled = 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,
audioOnlyEnabled,
skipPrejoin,
videoMuted
},
roomName,
tenant
);
// 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));
};

View File

@@ -0,0 +1,122 @@
import base64js from 'base64-js';
import { IConfig } from '../base/config/configType';
import { browser } from '../base/lib-jitsi-meet';
import { _getTokenAuthState } from './functions.any';
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
* audioOnlyEnabled: boolean | undefined,
* skipPrejoin: boolean | undefined,
* videoMuted: boolean | undefined
* }.
* @param {string?} roomName - The room name.
* @param {string?} tenant - The tenant name 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;
audioOnlyEnabled: boolean | undefined;
skipPrejoin: boolean | undefined;
videoMuted: boolean | undefined;
},
roomName: string | undefined,
// eslint-disable-next-line max-params
tenant: string | undefined): Promise<string | undefined> => {
const {
audioMuted = false,
audioOnlyEnabled = 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,
audioOnlyEnabled,
skipPrejoin,
videoMuted
},
roomName,
tenant
);
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));
}
window.sessionStorage.setItem('code_verifier', codeVerifier);
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);
};

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('features/authentication');

View File

@@ -0,0 +1,319 @@
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());
}

View File

@@ -0,0 +1,96 @@
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { assign } from '../base/redux/functions';
import {
CANCEL_LOGIN,
SET_TOKEN_AUTH_URL_SUCCESS,
STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FINISHED,
UPGRADE_ROLE_STARTED,
WAIT_FOR_OWNER
} from './actionTypes';
export interface IAuthenticationState {
error?: Object | undefined;
progress?: number | undefined;
thenableWithCancel?: {
cancel: Function;
};
tokenAuthUrlSuccessful?: boolean;
waitForOwnerTimeoutID?: number;
}
/**
* Sets up the persistence of the feature {@code authentication}.
*/
PersistenceRegistry.register('features/authentication', {
tokenAuthUrlSuccessful: true
});
/**
* 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
});
case SET_TOKEN_AUTH_URL_SUCCESS:
return assign(state, {
tokenAuthUrlSuccessful: action.value
});
case STOP_WAIT_FOR_OWNER:
return assign(state, {
error: undefined,
waitForOwnerTimeoutID: undefined
});
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;
});