diff --git a/lang/main.json b/lang/main.json index bdb6b4344..de572067e 100644 --- a/lang/main.json +++ b/lang/main.json @@ -264,7 +264,7 @@ "removeSharedVideoMsg": "Are you sure you would like to remove your shared video?", "alreadySharedVideoMsg": "Another member is already sharing video. This conference allows only one shared video at a time.", "WaitingForHost": "Waiting for the host ...", - "WaitForHostMsg": "The conference __room__ has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.", + "WaitForHostMsg": "The conference '__room__' has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.", "IamHost": "I am the host", "Cancel": "Cancel", "Submit": "Submit", diff --git a/modules/UI/authentication/AuthHandler.js b/modules/UI/authentication/AuthHandler.js index 74981f717..fd9c1b664 100644 --- a/modules/UI/authentication/AuthHandler.js +++ b/modules/UI/authentication/AuthHandler.js @@ -140,44 +140,35 @@ function initJWTTokenListener(room) { * @param {string} [lockPassword] password to use if the conference is locked */ function doXmppAuth (room, lockPassword) { - let loginDialog = LoginDialog.showAuthDialog(function (id, password) { - // auth "on the fly": - // 1. open new connection with proper id and password - // 2. connect to the room - // (this will store sessionId in the localStorage) - // 3. close new connection - // 4. reallocate focus in current room - openConnection({id, password, roomName: room.getName()}).then( - function (connection) { - // open room - let newRoom = connection.initJitsiConference( - room.getName(), APP.conference._getConferenceOptions() - ); - - loginDialog.displayConnectionStatus('connection.FETCH_SESSION_ID'); - - newRoom.room.moderator.authenticate().then(function () { - connection.disconnect(); + const loginDialog = LoginDialog.showAuthDialog(function (id, password) { + const authConnection = room.createAuthenticationConnection(); + authConnection.authenticateAndUpgradeRole({ + id, + password, + roomPassword: lockPassword, + onLoginSuccessful: () => { /* Called when XMPP login succeeds */ + loginDialog.displayConnectionStatus( + 'connection.FETCH_SESSION_ID'); + } + }) + .then(() => { loginDialog.displayConnectionStatus( 'connection.GOT_SESSION_ID'); - - // authenticate conference on the fly - room.join(lockPassword); - loginDialog.close(); - }).catch(function (error, code) { - connection.disconnect(); - - logger.error('Auth on the fly failed', error); - - loginDialog.displayError( - 'connection.GET_SESSION_ID_ERROR', {code: code}); + }) + .catch(error => { + logger.error('authenticateAndUpgradeRole failed', error); + if (error.authenticationError) { + loginDialog.displayError( + 'connection.GET_SESSION_ID_ERROR', { + msg: error.authenticationError + }); + } else { + loginDialog.displayError(error.connectionError); + } }); - }, function (err) { - loginDialog.displayError(err); - }); - }, function () { // user canceled + }, function () { loginDialog.close(); }); } diff --git a/modules/UI/authentication/LoginDialog.js b/modules/UI/authentication/LoginDialog.js index 08bf9f71d..1e1426214 100644 --- a/modules/UI/authentication/LoginDialog.js +++ b/modules/UI/authentication/LoginDialog.js @@ -1,4 +1,6 @@ /* global $, APP, config, JitsiMeetJS */ +import { toJid } from '../../../react/features/base/connection'; + const ConnectionErrors = JitsiMeetJS.errors.connection; /** @@ -19,26 +21,6 @@ function getPasswordInputHtml() { data-i18n="[placeholder]dialog.userPassword">`; } -/** - * Convert provided id to jid if it's not jid yet. - * @param {string} id user id or jid - * @returns {string} jid - */ -function toJid(id) { - if (id.indexOf("@") >= 0) { - return id; - } - - let jid = id.concat('@'); - if (config.hosts.authdomain) { - jid += config.hosts.authdomain; - } else { - jid += config.hosts.domain; - } - - return jid; -} - /** * Generate cancel button config for the dialog. * @returns {Object} @@ -90,7 +72,7 @@ function LoginDialog(successCallback, cancelCallback) { let password = f.password; if (jid && password) { connDialog.goToState('connecting'); - successCallback(toJid(jid), password); + successCallback(toJid(jid, config.hosts), password); } } else { // User cancelled @@ -223,7 +205,7 @@ export default { */ showAuthRequiredDialog: function (roomName, onAuthNow) { var msg = APP.translation.generateTranslationHTML( - "[html]dialog.WaitForHostMsg", {room: roomName} + "dialog.WaitForHostMsg", {room: roomName} ); var buttonTxt = APP.translation.generateTranslationHTML( diff --git a/react/features/app/components/App.native.js b/react/features/app/components/App.native.js index fdbc2d32e..6a5ac264f 100644 --- a/react/features/app/components/App.native.js +++ b/react/features/app/components/App.native.js @@ -15,6 +15,7 @@ import '../../mobile/proximity'; import '../../mobile/wake-lock'; import { AbstractApp } from './AbstractApp'; +import '../../authentication'; /** * Root application component. diff --git a/react/features/authentication/actionTypes.js b/react/features/authentication/actionTypes.js new file mode 100644 index 000000000..db93e681e --- /dev/null +++ b/react/features/authentication/actionTypes.js @@ -0,0 +1,75 @@ +/** + * The type of (redux) action which signals that {@link LoginDialog} has been + * canceled. + * + * { + * type: CANCEL_LOGIN + * } + */ +export const CANCEL_LOGIN = Symbol('CANCEL_LOGIN'); + +/** + * The type of (redux) action which signals that the {@link WaitForOwnerDialog} + * has been canceled. + * + * { + * type: CANCEL_WAIT_FOR_OWNER + * } + */ +export const CANCEL_WAIT_FOR_OWNER = Symbol('CANCEL_WAIT_FOR_OWNER'); + +/** + * 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 = Symbol('STOP_WAIT_FOR_OWNER'); + +/** + * The type of (redux) action which signals that the process of authenticating + * and upgrading the current conference user's role has been started. + * + * { + * type: UPGRADE_ROLE_STARTED, + * authConnection: JitsiAuthConnection + * } + */ +export const UPGRADE_ROLE_STARTED = Symbol('UPGRADE_ROLE_STARTED'); + +/** + * The type of (redux) action which informs that the authentication and role + * upgrade process has been completed successfully. + * + * { + * type: UPGRADE_ROLE_SUCCESS + * } + */ +export const UPGRADE_ROLE_SUCCESS = Symbol('UPGRADE_ROLE_SUCCESS'); + +/** + * The type of (redux) action which informs that the authentication and role + * upgrade process has failed with an error. Check the docs of + * {@link JitsiAuthConnection} for more details about the error structure. + * + * { + * type: UPGRADE_ROLE_SUCCESS, + * error: Object + * } + */ +export const UPGRADE_ROLE_FAILED = Symbol('UPGRADE_ROLE_FAILED'); + +/** + * 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 = Symbol('WAIT_FOR_OWNER'); diff --git a/react/features/authentication/actions.js b/react/features/authentication/actions.js new file mode 100644 index 000000000..1f6f8bac1 --- /dev/null +++ b/react/features/authentication/actions.js @@ -0,0 +1,210 @@ +import { openDialog } from '../base/dialog/actions'; +import { checkIfCanJoin } from '../base/conference/actions'; +import { + CANCEL_LOGIN, + CANCEL_WAIT_FOR_OWNER, + STOP_WAIT_FOR_OWNER, + UPGRADE_ROLE_FAILED, + UPGRADE_ROLE_STARTED, + UPGRADE_ROLE_SUCCESS, + WAIT_FOR_OWNER +} from './actionTypes'; +import { LoginDialog, WaitForOwnerDialog } from './components'; + +/** + * Instantiates new {@link JitsiAuthConnection} and uses it to authenticate and + * upgrade role of the current conference user to moderator which will allow to + * create and join new conference on XMPP password + guest access configuration. + * See {@link LoginDialog} description for more info. + * + * @param {string} id - XMPP user's id eg. user@domain.com. + * @param {string} userPassword - The user's password. + * @param {JitsiConference} conference - The conference for which user's role + * will be upgraded. + * @returns {function({dispatch: Function, getState: Function})} + */ +export function authenticateAndUpgradeRole(id, userPassword, conference) { + return (dispatch, getState) => { + const authConnection = conference.createAuthenticationConnection(); + + dispatch(_upgradeRoleStarted(authConnection)); + + const { password: roomPassword } + = getState()['features/base/conference']; + + authConnection.authenticateAndUpgradeRole({ + id, + password: userPassword, + roomPassword + }) + .then(() => { + dispatch(_upgradeRoleSuccess()); + }) + .catch(error => { + // Lack of error means the operation was canceled, so no need to log + // that on error level. + if (error.error) { + console.error('upgradeRoleFailed', error); + } + dispatch(_upgradeRoleFailed(error)); + }); + }; +} + +/** + * Cancels {@ink LoginDialog}. + * + * @returns {{ + * type: CANCEL_LOGIN + * }} + */ +export function cancelLogin() { + return { + type: CANCEL_LOGIN + }; +} + +/** + * Cancels {@link WaitForOwnerDialog}. Will navigate back to the welcome page. + * + * @returns {{ + * type: CANCEL_WAIT_FOR_OWNER + * }} + */ +export function cancelWaitForOwner() { + return { + type: CANCEL_WAIT_FOR_OWNER + }; +} + +/** + * Stops waiting for conference owner and clears any pending timeout. + * + * @returns {{ + * type: STOP_WAIT_FOR_OWNER + * }} + */ +export function clearWaitForOwnerTimeout() { + return { + type: STOP_WAIT_FOR_OWNER + }; +} + +/** + * Sets a delayed "wait for owner" handler function. + * + * @param {Function} handler - The "wait for owner" handler function. + * @param {number} waitMs - The delay in milliseconds. + * + * @private + * @returns {{ + * type: WAIT_FOR_OWNER, + * handler: Function, + * timeoutMs: number + * }} + */ +function _setWaitForOwnerTimeout(handler, waitMs) { + return { + type: WAIT_FOR_OWNER, + handler, + timeoutMs: waitMs + }; +} + +/** + * Displays {@link LoginDialog} which will ask to enter username and password + * for the current conference. + * + * @protected + * @returns {{ + * type: OPEN_DIALOG, + * component: LoginDialog, + * props: React.PropTypes + * }} + */ +export function _showLoginDialog() { + return openDialog(LoginDialog, { }); +} + +/** + * Displays {@link WaitForOnwerDialog}. + * + * @protected + * @returns {{ + * type: OPEN_DIALOG, + * component: WaitForOwnerDialog, + * props: React.PropTypes + * }} + */ +export function _showWaitForOwnerDialog() { + return openDialog(WaitForOwnerDialog, { }); +} + +/** + * Emits an error which occurred during {@link authenticateAndUpgradeRole}. + * + * @param {Object} error - Check the docs of {@link JitsiAuthConnection} in + * lib-jitsi-meet for more details about the error's structure. + * + * @private + * @returns {{ + * type: UPGRADE_ROLE_FAILED, + * error: Object + * }} + */ +function _upgradeRoleFailed(error) { + return { + type: UPGRADE_ROLE_FAILED, + error + }; +} + +/** + * Signals that the role upgrade process has been started using given + * {@link JitsiAuthConnection} instance. + * + * @param {JitsiAuthConnection} authenticationConnection - The authentication + * connection instance that can be used to cancel the process. + * + * @private + * @returns {{ + * type: UPGRADE_ROLE_STARTED, + * authConnection: JitsiAuthConnection + * }} + */ +function _upgradeRoleStarted(authenticationConnection) { + return { + type: UPGRADE_ROLE_STARTED, + authConnection: authenticationConnection + }; +} + +/** + * Signals that the role upgrade process has been completed successfully. + * + * @private + * @returns {{ + * type: UPGRADE_ROLE_SUCCESS + * }} + */ +function _upgradeRoleSuccess() { + return { + type: UPGRADE_ROLE_SUCCESS + }; +} + +/** + * 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({ dispatch: Function})} + */ +export function waitForOwner() { + return dispatch => { + dispatch( + _setWaitForOwnerTimeout( + () => dispatch(checkIfCanJoin()), + 5000)); + }; +} diff --git a/react/features/authentication/components/LoginDialog.native.js b/react/features/authentication/components/LoginDialog.native.js new file mode 100644 index 000000000..9a4f0652d --- /dev/null +++ b/react/features/authentication/components/LoginDialog.native.js @@ -0,0 +1,280 @@ +import React, { Component } from 'react'; +import { connect as reduxConnect } from 'react-redux'; +import { + Button, + Modal, + Text, + TextInput, + View +} from 'react-native'; +import { + authenticateAndUpgradeRole, + cancelLogin +} from '../actions'; +import { + connect, + toJid +} from '../../base/connection'; +import { translate } from '../../base/i18n'; +import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet'; +import styles from './styles'; + +/** + * 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 + * {@link JitsiAuthConnection} will be created from which 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 JitsiAuthConnection} from lib-jitsi-meet. + * + * See https://github.com/jitsi/jicofo#secure-domain for configuration + * parameters description. + */ +class LoginDialog extends Component { + /** + * LoginDialog component's property types. + * + * @static + */ + static propTypes = { + /** + * {@link JitsiConference} that needs authentication - will hold a valid + * value in XMPP login + guest access mode. + */ + conference: React.PropTypes.object, + + /** + * + */ + configHosts: React.PropTypes.object, + + /** + * Indicates if the dialog should display "connecting" status message. + */ + connecting: React.PropTypes.bool, + + /** + * Redux store dispatch method. + */ + dispatch: React.PropTypes.func, + + /** + * The error which occurred during login/authentication. + */ + error: React.PropTypes.string, + + /** + * Any extra details about the error provided by lib-jitsi-meet. + */ + errorDetails: React.PropTypes.string, + + /** + * Invoked to obtain translated strings. + */ + t: React.PropTypes.func + }; + + /** + * Initializes a new LoginDialog instance. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props) { + super(props); + + // Bind event handlers so they are only bound once for every instance. + this._onCancel = this._onCancel.bind(this); + this._onLogin = this._onLogin.bind(this); + this._onUsernameChange = this._onUsernameChange.bind(this); + this._onPasswordChange = this._onPasswordChange.bind(this); + + this.state = { + username: '', + password: '' + }; + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + * @returns {ReactElement} + */ + render() { + const { + error, + errorDetails, + connecting, + t + } = this.props; + + let messageKey = ''; + const messageOptions = { }; + + if (error === JitsiConnectionErrors.PASSWORD_REQUIRED) { + messageKey = 'dialog.incorrectPassword'; + } else if (error) { + messageKey = 'dialog.connectErrorWithMsg'; + + messageOptions.msg = `${error} ${errorDetails}`; + } + + return ( + + + Username: + + Password: + + + {error ? t(messageKey, messageOptions) : ''} + {connecting && !error + ? t('connection.CONNECTING') : ''} + +