diff --git a/conference.js b/conference.js index 7b204700d..3dc30d260 100644 --- a/conference.js +++ b/conference.js @@ -309,11 +309,6 @@ class ConferenceConnector { room.join(); }, 5000); - const { password } - = APP.store.getState()['features/base/conference']; - - AuthHandler.requireAuth(room, password); - break; } @@ -378,7 +373,6 @@ class ConferenceConnector { if (this.reconnectTimeout !== null) { clearTimeout(this.reconnectTimeout); } - AuthHandler.closeAuth(); } /** @@ -2242,7 +2236,7 @@ export default { }); APP.UI.addListener(UIEvents.AUTH_CLICKED, () => { - AuthHandler.authenticate(room); + AuthHandler.authenticateExternal(room); }); APP.UI.addListener( diff --git a/connection.js b/connection.js index d884acdfd..12b268df5 100644 --- a/connection.js +++ b/connection.js @@ -3,18 +3,21 @@ import { jitsiLocalStorage } from '@jitsi/js-utils'; import Logger from 'jitsi-meet-logger'; -import AuthHandler from './modules/UI/authentication/AuthHandler'; +import { redirectToTokenAuthService } from './modules/UI/authentication/AuthHandler'; +import { hideLoginDialog } from './react/features/authentication/actions.web'; +import { LoginDialog } from './react/features/authentication/components'; +import { isTokenAuthEnabled } from './react/features/authentication/functions'; import { connectionEstablished, connectionFailed } from './react/features/base/connection/actions'; +import { openDialog } from './react/features/base/dialog/actions'; import { isFatalJitsiConnectionError, JitsiConnectionErrors, JitsiConnectionEvents } from './react/features/base/lib-jitsi-meet'; import { setPrejoinDisplayNameRequired } from './react/features/prejoin/actions'; - const logger = Logger.getLogger(__filename); /** @@ -80,7 +83,7 @@ function checkForAttachParametersAndConnect(id, password, connection) { * @returns {Promise} connection if * everything is ok, else error. */ -function connect(id, password, roomName) { +export function connect(id, password, roomName) { const connectionConfig = Object.assign({}, config); const { jwt } = APP.store.getState()['features/base/jwt']; @@ -214,10 +217,39 @@ export function openConnection({ id, password, retry, roomName }) { const { jwt } = APP.store.getState()['features/base/jwt']; if (err === JitsiConnectionErrors.PASSWORD_REQUIRED && !jwt) { - return AuthHandler.requestAuth(roomName, connect); + return requestAuth(roomName); } } throw err; }); } + +/** + * Show Authentication Dialog and try to connect with new credentials. + * If failed to connect because of PASSWORD_REQUIRED error + * then ask for password again. + * @param {string} [roomName] name of the conference room + * + * @returns {Promise} + */ +function requestAuth(roomName) { + const config = APP.store.getState()['features/base/config']; + + if (isTokenAuthEnabled(config)) { + // This Promise never resolves as user gets redirected to another URL + return new Promise(() => redirectToTokenAuthService(roomName)); + } + + return new Promise(resolve => { + const onSuccess = connection => { + APP.store.dispatch(hideLoginDialog()); + resolve(connection); + }; + + APP.store.dispatch( + openDialog(LoginDialog, { onSuccess, + roomName }) + ); + }); +} diff --git a/lang/main.json b/lang/main.json index ae6ddc8a0..366a0f3c8 100644 --- a/lang/main.json +++ b/lang/main.json @@ -175,6 +175,7 @@ "alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.", "alreadySharedVideoTitle": "Only one shared video is allowed at a time", "applicationWindow": "Application window", + "authenticationRequired": "Authentication required", "Back": "Back", "cameraConstraintFailedError": "Your camera does not satisfy some of the required constraints.", "cameraNotFoundError": "Camera was not found.", @@ -227,6 +228,7 @@ "lockRoom": "Add meeting $t(lockRoomPasswordUppercase)", "lockTitle": "Lock failed", "logoutQuestion": "Are you sure you want to logout and stop the conference?", + "login": "Login", "logoutTitle": "Logout", "maxUsersLimitReached": "The limit for maximum number of participants has been reached. The conference is full. Please contact the meeting owner or try again later!", "maxUsersLimitReachedTitle": "Maximum participants limit reached", @@ -312,12 +314,13 @@ "tokenAuthFailedTitle": "Authentication failed", "transcribing": "Transcribing", "unlockRoom": "Remove meeting $t(lockRoomPassword)", - "user": "user", - "userPassword": "user password", + "user": "User", + "userIdentifier": "User identifier", + "userPassword": "User password", "videoLink": "Video link", "WaitForHostMsg": "The conference {{room}} has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.", "WaitForHostMsgWOk": "The conference {{room}} has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.", - "WaitingForHost": "Waiting for the host ...", + "WaitingForHostTitle": "Waiting for the host ...", "Yes": "Yes", "yourEntireScreen": "Your entire screen" }, diff --git a/modules/UI/UI.js b/modules/UI/UI.js index 4125028ad..1f17b9e5d 100644 --- a/modules/UI/UI.js +++ b/modules/UI/UI.js @@ -146,7 +146,6 @@ UI.start = function() { } APP.store.dispatch(setToolboxEnabled(false)); - UI.messageHandler.enablePopups(false); } }; diff --git a/modules/UI/authentication/AuthHandler.js b/modules/UI/authentication/AuthHandler.js index 82602b6d1..a88254ac8 100644 --- a/modules/UI/authentication/AuthHandler.js +++ b/modules/UI/authentication/AuthHandler.js @@ -1,25 +1,22 @@ -/* global APP, config, JitsiMeetJS, Promise */ +// @flow import Logger from 'jitsi-meet-logger'; import { openConnection } from '../../../connection'; -import { setJWT } from '../../../react/features/base/jwt'; import { - JitsiConnectionErrors -} from '../../../react/features/base/lib-jitsi-meet'; + isTokenAuthEnabled, + getTokenAuthUrl +} from '../../../react/features/authentication/functions'; +import { setJWT } from '../../../react/features/base/jwt'; import UIUtil from '../util/UIUtil'; import LoginDialog from './LoginDialog'; +let externalAuthWindow; +declare var APP: Object; + const logger = Logger.getLogger(__filename); -let externalAuthWindow; -let authRequiredDialog; - -const isTokenAuthEnabled - = typeof config.tokenAuthUrl === 'string' && config.tokenAuthUrl.length; -const getTokenAuthUrl - = JitsiMeetJS.util.AuthUtil.getTokenAuthUrl.bind(null, config.tokenAuthUrl); /** * Authenticate using external service or just focus @@ -29,6 +26,8 @@ const getTokenAuthUrl * @param {string} [lockPassword] password to use if the conference is locked */ function doExternalAuth(room, lockPassword) { + const config = APP.store.getState()['features/base/config']; + if (externalAuthWindow) { externalAuthWindow.focus(); @@ -37,8 +36,8 @@ function doExternalAuth(room, lockPassword) { if (room.isJoined()) { let getUrl; - if (isTokenAuthEnabled) { - getUrl = Promise.resolve(getTokenAuthUrl(room.getName(), true)); + if (isTokenAuthEnabled(config)) { + getUrl = Promise.resolve(getTokenAuthUrl(config)(room.getName(), true)); initJWTTokenListener(room); } else { getUrl = room.getExternalAuthUrl(true); @@ -48,13 +47,13 @@ function doExternalAuth(room, lockPassword) { url, () => { externalAuthWindow = null; - if (!isTokenAuthEnabled) { + if (!isTokenAuthEnabled(config)) { room.join(lockPassword); } } ); }); - } else if (isTokenAuthEnabled) { + } else if (isTokenAuthEnabled(config)) { redirectToTokenAuthService(room.getName()); } else { room.getExternalAuthUrl().then(UIUtil.redirect); @@ -67,10 +66,12 @@ function doExternalAuth(room, lockPassword) { * back with "?jwt={the JWT token}" query parameter added. * @param {string} [roomName] the name of the conference room. */ -function redirectToTokenAuthService(roomName) { +export function redirectToTokenAuthService(roomName: string) { + const config = APP.store.getState()['features/base/config']; + // FIXME: This method will not preserve the other URL params that were // originally passed. - UIUtil.redirect(getTokenAuthUrl(roomName, false)); + UIUtil.redirect(getTokenAuthUrl(config)(roomName, false)); } /** @@ -157,58 +158,15 @@ function initJWTTokenListener(room) { } /** - * Authenticate on the server. - * @param {JitsiConference} room - * @param {string} [lockPassword] password to use if the conference is locked - */ -function doXmppAuth(room, lockPassword) { - const loginDialog = LoginDialog.showAuthDialog( - /* successCallback */ (id, password) => { - room.authenticateAndUpgradeRole({ - id, - password, - roomPassword: lockPassword, - - /** Called when the XMPP login succeeds. */ - onLoginSuccessful() { - loginDialog.displayConnectionStatus( - 'connection.FETCH_SESSION_ID'); - } - }) - .then( - /* onFulfilled */ () => { - loginDialog.displayConnectionStatus( - 'connection.GOT_SESSION_ID'); - loginDialog.close(); - }, - /* onRejected */ error => { - logger.error('authenticateAndUpgradeRole failed', error); - - const { authenticationError, connectionError } = error; - - if (authenticationError) { - loginDialog.displayError( - 'connection.GET_SESSION_ID_ERROR', - { msg: authenticationError }); - } else if (connectionError) { - loginDialog.displayError(connectionError); - } - }); - }, - /* cancelCallback */ () => loginDialog.close()); -} - -/** - * Authenticate for the conference. * Uses external service for auth if conference supports that. * @param {JitsiConference} room * @param {string} [lockPassword] password to use if the conference is locked */ -function authenticate(room, lockPassword) { - if (isTokenAuthEnabled || room.isExternalAuthEnabled()) { +function authenticateExternal(room: Object, lockPassword: string) { + const config = APP.store.getState()['features/base/config']; + + if (isTokenAuthEnabled(config) || room.isExternalAuthEnabled()) { doExternalAuth(room, lockPassword); - } else { - doXmppAuth(room, lockPassword); } } @@ -219,7 +177,7 @@ function authenticate(room, lockPassword) { * @param {string} [lockPassword] password to use if the conference is locked * @returns {Promise} */ -function logout(room) { +function logout(room: Object) { return new Promise(resolve => { room.room.moderator.logout(resolve); }).then(url => { @@ -232,83 +190,7 @@ function logout(room) { }); } -/** - * Notify user that authentication is required to create the conference. - * @param {JitsiConference} room - * @param {string} [lockPassword] password to use if the conference is locked - */ -function requireAuth(room, lockPassword) { - if (authRequiredDialog) { - return; - } - - authRequiredDialog = LoginDialog.showAuthRequiredDialog( - room.getName(), authenticate.bind(null, room, lockPassword) - ); -} - -/** - * Close auth-related dialogs if there are any. - */ -function closeAuth() { - if (externalAuthWindow) { - externalAuthWindow.close(); - externalAuthWindow = null; - } - - if (authRequiredDialog) { - authRequiredDialog.close(); - authRequiredDialog = null; - } -} - -/** - * - */ -function showXmppPasswordPrompt(roomName, connect) { - return new Promise((resolve, reject) => { - const authDialog = LoginDialog.showAuthDialog( - (id, password) => { - connect(id, password, roomName).then(connection => { - authDialog.close(); - resolve(connection); - }, err => { - if (err === JitsiConnectionErrors.PASSWORD_REQUIRED) { - authDialog.displayError(err); - } else { - authDialog.close(); - reject(err); - } - }); - } - ); - }); -} - -/** - * Show Authentication Dialog and try to connect with new credentials. - * If failed to connect because of PASSWORD_REQUIRED error - * then ask for password again. - * @param {string} [roomName] name of the conference room - * @param {function(id, password, roomName)} [connect] function that returns - * a Promise which resolves with JitsiConnection or fails with one of - * JitsiConnectionErrors. - * @returns {Promise} - */ -function requestAuth(roomName, connect) { - if (isTokenAuthEnabled) { - // This Promise never resolves as user gets redirected to another URL - return new Promise(() => redirectToTokenAuthService(roomName)); - } - - return showXmppPasswordPrompt(roomName, connect); - -} - export default { - authenticate, - requireAuth, - requestAuth, - closeAuth, + authenticateExternal, logout }; diff --git a/modules/UI/authentication/LoginDialog.js b/modules/UI/authentication/LoginDialog.js index be8e1b1a3..fb1a92dfd 100644 --- a/modules/UI/authentication/LoginDialog.js +++ b/modules/UI/authentication/LoginDialog.js @@ -212,45 +212,5 @@ export default { } return dialog; - }, - - /** - * Shows a notification that authentication is required to create the - * conference, so the local participant should authenticate or wait for a - * host. - * - * @param {string} room - The name of the conference. - * @param {function} onAuthNow - The callback to invoke if the local - * participant wants to authenticate. - * @returns dialog - */ - showAuthRequiredDialog(room, onAuthNow) { - const msg = APP.translation.generateTranslationHTML( - '[html]dialog.WaitForHostMsg', - { room } - ); - const buttonTxt = APP.translation.generateTranslationHTML( - 'dialog.IamHost' - ); - const buttons = [ { - title: buttonTxt, - value: 'authNow' - } ]; - - return APP.UI.messageHandler.openDialog( - 'dialog.WaitingForHost', - msg, - true, - buttons, - (e, submitValue) => { - // Do not close the dialog yet. - e.preventDefault(); - - // Open login popup. - if (submitValue === 'authNow') { - onAuthNow(); - } - } - ); } }; diff --git a/modules/UI/util/MessageHandler.js b/modules/UI/util/MessageHandler.js index 1f446420a..63a0e6f26 100644 --- a/modules/UI/util/MessageHandler.js +++ b/modules/UI/util/MessageHandler.js @@ -12,12 +12,6 @@ import { const logger = Logger.getLogger(__filename); -/** - * Flag for enabling/disabling popups. - * @type {boolean} - */ -let popupEnabled = true; - /** * Currently displayed two button dialog. * @type {null} @@ -167,7 +161,7 @@ const messageHandler = { let { classes } = options; - if (!popupEnabled || twoButtonDialog) { + if (twoButtonDialog) { return null; } @@ -233,88 +227,6 @@ const messageHandler = { return $.prompt.getApi(); }, - /** - * Shows a message to the user with two buttons: first is given as a - * parameter and the second is Cancel. - * - * @param titleKey the key for the title of the message - * @param msgString the text of the message - * @param persistent boolean value which determines whether the message is - * persistent or not - * @param buttons object with the buttons. The keys must be the name of the - * button and value is the value that will be passed to - * submitFunction - * @param submitFunction function to be called on submit - * @param loadedFunction function to be called after the prompt is fully - * loaded - * @param closeFunction function to be called on dialog close - * @param {object} dontShowAgain - options for dont show again checkbox. - * @param {string} dontShowAgain.id the id of the checkbox. - * @param {string} dontShowAgain.textKey the key for the text displayed - * next to checkbox - * @param {boolean} dontShowAgain.checked if true the checkbox is foing to - * be checked - * @param {Array} dontShowAgain.buttonValues The button values that will - * trigger storing the checkbox value - * @param {string} dontShowAgain.localStorageKey the key for the local - * storage. if not provided dontShowAgain.id will be used. - */ - openDialog(// eslint-disable-line max-params - titleKey, - msgString, - persistent, - buttons, - submitFunction, - loadedFunction, - closeFunction, - dontShowAgain) { - if (!popupEnabled) { - return; - } - - if (dontShowTheDialog(dontShowAgain)) { - // Maybe we should pass some parameters here? I'm not sure - // and currently we don't need any parameters. - submitFunction(); - - return; - } - - const args = { - title: this._getFormattedTitleString(titleKey), - persistent, - buttons, - defaultButton: 1, - promptspeed: 0, - loaded() { - if (loadedFunction) { - // eslint-disable-next-line prefer-rest-params - loadedFunction.apply(this, arguments); - } - - // Hide the close button - if (persistent) { - $('.jqiclose', this).hide(); - } - }, - submit: dontShowAgainSubmitFunctionWrapper( - dontShowAgain, submitFunction), - close: closeFunction, - classes: this._getDialogClasses() - }; - - if (persistent) { - args.closeText = ''; - } - - const dialog = $.prompt( - msgString + generateDontShowCheckbox(dontShowAgain), args); - - APP.translation.translateElement(dialog); - - return $.prompt.getApi(); - }, - /** * Returns the formatted title string. * @@ -358,9 +270,6 @@ const messageHandler = { * @param translateOptions options passed to translation */ openDialogWithStates(statesObject, options, translateOptions) { - if (!popupEnabled) { - return; - } const { classes, size } = options; const defaultClasses = this._getDialogClasses(size); @@ -397,10 +306,6 @@ const messageHandler = { */ // eslint-disable-next-line max-params openCenteredPopup(url, w, h, onPopupClosed) { - if (!popupEnabled) { - return; - } - const l = window.screenX + (window.innerWidth / 2) - (w / 2); const t = window.screenY + (window.innerHeight / 2) - (h / 2); const popup = window.open( @@ -481,19 +386,6 @@ const messageHandler = { notify(titleKey, messageKey, messageArguments) { this.participantNotification( null, titleKey, null, messageKey, messageArguments); - }, - - enablePopups(enable) { - popupEnabled = enable; - }, - - /** - * Returns true if dialog is opened - * false otherwise - * @returns {boolean} isOpened - */ - isDialogOpened() { - return Boolean($.prompt.getCurrentStateName()); } }; diff --git a/react/features/app/middlewares.web.js b/react/features/app/middlewares.web.js index edf3c45fc..7abfdb8cf 100644 --- a/react/features/app/middlewares.web.js +++ b/react/features/app/middlewares.web.js @@ -1,5 +1,6 @@ // @flow +import '../authentication/middleware'; import '../base/devices/middleware'; import '../e2ee/middleware'; import '../external-api/middleware'; diff --git a/react/features/app/reducers.any.js b/react/features/app/reducers.any.js index e41000c22..bf1dcbfa8 100644 --- a/react/features/app/reducers.any.js +++ b/react/features/app/reducers.any.js @@ -1,6 +1,7 @@ // @flow import '../analytics/reducer'; +import '../authentication/reducer'; import '../base/app/reducer'; import '../base/audio-only/reducer'; import '../base/color-scheme/reducer'; diff --git a/react/features/app/reducers.native.js b/react/features/app/reducers.native.js index 56c73c2e9..7b8a31cf0 100644 --- a/react/features/app/reducers.native.js +++ b/react/features/app/reducers.native.js @@ -1,6 +1,5 @@ // @flow -import '../authentication/reducer'; import '../mobile/audio-mode/reducer'; import '../mobile/background/reducer'; import '../mobile/call-integration/reducer'; diff --git a/react/features/authentication/actions.js b/react/features/authentication/actions.native.js similarity index 98% rename from react/features/authentication/actions.js rename to react/features/authentication/actions.native.js index 2183fa3e4..0c6e3389c 100644 --- a/react/features/authentication/actions.js +++ b/react/features/authentication/actions.native.js @@ -3,9 +3,9 @@ import type { Dispatch } from 'redux'; import { appNavigate } from '../app/actions'; -import { checkIfCanJoin, conferenceLeft } from '../base/conference'; -import { connectionFailed } from '../base/connection'; -import { openDialog } from '../base/dialog'; +import { checkIfCanJoin, conferenceLeft } from '../base/conference/actions'; +import { connectionFailed } from '../base/connection/actions.native'; +import { openDialog } from '../base/dialog/actions'; import { set } from '../base/redux'; import { diff --git a/react/features/authentication/actions.web.js b/react/features/authentication/actions.web.js new file mode 100644 index 000000000..3ecef5de5 --- /dev/null +++ b/react/features/authentication/actions.web.js @@ -0,0 +1,67 @@ +// @flow + +import { maybeRedirectToWelcomePage } from '../app/actions'; +import { hideDialog, openDialog } from '../base/dialog/actions'; + +import { + CANCEL_LOGIN +} from './actionTypes'; +import { WaitForOwnerDialog, LoginDialog } from './components'; + +/** + * 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. + * + * @returns {Function} + */ +export function cancelWaitForOwner() { + return (dispatch: Function) => { + dispatch(maybeRedirectToWelcomePage()); + }; +} + +/** + * Hides a authentication dialog where the local participant + * should authenticate. + * + * @returns {Function}. + */ +export function hideLoginDialog() { + return hideDialog(LoginDialog); +} + +/** + * Shows a authentication dialog where the local participant + * should authenticate. + * + * @returns {Function}. + */ +export function openLoginDialog() { + return openDialog(LoginDialog); +} + +/** + * Shows a notification dialog that authentication is required to create the. + * Conference, so the local participant should authenticate or wait for a + * host. + * + * @returns {Function}. + */ +export function openWaitForOwnerDialog() { + return openDialog(WaitForOwnerDialog); +} + + diff --git a/react/features/authentication/components/_.native.js b/react/features/authentication/components/_.native.js new file mode 100644 index 000000000..738c4d2b8 --- /dev/null +++ b/react/features/authentication/components/_.native.js @@ -0,0 +1 @@ +export * from './native'; diff --git a/react/features/authentication/components/_.web.js b/react/features/authentication/components/_.web.js new file mode 100644 index 000000000..b80c83af3 --- /dev/null +++ b/react/features/authentication/components/_.web.js @@ -0,0 +1 @@ +export * from './web'; diff --git a/react/features/authentication/components/index.js b/react/features/authentication/components/index.js index 52c59fc63..cda61441e 100644 --- a/react/features/authentication/components/index.js +++ b/react/features/authentication/components/index.js @@ -1,2 +1 @@ -export { default as LoginDialog } from './LoginDialog'; -export { default as WaitForOwnerDialog } from './WaitForOwnerDialog'; +export * from './_'; diff --git a/react/features/authentication/components/LoginDialog.native.js b/react/features/authentication/components/native/LoginDialog.js similarity index 95% rename from react/features/authentication/components/LoginDialog.native.js rename to react/features/authentication/components/native/LoginDialog.js index 3d1ebc149..383fa2857 100644 --- a/react/features/authentication/components/LoginDialog.native.js +++ b/react/features/authentication/components/native/LoginDialog.js @@ -5,20 +5,20 @@ import { Text, TextInput, View } from 'react-native'; import { connect as reduxConnect } from 'react-redux'; import type { Dispatch } from 'redux'; -import { ColorSchemeRegistry } from '../../base/color-scheme'; -import { toJid } from '../../base/connection'; -import { connect } from '../../base/connection/actions.native'; +import { ColorSchemeRegistry } from '../../../base/color-scheme'; +import { toJid } from '../../../base/connection'; +import { connect } from '../../../base/connection/actions.native'; import { CustomSubmitDialog, FIELD_UNDERLINE, PLACEHOLDER_COLOR, _abstractMapStateToProps, inputDialog as inputDialogStyle -} from '../../base/dialog'; -import { translate } from '../../base/i18n'; -import { JitsiConnectionErrors } from '../../base/lib-jitsi-meet'; -import type { StyleType } from '../../base/styles'; -import { authenticateAndUpgradeRole, cancelLogin } from '../actions'; +} from '../../../base/dialog'; +import { translate } from '../../../base/i18n'; +import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet'; +import type { StyleType } from '../../../base/styles'; +import { authenticateAndUpgradeRole, cancelLogin } from '../../actions.native'; // Register styles. import './styles'; diff --git a/react/features/authentication/components/WaitForOwnerDialog.native.js b/react/features/authentication/components/native/WaitForOwnerDialog.js similarity index 91% rename from react/features/authentication/components/WaitForOwnerDialog.native.js rename to react/features/authentication/components/native/WaitForOwnerDialog.js index d5db88ed9..d2458e727 100644 --- a/react/features/authentication/components/WaitForOwnerDialog.native.js +++ b/react/features/authentication/components/native/WaitForOwnerDialog.js @@ -3,10 +3,10 @@ import React, { Component } from 'react'; import type { Dispatch } from 'redux'; -import { ConfirmDialog } from '../../base/dialog'; -import { translate } from '../../base/i18n'; -import { connect } from '../../base/redux'; -import { cancelWaitForOwner, _openLoginDialog } from '../actions'; +import { ConfirmDialog } from '../../../base/dialog'; +import { translate } from '../../../base/i18n'; +import { connect } from '../../../base/redux'; +import { cancelWaitForOwner, _openLoginDialog } from '../../actions.native'; /** * The type of the React {@code Component} props of {@link WaitForOwnerDialog}. @@ -107,9 +107,7 @@ class WaitForOwnerDialog extends Component { * * @param {Object} state - The Redux state. * @private - * @returns {{ - * _room: string - * }} + * @returns {Props} */ function _mapStateToProps(state) { const { authRequired } = state['features/base/conference']; diff --git a/react/features/authentication/components/native/index.js b/react/features/authentication/components/native/index.js new file mode 100644 index 000000000..52c59fc63 --- /dev/null +++ b/react/features/authentication/components/native/index.js @@ -0,0 +1,2 @@ +export { default as LoginDialog } from './LoginDialog'; +export { default as WaitForOwnerDialog } from './WaitForOwnerDialog'; diff --git a/react/features/authentication/components/styles.js b/react/features/authentication/components/native/styles.js similarity index 86% rename from react/features/authentication/components/styles.js rename to react/features/authentication/components/native/styles.js index 205b345ec..653cad53e 100644 --- a/react/features/authentication/components/styles.js +++ b/react/features/authentication/components/native/styles.js @@ -1,5 +1,5 @@ -import { ColorSchemeRegistry, schemeColor } from '../../base/color-scheme'; -import { BoxModel } from '../../base/styles'; +import { ColorSchemeRegistry, schemeColor } from '../../../base/color-scheme'; +import { BoxModel } from '../../../base/styles'; /** * The styles of the authentication feature. diff --git a/react/features/authentication/components/web/LoginDialog.js b/react/features/authentication/components/web/LoginDialog.js new file mode 100644 index 000000000..c0c066ab8 --- /dev/null +++ b/react/features/authentication/components/web/LoginDialog.js @@ -0,0 +1,313 @@ +// @flow + +import { FieldTextStateless as TextField } from '@atlaskit/field-text'; +import React, { Component } from 'react'; +import type { Dispatch } from 'redux'; + +import { connect } from '../../../../../connection'; +import { toJid } from '../../../base/connection/functions'; +import { Dialog } from '../../../base/dialog'; +import { translate, translateToHTML } from '../../../base/i18n'; +import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet'; +import { connect as reduxConnect } from '../../../base/redux'; +import { authenticateAndUpgradeRole } from '../../actions.native'; +import { cancelLogin } from '../../actions.web'; + +/** + * The type of the React {@code Component} props of {@link LoginDialog}. + */ +type Props = { + + /** + * {@link JitsiConference} that needs authentication - will hold a valid + * value in XMPP login + guest access mode. + */ + _conference: Object, + + /** + * The server hosts specified in the global config. + */ + _configHosts: Object, + + /** + * Indicates if the dialog should display "connecting" status message. + */ + _connecting: boolean, + + /** + * The error which occurred during login/authentication. + */ + _error: Object, + + /** + * 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: Dispatch, + + /** + * Invoked when username and password are submitted. + */ + onSuccess: Function, + + /** + * Conference room name. + */ + roomName: string, + + /** + * Invoked to obtain translated strings. + */ + t: Function +} + +/** + * The type of the React {@code Component} state of {@link LoginDialog}. + */ +type State = { + + /** + * The user entered password for the conference. + */ + password: string, + + /** + * The user entered local participant name. + */ + username: string, + + /** + * Authentication process starts before joining the conference room. + */ + loginStarted: boolean +} + +/** + * Component that renders the login in conference dialog. + * + * @returns {React$Element} + */ +class LoginDialog extends Component { + /** + * Initializes a new {@code LoginDialog} instance. + * + * @inheritdoc + */ + constructor(props: Props) { + super(props); + + this.state = { + username: '', + password: '', + loginStarted: false + }; + + this._onCancelLogin = this._onCancelLogin.bind(this); + this._onLogin = this._onLogin.bind(this); + this._onChange = this._onChange.bind(this); + } + + _onCancelLogin: () => void; + + /** + * Called when the cancel button is clicked. + * + * @private + * @returns {void} + */ + _onCancelLogin() { + const { dispatch } = this.props; + + dispatch(cancelLogin()); + } + + _onLogin: () => void; + + /** + * Notifies this LoginDialog that the login button (OK) has been pressed by + * the user. + * + * @private + * @returns {void} + */ + _onLogin() { + const { + _conference: conference, + _configHosts: configHosts, + roomName, + onSuccess, + dispatch + } = this.props; + const { password, username } = this.state; + const jid = toJid(username, configHosts); + + if (conference) { + dispatch(authenticateAndUpgradeRole(jid, password, conference)); + } else { + this.setState({ + loginStarted: true + }); + + connect(jid, password, roomName) + .then(connection => { + onSuccess && onSuccess(connection); + }) + .catch(() => { + this.setState({ + loginStarted: false + }); + }); + } + } + + _onChange: Object => void; + + /** + * Callback for the onChange event of the field. + * + * @param {Object} evt - The static event. + * @returns {void} + */ + _onChange(evt: Object) { + this.setState({ + [evt.target.name]: evt.target.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 = {}; + let messageKey; + + if (progress && progress >= 0.5) { + messageKey = t('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) + && credentials.password === password) { + messageKey = t('dialog.incorrectPassword'); + } + } else if (name) { + messageKey = t('dialog.connectErrorWithMsg'); + messageOptions.msg = `${name} ${error.message}`; + } + } else if (connecting) { + messageKey = t('connection.CONNECTING'); + } + + if (messageKey) { + return ( + + { translateToHTML(t, messageKey, messageOptions) } + + ); + } + + return null; + } + + /** + * Implements {@Component#render}. + * + * @inheritdoc + */ + render() { + const { + _connecting: connecting, + t + } = this.props; + const { password, loginStarted, username } = this.state; + + return ( + + + + { this.renderMessage() } + + ); + } +} + +/** + * Maps (parts of) the Redux state to the associated props for the + * {@code LoginDialog} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {Props} + */ +function mapStateToProps(state) { + const { + error: authenticateAndUpgradeRoleError, + progress, + thenableWithCancel + } = state['features/authentication']; + const { authRequired } = state['features/base/conference']; + const { hosts: configHosts } = state['features/base/config']; + const { + connecting, + error: connectionError + } = state['features/base/connection']; + + return { + _conference: authRequired, + _configHosts: configHosts, + _connecting: connecting || thenableWithCancel, + _error: connectionError || authenticateAndUpgradeRoleError, + _progress: progress + }; +} + +export default translate(reduxConnect(mapStateToProps)(LoginDialog)); diff --git a/react/features/authentication/components/web/WaitForOwnerDialog.js b/react/features/authentication/components/web/WaitForOwnerDialog.js new file mode 100644 index 000000000..207cc7fc0 --- /dev/null +++ b/react/features/authentication/components/web/WaitForOwnerDialog.js @@ -0,0 +1,129 @@ +// @flow + +import React, { PureComponent } from 'react'; +import type { Dispatch } from 'redux'; + +import { Dialog } from '../../../base/dialog'; +import { translate, translateToHTML } from '../../../base/i18n'; +import { connect } from '../../../base/redux'; +import { openLoginDialog, cancelWaitForOwner } from '../../actions.web'; + +/** + * The type of the React {@code Component} props of {@link WaitForOwnerDialog}. + */ +type Props = { + + /** + * The name of the conference room (without the domain part). + */ + _room: string, + + /** + * Redux store dispatch method. + */ + dispatch: Dispatch, + + /** + * Function to be invoked after click. + */ + onAuthNow: ?Function, + + /** + * Invoked to obtain translated strings. + */ + t: Function +} + +/** + * Authentication message dialog for host confirmation. + * + * @returns {React$Element} + */ +class WaitForOwnerDialog extends PureComponent { + /** + * Instantiates a new component. + * + * @param {Object} props - The read-only properties with which the new + * instance is to be initialized. + */ + constructor(props: Props) { + super(props); + + this._onCancelWaitForOwner = this._onCancelWaitForOwner.bind(this); + this._onIAmHost = this._onIAmHost.bind(this); + } + + _onCancelWaitForOwner: () => void; + + /** + * Called when the cancel button is clicked. + * + * @private + * @returns {void} + */ + _onCancelWaitForOwner() { + const { dispatch } = this.props; + + dispatch(cancelWaitForOwner()); + } + + _onIAmHost: () => void; + + /** + * Called when the OK button is clicked. + * + * @private + * @returns {void} + */ + _onIAmHost() { + const { dispatch } = this.props; + + dispatch(openLoginDialog()); + } + + /** + * Implements React's {@link Component#render()}. + * + * @inheritdoc + */ + render() { + const { + _room, + t + } = this.props; + + return ( + + + { + translateToHTML( + t, 'dialog.WaitForHostMsg', { room: _room }) + } + + + ); + } +} + +/** + * Maps (parts of) the Redux state to the associated props for the + * {@code WaitForOwnerDialog} component. + * + * @param {Object} state - The Redux state. + * @private + * @returns {Props} + */ +function mapStateToProps(state) { + const { authRequired } = state['features/base/conference']; + + return { + _room: authRequired && authRequired.getName() + }; +} + +export default translate(connect(mapStateToProps)(WaitForOwnerDialog)); diff --git a/react/features/authentication/components/web/index.js b/react/features/authentication/components/web/index.js new file mode 100644 index 000000000..917f0ce38 --- /dev/null +++ b/react/features/authentication/components/web/index.js @@ -0,0 +1,4 @@ +// @flow + +export { default as WaitForOwnerDialog } from './WaitForOwnerDialog'; +export { default as LoginDialog } from './LoginDialog'; diff --git a/react/features/authentication/functions.js b/react/features/authentication/functions.js new file mode 100644 index 000000000..6735e548c --- /dev/null +++ b/react/features/authentication/functions.js @@ -0,0 +1,25 @@ +// @flow + +import JitsiMeetJS from '../../../react/features/base/lib-jitsi-meet'; + + +/** + * Checks if the token for authentication is available. + * + * @param {Object} config - Configuration state object from store. + * @returns {boolean} + */ +export const isTokenAuthEnabled = (config: Object) => + typeof config.tokenAuthUrl === 'string' + && config.tokenAuthUrl.length; + + +/** + * Token url. + * + * @param {Object} config - Configuration state object from store. + * @returns {string} + */ +export const getTokenAuthUrl = (config: Object) => + JitsiMeetJS.util.AuthUtil.getTokenAuthUrl.bind(null, + config.tokenAuthUrl); diff --git a/react/features/authentication/index.js b/react/features/authentication/index.js deleted file mode 100644 index 803dacd06..000000000 --- a/react/features/authentication/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export * from './actions'; -export * from './actionTypes'; -export * from './components'; diff --git a/react/features/authentication/middleware.js b/react/features/authentication/middleware.native.js similarity index 99% rename from react/features/authentication/middleware.js rename to react/features/authentication/middleware.native.js index 0f0cb3e7b..8b3477b14 100644 --- a/react/features/authentication/middleware.js +++ b/react/features/authentication/middleware.native.js @@ -26,7 +26,7 @@ import { _openWaitForOwnerDialog, stopWaitForOwner, waitForOwner -} from './actions'; +} from './actions.native'; import { LoginDialog, WaitForOwnerDialog } from './components'; /** diff --git a/react/features/authentication/middleware.web.js b/react/features/authentication/middleware.web.js new file mode 100644 index 000000000..715eda087 --- /dev/null +++ b/react/features/authentication/middleware.web.js @@ -0,0 +1,134 @@ +// @flow + +import { maybeRedirectToWelcomePage } from '../app/actions'; +import { + CONFERENCE_FAILED, + CONFERENCE_JOINED, + CONFERENCE_LEFT +} from '../base/conference'; +import { CONNECTION_ESTABLISHED } from '../base/connection'; +import { hideDialog, isDialogOpen } from '../base/dialog'; +import { + JitsiConferenceErrors +} from '../base/lib-jitsi-meet'; +import { MiddlewareRegistry } from '../base/redux'; + +import { + CANCEL_LOGIN, + STOP_WAIT_FOR_OWNER, + WAIT_FOR_OWNER +} from './actionTypes'; +import { + stopWaitForOwner, + waitForOwner +} from './actions.native'; +import { + hideLoginDialog, + openWaitForOwnerDialog +} from './actions.web'; +import { LoginDialog, WaitForOwnerDialog } from './components'; + +/** + * 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: { + if (!isDialogOpen(store, WaitForOwnerDialog)) { + if (isWaitingForOwner(store)) { + store.dispatch(openWaitForOwnerDialog()); + + return next(action); + } + + store.dispatch(hideLoginDialog()); + + store.dispatch(maybeRedirectToWelcomePage()); + } + break; + } + + case CONFERENCE_FAILED: { + const { error } = action; + let recoverable; + + if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) { + if (typeof error.recoverable === 'undefined') { + error.recoverable = true; + } + recoverable = error.recoverable; + } + if (recoverable) { + store.dispatch(waitForOwner()); + } else { + store.dispatch(stopWaitForOwner()); + } + break; + } + + case CONFERENCE_JOINED: + 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 STOP_WAIT_FOR_OWNER: + clearExistingWaitForOwnerTimeout(store); + store.dispatch(hideDialog(WaitForOwnerDialog)); + break; + + case WAIT_FOR_OWNER: { + clearExistingWaitForOwnerTimeout(store); + + const { handler, timeoutMs } = action; + + action.waitForOwnerTimeoutID = setTimeout(handler, timeoutMs); + + 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 }: { getState: Function }) { + 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 {void} + */ +function isWaitingForOwner({ getState }: { getState: Function }) { + return getState()['features/authentication'].waitForOwnerTimeoutID; +} diff --git a/react/features/authentication/reducer.js b/react/features/authentication/reducer.js index 7ebc9a848..1191943f8 100644 --- a/react/features/authentication/reducer.js +++ b/react/features/authentication/reducer.js @@ -1,4 +1,4 @@ -/* @flow */ +// @flow import { assign, ReducerRegistry } from '../base/redux'; @@ -10,6 +10,14 @@ import { WAIT_FOR_OWNER } from './actionTypes'; +/** + * 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('features/authentication', (state = {}, action) => { switch (action.type) { case CANCEL_LOGIN: