feat(authentication) refactor auth dialogs to use React

This commit is contained in:
Calinteodor 2021-03-24 16:09:40 +02:00 committed by GitHub
parent 11202595bd
commit e035d33fa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 775 additions and 334 deletions

View File

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

View File

@ -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<JitsiConnection>} 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<JitsiConnection>}
*/
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 })
);
});
}

View File

@ -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 <b>{{room}}</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"WaitForHostMsgWOk": "The conference <b>{{room}}</b> 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"
},

View File

@ -146,7 +146,6 @@ UI.start = function() {
}
APP.store.dispatch(setToolboxEnabled(false));
UI.messageHandler.enablePopups(false);
}
};

View File

@ -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<JitsiConnection>}
*/
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
};

View File

@ -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();
}
}
);
}
};

View File

@ -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());
}
};

View File

@ -1,5 +1,6 @@
// @flow
import '../authentication/middleware';
import '../base/devices/middleware';
import '../e2ee/middleware';
import '../external-api/middleware';

View File

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

View File

@ -1,6 +1,5 @@
// @flow
import '../authentication/reducer';
import '../mobile/audio-mode/reducer';
import '../mobile/background/reducer';
import '../mobile/call-integration/reducer';

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './native';

View File

@ -0,0 +1 @@
export * from './web';

View File

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

View File

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

View File

@ -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<Props> {
*
* @param {Object} state - The Redux state.
* @private
* @returns {{
* _room: string
* }}
* @returns {Props}
*/
function _mapStateToProps(state) {
const { authRequired } = state['features/base/conference'];

View File

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

View File

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

View File

@ -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<any>,
/**
* 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<any>}
*/
class LoginDialog extends Component<Props, State> {
/**
* 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 (
<span>
{ translateToHTML(t, messageKey, messageOptions) }
</span>
);
}
return null;
}
/**
* Implements {@Component#render}.
*
* @inheritdoc
*/
render() {
const {
_connecting: connecting,
t
} = this.props;
const { password, loginStarted, username } = this.state;
return (
<Dialog
okDisabled = {
connecting
|| loginStarted
|| !password
|| !username
}
okKey = { t('dialog.login') }
onCancel = { this._onCancelLogin }
onSubmit = { this._onLogin }
titleKey = { t('dialog.authenticationRequired') }
width = { 'small' }>
<TextField
autoFocus = { true }
className = 'input-control'
compact = { false }
label = { t('dialog.user') }
name = 'username'
onChange = { this._onChange }
placeholder = { t('dialog.userIdentifier') }
shouldFitContainer = { true }
type = 'text'
value = { username } />
<TextField
className = 'input-control'
compact = { false }
label = { t('dialog.userPassword') }
name = 'password'
onChange = { this._onChange }
shouldFitContainer = { true }
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 {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));

View File

@ -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<any>,
/**
* Function to be invoked after click.
*/
onAuthNow: ?Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
}
/**
* Authentication message dialog for host confirmation.
*
* @returns {React$Element<any>}
*/
class WaitForOwnerDialog extends PureComponent<Props> {
/**
* 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 (
<Dialog
okKey = { t('dialog.IamHost') }
onCancel = { this._onCancelWaitForOwner }
onSubmit = { this._onIAmHost }
titleKey = { t('dialog.WaitingForHostTitle') }
width = { 'small' }>
<span>
{
translateToHTML(
t, 'dialog.WaitForHostMsg', { room: _room })
}
</span>
</Dialog>
);
}
}
/**
* 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));

View File

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

View File

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

View File

@ -1,3 +0,0 @@
export * from './actions';
export * from './actionTypes';
export * from './components';

View File

@ -26,7 +26,7 @@ import {
_openWaitForOwnerDialog,
stopWaitForOwner,
waitForOwner
} from './actions';
} from './actions.native';
import { LoginDialog, WaitForOwnerDialog } from './components';
/**

View File

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

View File

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