mirror of https://github.com/vector-im/riot-web
				
				
				
			Merge remote-tracking branch 'origin/develop' into aviraldg-babelrc
						commit
						74b443f0d3
					
				|  | @ -54,6 +54,9 @@ class Signup { | |||
|  * This exists for the lifetime of a user's attempt to register an account, | ||||
|  * so if their registration attempt fails for whatever reason and they | ||||
|  * try again, call register() on the same instance again. | ||||
|  * | ||||
|  * TODO: parts of this overlap heavily with InteractiveAuth in the js-sdk. It | ||||
|  * would be nice to make use of that rather than rolling our own version of it. | ||||
|  */ | ||||
| class Register extends Signup { | ||||
|     constructor(hsUrl, isUrl, opts) { | ||||
|  | @ -130,6 +133,18 @@ class Register extends Signup { | |||
|         this.password = password; | ||||
|         const client = this._createTemporaryClient(); | ||||
|         this.activeStage = null; | ||||
| 
 | ||||
|         // If there hasn't been a client secret set by this point,
 | ||||
|         // generate one for this session. It will only be used if
 | ||||
|         // we do email verification, but far simpler to just make
 | ||||
|         // sure we have one.
 | ||||
|         // We re-use this same secret over multiple calls to register
 | ||||
|         // so that the identity server can honour the sendAttempt
 | ||||
|         // parameter and not re-send email unless we actually want
 | ||||
|         // another mail to be sent.
 | ||||
|         if (!this.params.clientSecret) { | ||||
|             this.params.clientSecret = client.generateClientSecret(); | ||||
|         } | ||||
|         return this._tryRegister(client); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -53,66 +53,24 @@ class RecaptchaStage extends Stage { | |||
|     constructor(matrixClient, signupInstance) { | ||||
|         super(RecaptchaStage.TYPE, matrixClient, signupInstance); | ||||
|         this.defer = q.defer(); // resolved with the captcha response
 | ||||
|         this.publicKey = null; // from the HS
 | ||||
|         this.divId = null; // from the UI component
 | ||||
|     } | ||||
| 
 | ||||
|     // called when the UI component has loaded the recaptcha <div> so we can
 | ||||
|     // render to it.
 | ||||
|     // called when the recaptcha has been completed.
 | ||||
|     onReceiveData(data) { | ||||
|         if (!data || !data.divId) { | ||||
|         if (!data || !data.response) { | ||||
|             return; | ||||
|         } | ||||
|         this.divId = data.divId; | ||||
|         this._attemptRender(); | ||||
|         this.defer.resolve({ | ||||
|             auth: { | ||||
|                 type: 'm.login.recaptcha', | ||||
|                 response: data.response, | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     complete() { | ||||
|         var publicKey; | ||||
|         var serverParams = this.signupInstance.getServerData().params; | ||||
|         if (serverParams && serverParams["m.login.recaptcha"]) { | ||||
|             publicKey = serverParams["m.login.recaptcha"].public_key; | ||||
|         } | ||||
|         if (!publicKey) { | ||||
|             return q.reject({ | ||||
|                 message: "This server has not supplied enough information for Recaptcha " + | ||||
|                 "authentication", | ||||
|                 isFatal: true | ||||
|             }); | ||||
|         } | ||||
|         this.publicKey = publicKey; | ||||
|         this._attemptRender(); | ||||
|         return this.defer.promise; | ||||
|     } | ||||
| 
 | ||||
|     _attemptRender() { | ||||
|         if (!global.grecaptcha) { | ||||
|             console.error("grecaptcha not loaded!"); | ||||
|             return; | ||||
|         } | ||||
|         if (!this.publicKey) { | ||||
|             console.error("No public key for recaptcha!"); | ||||
|             return; | ||||
|         } | ||||
|         if (!this.divId) { | ||||
|             console.error("No div ID specified!"); | ||||
|             return; | ||||
|         } | ||||
|         console.log("Rendering to %s", this.divId); | ||||
|         var self = this; | ||||
|         global.grecaptcha.render(this.divId, { | ||||
|             sitekey: this.publicKey, | ||||
|             callback: function(response) { | ||||
|                 console.log("Received captcha response"); | ||||
|                 self.defer.resolve({ | ||||
|                     auth: { | ||||
|                         type: 'm.login.recaptcha', | ||||
|                         response: response | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| RecaptchaStage.TYPE = "m.login.recaptcha"; | ||||
| 
 | ||||
|  | @ -158,7 +116,11 @@ class EmailIdentityStage extends Stage { | |||
|             return this._completeVerify(); | ||||
|         } | ||||
| 
 | ||||
|         this.clientSecret = this.client.generateClientSecret(); | ||||
|         this.clientSecret = this.signupInstance.params.clientSecret; | ||||
|         if (!this.clientSecret) { | ||||
|             return q.reject(new Error("No client secret specified by Signup class!")); | ||||
|         } | ||||
| 
 | ||||
|         var nextLink = this.signupInstance.params.registrationUrl + | ||||
|                        '?client_secret=' + | ||||
|                        encodeURIComponent(this.clientSecret) + | ||||
|  |  | |||
|  | @ -77,6 +77,8 @@ import views$dialogs$EncryptedEventDialog from './components/views/dialogs/Encry | |||
| module.exports.components['views.dialogs.EncryptedEventDialog'] = views$dialogs$EncryptedEventDialog; | ||||
| import views$dialogs$ErrorDialog from './components/views/dialogs/ErrorDialog'; | ||||
| module.exports.components['views.dialogs.ErrorDialog'] = views$dialogs$ErrorDialog; | ||||
| import views$dialogs$InteractiveAuthDialog from './components/views/dialogs/InteractiveAuthDialog'; | ||||
| module.exports.components['views.dialogs.InteractiveAuthDialog'] = views$dialogs$InteractiveAuthDialog; | ||||
| import views$dialogs$LogoutPrompt from './components/views/dialogs/LogoutPrompt'; | ||||
| module.exports.components['views.dialogs.LogoutPrompt'] = views$dialogs$LogoutPrompt; | ||||
| import views$dialogs$NeedToRegisterDialog from './components/views/dialogs/NeedToRegisterDialog'; | ||||
|  | @ -117,6 +119,8 @@ import views$login$CasLogin from './components/views/login/CasLogin'; | |||
| module.exports.components['views.login.CasLogin'] = views$login$CasLogin; | ||||
| import views$login$CustomServerDialog from './components/views/login/CustomServerDialog'; | ||||
| module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog; | ||||
| import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents'; | ||||
| module.exports.components['views.login.InteractiveAuthEntryComponents'] = views$login$InteractiveAuthEntryComponents; | ||||
| import views$login$LoginFooter from './components/views/login/LoginFooter'; | ||||
| module.exports.components['views.login.LoginFooter'] = views$login$LoginFooter; | ||||
| import views$login$LoginHeader from './components/views/login/LoginHeader'; | ||||
|  |  | |||
|  | @ -28,6 +28,10 @@ var CaptchaForm = require("../../views/login/CaptchaForm"); | |||
| 
 | ||||
| var MIN_PASSWORD_LENGTH = 6; | ||||
| 
 | ||||
| /** | ||||
|  * TODO: It would be nice to make use of the InteractiveAuthEntryComponents | ||||
|  * here, rather than inventing our own. | ||||
|  */ | ||||
| module.exports = React.createClass({ | ||||
|     displayName: 'Registration', | ||||
| 
 | ||||
|  | @ -228,12 +232,9 @@ module.exports = React.createClass({ | |||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onCaptchaLoaded: function(divIdName) { | ||||
|     onCaptchaResponse: function(response) { | ||||
|         this.registerLogic.tellStage("m.login.recaptcha", { | ||||
|             divId: divIdName | ||||
|         }); | ||||
|         this.setState({ | ||||
|             busy: false // requires user input
 | ||||
|             response: response | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|  | @ -267,8 +268,15 @@ module.exports = React.createClass({ | |||
|                 ); | ||||
|                 break; | ||||
|             case "Register.STEP_m.login.recaptcha": | ||||
|                 var publicKey; | ||||
|                 var serverParams = this.registerLogic.getServerData().params; | ||||
|                 if (serverParams && serverParams["m.login.recaptcha"]) { | ||||
|                     publicKey = serverParams["m.login.recaptcha"].public_key; | ||||
|                 } | ||||
|                 registerStep = ( | ||||
|                     <CaptchaForm onCaptchaLoaded={this.onCaptchaLoaded} /> | ||||
|                     <CaptchaForm sitePublicKey={publicKey} | ||||
|                         onCaptchaResponse={this.onCaptchaResponse} | ||||
|                     /> | ||||
|                 ); | ||||
|                 break; | ||||
|             default: | ||||
|  |  | |||
|  | @ -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> | ||||
|         ); | ||||
|     }, | ||||
| }); | ||||
|  | @ -26,28 +26,34 @@ module.exports = React.createClass({ | |||
|     displayName: 'CaptchaForm', | ||||
| 
 | ||||
|     propTypes: { | ||||
|         onCaptchaLoaded: React.PropTypes.func.isRequired // called with div id name
 | ||||
|         sitePublicKey: React.PropTypes.string, | ||||
| 
 | ||||
|         // called with the captcha response
 | ||||
|         onCaptchaResponse: React.PropTypes.func, | ||||
|     }, | ||||
| 
 | ||||
|     getDefaultProps: function() { | ||||
|         return { | ||||
|             onCaptchaLoaded: function() { | ||||
|                 console.error("Unhandled onCaptchaLoaded"); | ||||
|             } | ||||
|             onCaptchaResponse: () => {}, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|         return { | ||||
|             errorText: null, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentDidMount: function() { | ||||
|         // Just putting a script tag into the returned jsx doesn't work, annoyingly,
 | ||||
|         // so we do this instead.
 | ||||
|         var self = this; | ||||
|         if (this.refs.recaptchaContainer) { | ||||
|         if (global.grecaptcha) { | ||||
|             // already loaded
 | ||||
|             this._onCaptchaLoaded(); | ||||
|         } else { | ||||
|             console.log("Loading recaptcha script..."); | ||||
|             var scriptTag = document.createElement('script'); | ||||
|             window.mx_on_recaptcha_loaded = function() { | ||||
|                 console.log("Loaded recaptcha script."); | ||||
|                 self.props.onCaptchaLoaded(DIV_ID); | ||||
|             }; | ||||
|             window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded()}; | ||||
|             scriptTag.setAttribute( | ||||
|                 'src', global.location.protocol+"//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit" | ||||
|             ); | ||||
|  | @ -55,13 +61,54 @@ module.exports = React.createClass({ | |||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _renderRecaptcha: function(divId) { | ||||
|         if (!global.grecaptcha) { | ||||
|             console.error("grecaptcha not loaded!"); | ||||
|             throw new Error("Recaptcha did not load successfully"); | ||||
|         } | ||||
| 
 | ||||
|         var publicKey = this.props.sitePublicKey; | ||||
|         if (!publicKey) { | ||||
|             console.error("No public key for recaptcha!"); | ||||
|             throw new Error( | ||||
|                 "This server has not supplied enough information for Recaptcha " | ||||
|                     + "authentication"); | ||||
|         } | ||||
| 
 | ||||
|         console.log("Rendering to %s", divId); | ||||
|         global.grecaptcha.render(divId, { | ||||
|             sitekey: publicKey, | ||||
|             callback: this.props.onCaptchaResponse, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     _onCaptchaLoaded: function() { | ||||
|         console.log("Loaded recaptcha script."); | ||||
|         try { | ||||
|             this._renderRecaptcha(DIV_ID); | ||||
|         } catch (e) { | ||||
|             this.setState({ | ||||
|                 errorText: e.toString(), | ||||
|             }) | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         // FIXME: Tight coupling with the div id and SignupStages.js
 | ||||
|         let error = null; | ||||
|         if (this.state.errorText) { | ||||
|             error = ( | ||||
|                 <div className="error"> | ||||
|                     {this.state.errorText} | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <div ref="recaptchaContainer"> | ||||
|                 This Home Server would like to make sure you are not a robot | ||||
|                 <div id={DIV_ID}></div> | ||||
|                 {error} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| }); | ||||
| }); | ||||
|  |  | |||
|  | @ -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; | ||||
| }; | ||||
|  | @ -21,6 +21,7 @@ import q from 'q'; | |||
| import sdk from '../../../index'; | ||||
| import MatrixClientPeg from '../../../MatrixClientPeg'; | ||||
| import DateUtils from '../../../DateUtils'; | ||||
| import Modal from '../../../Modal'; | ||||
| 
 | ||||
| export default class DevicesPanelEntry extends React.Component { | ||||
|     constructor(props, context) { | ||||
|  | @ -35,6 +36,7 @@ export default class DevicesPanelEntry extends React.Component { | |||
| 
 | ||||
|         this._onDeleteClick = this._onDeleteClick.bind(this); | ||||
|         this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this); | ||||
|         this._makeDeleteRequest = this._makeDeleteRequest.bind(this); | ||||
|     } | ||||
| 
 | ||||
|     componentWillUnmount() { | ||||
|  | @ -52,22 +54,44 @@ export default class DevicesPanelEntry extends React.Component { | |||
|     } | ||||
| 
 | ||||
|     _onDeleteClick() { | ||||
|         const device = this.props.device; | ||||
|         this.setState({deleting: true}); | ||||
| 
 | ||||
|         MatrixClientPeg.get().deleteDevice(device.device_id).done( | ||||
|         // try without interactive auth to start off
 | ||||
|         this._makeDeleteRequest(null).catch((error) => { | ||||
|             if (this._unmounted) { return; } | ||||
|             if (error.httpStatus !== 401 || !error.data || !error.data.flows) { | ||||
|                 // doesn't look like an interactive-auth failure
 | ||||
|                 throw e; | ||||
|             } | ||||
| 
 | ||||
|             // pop up an interactive auth dialog
 | ||||
|             var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); | ||||
| 
 | ||||
|             Modal.createDialog(InteractiveAuthDialog, { | ||||
|                 authData: error.data, | ||||
|                 makeRequest: this._makeDeleteRequest, | ||||
|             }); | ||||
| 
 | ||||
|             this.setState({ | ||||
|                 deleting: false, | ||||
|             }); | ||||
|         }).catch((e) => { | ||||
|             console.error("Error deleting device", e); | ||||
|             if (this._unmounted) { return; } | ||||
|             this.setState({ | ||||
|                 deleting: false, | ||||
|                 deleteError: "Failed to delete device", | ||||
|             }); | ||||
|         }).done(); | ||||
|     } | ||||
| 
 | ||||
|     _makeDeleteRequest(auth) { | ||||
|         const device = this.props.device; | ||||
|         return MatrixClientPeg.get().deleteDevice(device.device_id, auth).then( | ||||
|             () => { | ||||
|                 this.props.onDeleted(); | ||||
|                 if (this._unmounted) { return; } | ||||
|                 this.setState({ deleting: false }); | ||||
|             }, | ||||
|             (e) => { | ||||
|                 console.error("Error deleting device", e); | ||||
|                 if (this._unmounted) { return; } | ||||
|                 this.setState({ | ||||
|                     deleting: false, | ||||
|                     deleteError: "Failed to delete device", | ||||
|                 }); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|  |  | |||
|  | @ -0,0 +1,108 @@ | |||
| /* | ||||
| 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 expect from 'expect'; | ||||
| import q from 'q'; | ||||
| import React from 'react'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import ReactTestUtils from 'react-addons-test-utils'; | ||||
| import sinon from 'sinon'; | ||||
| 
 | ||||
| import sdk from 'matrix-react-sdk'; | ||||
| import MatrixClientPeg from 'MatrixClientPeg'; | ||||
| 
 | ||||
| import * as test_utils from '../../../test-utils'; | ||||
| 
 | ||||
| const InteractiveAuthDialog = sdk.getComponent( | ||||
|     'views.dialogs.InteractiveAuthDialog' | ||||
| ); | ||||
| 
 | ||||
| describe('InteractiveAuthDialog', function () { | ||||
|     var parentDiv; | ||||
|     var sandbox; | ||||
| 
 | ||||
|     beforeEach(function() { | ||||
|         test_utils.beforeEach(this); | ||||
|         sandbox = test_utils.stubClient(sandbox); | ||||
|         parentDiv = document.createElement('div'); | ||||
|         document.body.appendChild(parentDiv); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(function() { | ||||
|         ReactDOM.unmountComponentAtNode(parentDiv); | ||||
|         parentDiv.remove(); | ||||
|         sandbox.restore(); | ||||
|     }); | ||||
| 
 | ||||
|     it('Should successfully complete a password flow', function(done) { | ||||
|         const onFinished = sinon.spy(); | ||||
|         const doRequest = sinon.stub().returns(q({a:1})); | ||||
| 
 | ||||
|         // tell the stub matrixclient to return a real userid
 | ||||
|         var client = MatrixClientPeg.get(); | ||||
|         client.credentials = {userId: "@user:id"}; | ||||
| 
 | ||||
|         const dlg = ReactDOM.render( | ||||
|             <InteractiveAuthDialog | ||||
|                 authData={{ | ||||
|                     session: "sess", | ||||
|                     flows: [ | ||||
|                         {"stages":["m.login.password"]} | ||||
|                     ] | ||||
|                 }} | ||||
|                 makeRequest={doRequest} | ||||
|                 onFinished={onFinished} | ||||
|             />, parentDiv); | ||||
| 
 | ||||
|         // at this point there should be a password box
 | ||||
|         const passwordNode = ReactTestUtils.findRenderedDOMComponentWithTag( | ||||
|             dlg, "input" | ||||
|         ); | ||||
|         expect(passwordNode.type).toEqual("password"); | ||||
| 
 | ||||
|         // 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
 | ||||
|         // trigger a request
 | ||||
|         passwordNode.value = "s3kr3t"; | ||||
|         ReactTestUtils.Simulate.change(passwordNode); | ||||
|         expect(submitNode.disabled).toBe(false); | ||||
|         ReactTestUtils.Simulate.keyDown(passwordNode, { | ||||
|             key: "Enter", keyCode: 13, which: 13, | ||||
|         }); | ||||
| 
 | ||||
|         expect(doRequest.callCount).toEqual(1); | ||||
|         expect(doRequest.calledWithExactly({ | ||||
|             session: "sess", | ||||
|             type: "m.login.password", | ||||
|             password: "s3kr3t", | ||||
|             user: "@user:id", | ||||
|         })).toBe(true); | ||||
| 
 | ||||
|         // the submit button should now be disabled (and be a spinner)
 | ||||
|         expect(submitNode.disabled).toBe(true); | ||||
| 
 | ||||
|         // let the request complete
 | ||||
|         q.delay(1).then(() => { | ||||
|             expect(onFinished.callCount).toEqual(1); | ||||
|             expect(onFinished.calledWithExactly(true, {a:1})).toBe(true); | ||||
|         }).done(done, done); | ||||
|     }); | ||||
| }); | ||||
		Loading…
	
		Reference in New Issue
	
	 David Baker
						David Baker