InteractiveAuthDialog

A dialog which will take the user through an interactive-auth process
This commit is contained in:
Richard van der Hoff 2016-10-11 18:04:55 +01:00
parent 6ffddabaaa
commit 1a81397d31
4 changed files with 540 additions and 0 deletions

View file

@ -51,6 +51,7 @@ module.exports.components['views.dialogs.ChatInviteDialog'] = require('./compone
module.exports.components['views.dialogs.DeactivateAccountDialog'] = require('./components/views/dialogs/DeactivateAccountDialog');
module.exports.components['views.dialogs.EncryptedEventDialog'] = require('./components/views/dialogs/EncryptedEventDialog');
module.exports.components['views.dialogs.ErrorDialog'] = require('./components/views/dialogs/ErrorDialog');
module.exports.components['views.dialogs.InteractiveAuthDialog'] = require('./components/views/dialogs/InteractiveAuthDialog');
module.exports.components['views.dialogs.LogoutPrompt'] = require('./components/views/dialogs/LogoutPrompt');
module.exports.components['views.dialogs.NeedToRegisterDialog'] = require('./components/views/dialogs/NeedToRegisterDialog');
module.exports.components['views.dialogs.QuestionDialog'] = require('./components/views/dialogs/QuestionDialog');

View file

@ -0,0 +1,219 @@
/*
Copyright 2016 OpenMarket 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 '../login/InteractiveAuthEntryComponents';
export default React.createClass({
displayName: 'InteractiveAuthDialog',
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,
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,
});
});
},
_onKeyDown: function(e) {
if (e.keyCode === 27) { // escape
e.stopPropagation();
e.preventDefault();
if (!this.state.busy) {
this._onCancel();
}
}
else if (e.keyCode === 13) { // enter
e.stopPropagation();
e.preventDefault();
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");
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 (
<div className="mx_InteractiveAuthDialog" onKeyDown={this._onKeyDown}>
<div className="mx_Dialog_title">
{this.props.title}
</div>
<div className="mx_Dialog_content">
<p>This operation requires additional authentication.</p>
{this._renderCurrentStage()}
{error}
</div>
<div className="mx_Dialog_buttons">
{submitButton}
{cancelButton}
</div>
</div>
);
},
});

View file

@ -0,0 +1,212 @@
/*
Copyright 2016 OpenMarket 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 React from 'react';
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
* for an auth stage. (The intention is that they could also be used for other
* components, such as the registration flow).
*
* Call getEntryComponentForLoginType() to get a component suitable for a
* particular login type. Each component requires the same properties:
*
* loginType: the login type of the auth stage being attempted
* authSessionId: session id from the server
* 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
*
* 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.
*/
export const PasswordAuthEntry = React.createClass({
displayName: 'PasswordAuthEntry',
statics: {
LOGIN_TYPE: "m.login.password",
},
propTypes: {
submitAuthDict: React.PropTypes.func.isRequired,
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string,
},
componentWillMount: function() {
this.props.setSubmitButtonEnabled(false);
},
focus: function() {
if (this.refs.passwordField) {
this.refs.passwordField.focus();
}
},
onSubmitClick: function() {
this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE,
user: MatrixClientPeg.get().credentials.userId,
password: this.refs.passwordField.value,
});
},
_onPasswordFieldChange: function (ev) {
// enable the submit button iff the password is non-empty
this.props.setSubmitButtonEnabled(Boolean(ev.target.value));
},
render: function() {
let passwordBoxClass = null;
if (this.props.errorText) {
passwordBoxClass = 'error';
}
return (
<div>
<p>To continue, please enter your password.</p>
<p>Password:</p>
<input
ref="passwordField"
className={passwordBoxClass}
onChange={this._onPasswordFieldChange}
type="password"
/>
<div className="error">
{this.props.errorText}
</div>
</div>
);
},
});
export const RecaptchaAuthEntry = React.createClass({
displayName: 'RecaptchaAuthEntry',
statics: {
LOGIN_TYPE: "m.login.recaptcha",
},
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,
response: response,
});
},
render: function() {
const CaptchaForm = sdk.getComponent("views.login.CaptchaForm");
var sitePublicKey = this.props.stageParams.public_key;
return (
<div>
<CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse}
/>
<div className="error">
{this.props.errorText}
</div>
</div>
);
},
});
export const FallbackAuthEntry = React.createClass({
displayName: 'FallbackAuthEntry',
propTypes: {
authSessionId: React.PropTypes.string.isRequired,
loginType: React.PropTypes.string.isRequired,
submitAuthDict: React.PropTypes.func.isRequired,
setSubmitButtonEnabled: React.PropTypes.func.isRequired,
errorText: React.PropTypes.string,
},
componentWillMount: function() {
// 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);
},
componentWillUnmount: function() {
window.removeEventListener("message", this._onReceiveMessage);
if (this._popupWindow) {
this._popupWindow.close();
}
},
onSubmitClick: function() {
var url = MatrixClientPeg.get().getFallbackAuthUrl(
this.props.loginType,
this.props.authSessionId
);
this._popupWindow = window.open(url);
this.props.setSubmitButtonEnabled(false);
},
_onReceiveMessage: function(event) {
if (
event.data === "authDone" &&
event.origin === MatrixClientPeg.get().getHomeserverUrl()
) {
this.props.submitAuthDict({});
}
},
render: function() {
return (
<div>
Click "Submit" to authenticate
<div className="error">
{this.props.errorText}
</div>
</div>
);
},
});
const AuthEntryComponents = [
PasswordAuthEntry,
RecaptchaAuthEntry,
];
export function getEntryComponentForLoginType(loginType) {
for (var c of AuthEntryComponents) {
if (c.LOGIN_TYPE == loginType) {
return c;
}
}
return FallbackAuthEntry;
};