Merge pull request #691 from matrix-org/dbkr/interactive_auth_nondialog
Split out InterActiveAuthDialog
This commit is contained in:
commit
17b08aedfc
6 changed files with 239 additions and 183 deletions
|
@ -31,6 +31,8 @@ import structures$CreateRoom from './components/structures/CreateRoom';
|
|||
structures$CreateRoom && (module.exports.components['structures.CreateRoom'] = structures$CreateRoom);
|
||||
import structures$FilePanel from './components/structures/FilePanel';
|
||||
structures$FilePanel && (module.exports.components['structures.FilePanel'] = structures$FilePanel);
|
||||
import structures$InteractiveAuth from './components/structures/InteractiveAuth';
|
||||
structures$InteractiveAuth && (module.exports.components['structures.InteractiveAuth'] = structures$InteractiveAuth);
|
||||
import structures$LoggedInView from './components/structures/LoggedInView';
|
||||
structures$LoggedInView && (module.exports.components['structures.LoggedInView'] = structures$LoggedInView);
|
||||
import structures$MatrixChat from './components/structures/MatrixChat';
|
||||
|
|
152
src/components/structures/InteractiveAuth.js
Normal file
152
src/components/structures/InteractiveAuth.js
Normal file
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import sdk from '../../index';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../views/login/InteractiveAuthEntryComponents';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'InteractiveAuth',
|
||||
|
||||
propTypes: {
|
||||
// response from initial request. If not supplied, will do a request on
|
||||
// mount.
|
||||
authData: React.PropTypes.shape({
|
||||
flows: React.PropTypes.array,
|
||||
params: React.PropTypes.object,
|
||||
session: React.PropTypes.string,
|
||||
}),
|
||||
|
||||
// callback
|
||||
makeRequest: React.PropTypes.func.isRequired,
|
||||
|
||||
// callback called when the auth process has finished
|
||||
// @param {bool} status True if the operation requiring
|
||||
// auth was completed sucessfully, false if canceled.
|
||||
// @param result The result of the authenticated call
|
||||
onFinished: React.PropTypes.func.isRequired,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authStage: null,
|
||||
busy: false,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
submitButtonEnabled: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._authLogic = new InteractiveAuth({
|
||||
authData: this.props.authData,
|
||||
doRequest: this._requestCallback,
|
||||
startAuthStage: this._startAuthStage,
|
||||
});
|
||||
|
||||
this._authLogic.attemptAuth().then((result) => {
|
||||
this.props.onFinished(true, result);
|
||||
}).catch((error) => {
|
||||
console.error("Error during user-interactive auth:", error);
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = error.message || error.toString();
|
||||
this.setState({
|
||||
errorText: msg
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_startAuthStage: function(stageType, error) {
|
||||
this.setState({
|
||||
authStage: stageType,
|
||||
errorText: error ? error.error : null,
|
||||
}, this._setFocus);
|
||||
},
|
||||
|
||||
_requestCallback: function(auth) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
return this.props.makeRequest(auth).finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_setFocus: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
|
||||
this.refs.stageComponent.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_submitAuthDict: function(authData) {
|
||||
this._authLogic.submitAuthDict(authData);
|
||||
},
|
||||
|
||||
_renderCurrentStage: function() {
|
||||
const stage = this.state.authStage;
|
||||
var StageComponent = getEntryComponentForLoginType(stage);
|
||||
return (
|
||||
<StageComponent ref="stageComponent"
|
||||
loginType={stage}
|
||||
authSessionId={this._authLogic.getSessionId()}
|
||||
stageParams={this._authLogic.getStageParams(stage)}
|
||||
submitAuthDict={this._submitAuthDict}
|
||||
errorText={this.state.stageErrorText}
|
||||
busy={this.state.busy}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{this._renderCurrentStage()}
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
|
||||
import * as KeyCode from '../../../KeyCode';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
/**
|
||||
* Basic container for modal dialogs.
|
||||
|
@ -59,9 +60,21 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_onCancelClick: function(e) {
|
||||
this.props.onFinished();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div onKeyDown={this._onKeyDown} className={this.props.className}>
|
||||
<AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<img
|
||||
src="img/cancel.svg" width="18" height="18"
|
||||
alt="Cancel" title="Cancel"
|
||||
/>
|
||||
</AccessibleButton>
|
||||
<div className='mx_Dialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,13 +16,12 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
const InteractiveAuth = Matrix.InteractiveAuth;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import sdk from '../../../index';
|
||||
|
||||
import {getEntryComponentForLoginType} from '../login/InteractiveAuthEntryComponents';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'InteractiveAuthDialog',
|
||||
|
@ -41,168 +41,29 @@ export default React.createClass({
|
|||
onFinished: React.PropTypes.func.isRequired,
|
||||
|
||||
title: React.PropTypes.string,
|
||||
submitButtonLabel: React.PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title: "Authentication",
|
||||
submitButtonLabel: "Submit",
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
authStage: null,
|
||||
busy: false,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
submitButtonEnabled: false,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._unmounted = false;
|
||||
this._authLogic = new InteractiveAuth({
|
||||
authData: this.props.authData,
|
||||
doRequest: this._requestCallback,
|
||||
startAuthStage: this._startAuthStage,
|
||||
});
|
||||
|
||||
this._authLogic.attemptAuth().then((result) => {
|
||||
this.props.onFinished(true, result);
|
||||
}).catch((error) => {
|
||||
console.error("Error during user-interactive auth:", error);
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const msg = error.message || error.toString();
|
||||
this.setState({
|
||||
errorText: msg
|
||||
});
|
||||
}).done();
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
|
||||
_startAuthStage: function(stageType, error) {
|
||||
this.setState({
|
||||
authStage: stageType,
|
||||
errorText: error ? error.error : null,
|
||||
}, this._setFocus);
|
||||
},
|
||||
|
||||
_requestCallback: function(auth) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
return this.props.makeRequest(auth).finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_onEnterPressed: function(e) {
|
||||
if (this.state.submitButtonEnabled && !this.state.busy) {
|
||||
this._onSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
_onSubmit: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.onSubmitClick) {
|
||||
this.refs.stageComponent.onSubmitClick();
|
||||
}
|
||||
},
|
||||
|
||||
_setFocus: function() {
|
||||
if (this.refs.stageComponent && this.refs.stageComponent.focus) {
|
||||
this.refs.stageComponent.focus();
|
||||
}
|
||||
},
|
||||
|
||||
_onCancel: function() {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
|
||||
_setSubmitButtonEnabled: function(enabled) {
|
||||
this.setState({
|
||||
submitButtonEnabled: enabled,
|
||||
});
|
||||
},
|
||||
|
||||
_submitAuthDict: function(authData) {
|
||||
this._authLogic.submitAuthDict(authData);
|
||||
},
|
||||
|
||||
_renderCurrentStage: function() {
|
||||
const stage = this.state.authStage;
|
||||
var StageComponent = getEntryComponentForLoginType(stage);
|
||||
return (
|
||||
<StageComponent ref="stageComponent"
|
||||
loginType={stage}
|
||||
authSessionId={this._authLogic.getSessionId()}
|
||||
stageParams={this._authLogic.getStageParams(stage)}
|
||||
submitAuthDict={this._submitAuthDict}
|
||||
setSubmitButtonEnabled={this._setSubmitButtonEnabled}
|
||||
errorText={this.state.stageErrorText}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
||||
let error = null;
|
||||
if (this.state.errorText) {
|
||||
error = (
|
||||
<div className="error">
|
||||
{this.state.errorText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const submitLabel = this.state.busy ? <Loader /> : this.props.submitButtonLabel;
|
||||
const submitEnabled = this.state.submitButtonEnabled && !this.state.busy;
|
||||
|
||||
const submitButton = (
|
||||
<button className="mx_Dialog_primary"
|
||||
onClick={this._onSubmit}
|
||||
disabled={!submitEnabled}
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
);
|
||||
|
||||
const cancelButton = (
|
||||
<button onClick={this._onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseDialog className="mx_InteractiveAuthDialog"
|
||||
onEnterPressed={this._onEnterPressed}
|
||||
onFinished={this.props.onFinished}
|
||||
title={this.props.title}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>This operation requires additional authentication.</p>
|
||||
{this._renderCurrentStage()}
|
||||
{error}
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
{submitButton}
|
||||
{cancelButton}
|
||||
<div>
|
||||
<InteractiveAuth ref={this._collectInteractiveAuth}
|
||||
authData={this.props.authData}
|
||||
makeRequest={this.props.makeRequest}
|
||||
onFinished={this.props.onFinished}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,7 +21,7 @@ import sdk from '../../../index';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuthDialog to prompt the user to enter the information needed
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
* for an auth stage. (The intention is that they could also be used for other
|
||||
* components, such as the registration flow).
|
||||
*
|
||||
|
@ -32,10 +33,10 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
|||
* stageParams: params from the server for the stage being attempted
|
||||
* errorText: error message from a previous attempt to authenticate
|
||||
* submitAuthDict: a function which will be called with the new auth dict
|
||||
* setSubmitButtonEnabled: a function which will enable/disable the 'submit' button
|
||||
* busy: a boolean indicating whether the auth logic is doing something
|
||||
* the user needs to wait for.
|
||||
*
|
||||
* Each component may also provide the following functions (beyond the standard React ones):
|
||||
* onSubmitClick: handle a 'submit' button click
|
||||
* focus: set the input focus appropriately in the form.
|
||||
*/
|
||||
|
||||
|
@ -48,12 +49,16 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
propTypes: {
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
// is the auth logic currently waiting for something to
|
||||
// happen?
|
||||
busy: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.setSubmitButtonEnabled(false);
|
||||
getInitialState: function() {
|
||||
return {
|
||||
passwordValid: false,
|
||||
};
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
|
@ -62,7 +67,10 @@ export const PasswordAuthEntry = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSubmitClick: function() {
|
||||
_onSubmit: function(e) {
|
||||
e.preventDefault();
|
||||
if (this.props.busy) return;
|
||||
|
||||
this.props.submitAuthDict({
|
||||
type: PasswordAuthEntry.LOGIN_TYPE,
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
|
@ -72,7 +80,9 @@ export const PasswordAuthEntry = React.createClass({
|
|||
|
||||
_onPasswordFieldChange: function(ev) {
|
||||
// enable the submit button iff the password is non-empty
|
||||
this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
|
||||
this.setState({
|
||||
passwordValid: Boolean(this.refs.passwordField.value),
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -82,16 +92,34 @@ export const PasswordAuthEntry = React.createClass({
|
|||
passwordBoxClass = 'error';
|
||||
}
|
||||
|
||||
let submitButtonOrSpinner;
|
||||
if (this.props.busy) {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
submitButtonOrSpinner = <Loader />;
|
||||
} else {
|
||||
submitButtonOrSpinner = (
|
||||
<input type="submit"
|
||||
className="mx_Dialog_primary"
|
||||
disabled={!this.state.passwordValid}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>To continue, please enter your password.</p>
|
||||
<p>Password:</p>
|
||||
<form onSubmit={this._onSubmit}>
|
||||
<input
|
||||
ref="passwordField"
|
||||
className={passwordBoxClass}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
type="password"
|
||||
/>
|
||||
<div className="mx_button_row">
|
||||
{submitButtonOrSpinner}
|
||||
</div>
|
||||
</form>
|
||||
<div className="error">
|
||||
{this.props.errorText}
|
||||
</div>
|
||||
|
@ -110,14 +138,9 @@ export const RecaptchaAuthEntry = React.createClass({
|
|||
propTypes: {
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
stageParams: React.PropTypes.object.isRequired,
|
||||
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.setSubmitButtonEnabled(false);
|
||||
},
|
||||
|
||||
_onCaptchaResponse: function(response) {
|
||||
this.props.submitAuthDict({
|
||||
type: RecaptchaAuthEntry.LOGIN_TYPE,
|
||||
|
@ -148,7 +171,6 @@ export const FallbackAuthEntry = React.createClass({
|
|||
authSessionId: React.PropTypes.string.isRequired,
|
||||
loginType: React.PropTypes.string.isRequired,
|
||||
submitAuthDict: React.PropTypes.func.isRequired,
|
||||
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
|
||||
errorText: React.PropTypes.string,
|
||||
},
|
||||
|
||||
|
@ -156,7 +178,6 @@ export const FallbackAuthEntry = React.createClass({
|
|||
// we have to make the user click a button, as browsers will block
|
||||
// the popup if we open it immediately.
|
||||
this._popupWindow = null;
|
||||
this.props.setSubmitButtonEnabled(true);
|
||||
window.addEventListener("message", this._onReceiveMessage);
|
||||
},
|
||||
|
||||
|
@ -167,13 +188,12 @@ export const FallbackAuthEntry = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onSubmitClick: function() {
|
||||
_onShowFallbackClick: function() {
|
||||
var url = MatrixClientPeg.get().getFallbackAuthUrl(
|
||||
this.props.loginType,
|
||||
this.props.authSessionId
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this.props.setSubmitButtonEnabled(false);
|
||||
},
|
||||
|
||||
_onReceiveMessage: function(event) {
|
||||
|
@ -188,7 +208,7 @@ export const FallbackAuthEntry = React.createClass({
|
|||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
Click "Submit" to authenticate
|
||||
<a onClick={this._onShowFallbackClick}>Start authentication</a>
|
||||
<div className="error">
|
||||
{this.props.errorText}
|
||||
</div>
|
||||
|
|
|
@ -67,16 +67,24 @@ describe('InteractiveAuthDialog', function () {
|
|||
onFinished={onFinished}
|
||||
/>, parentDiv);
|
||||
|
||||
// at this point there should be a password box
|
||||
const passwordNode = ReactTestUtils.findRenderedDOMComponentWithTag(
|
||||
// at this point there should be a password box and a submit button
|
||||
const formNode = ReactTestUtils.findRenderedDOMComponentWithTag(dlg, "form");
|
||||
const inputNodes = ReactTestUtils.scryRenderedDOMComponentsWithTag(
|
||||
dlg, "input"
|
||||
);
|
||||
expect(passwordNode.type).toEqual("password");
|
||||
let passwordNode;
|
||||
let submitNode;
|
||||
for (const node of inputNodes) {
|
||||
if (node.type == 'password') {
|
||||
passwordNode = node;
|
||||
} else if (node.type == 'submit') {
|
||||
submitNode = node;
|
||||
}
|
||||
}
|
||||
expect(passwordNode).toExist();
|
||||
expect(submitNode).toExist();
|
||||
|
||||
// submit should be disabled
|
||||
const submitNode = ReactTestUtils.findRenderedDOMComponentWithClass(
|
||||
dlg, "mx_Dialog_primary"
|
||||
);
|
||||
expect(submitNode.disabled).toBe(true);
|
||||
|
||||
// put something in the password box, and hit enter; that should
|
||||
|
@ -84,9 +92,7 @@ describe('InteractiveAuthDialog', function () {
|
|||
passwordNode.value = "s3kr3t";
|
||||
ReactTestUtils.Simulate.change(passwordNode);
|
||||
expect(submitNode.disabled).toBe(false);
|
||||
ReactTestUtils.Simulate.keyDown(passwordNode, {
|
||||
key: "Enter", keyCode: 13, which: 13,
|
||||
});
|
||||
ReactTestUtils.Simulate.submit(formNode, {});
|
||||
|
||||
expect(doRequest.callCount).toEqual(1);
|
||||
expect(doRequest.calledWithExactly({
|
||||
|
@ -96,8 +102,10 @@ describe('InteractiveAuthDialog', function () {
|
|||
user: "@user:id",
|
||||
})).toBe(true);
|
||||
|
||||
// the submit button should now be disabled (and be a spinner)
|
||||
expect(submitNode.disabled).toBe(true);
|
||||
// there should now be a spinner
|
||||
ReactTestUtils.findRenderedComponentWithType(
|
||||
dlg, sdk.getComponent('elements.Spinner'),
|
||||
);
|
||||
|
||||
// let the request complete
|
||||
q.delay(1).then(() => {
|
||||
|
|
Loading…
Reference in a new issue