Merge pull request #729 from matrix-org/dbkr/register_ui_auth
Port registration over to use InteractiveAuthpull/21833/head
						commit
						1bdf213d67
					
				|  | @ -155,7 +155,7 @@ function _loginWithToken(queryParams, defaultDeviceDisplayName) { | |||
| function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) { | ||||
|     console.log("Doing guest login on %s", hsUrl); | ||||
| 
 | ||||
|     // TODO: we should probably de-duplicate this and Signup.Login.loginAsGuest.
 | ||||
|     // TODO: we should probably de-duplicate this and Login.loginAsGuest.
 | ||||
|     // Not really sure where the right home for it is.
 | ||||
| 
 | ||||
|     // create a temporary MatrixClient to do the login
 | ||||
|  | @ -315,6 +315,9 @@ export function setLoggedIn(credentials) { | |||
|         console.warn("No local storage available: can't persist session!"); | ||||
|     } | ||||
| 
 | ||||
|     // stop any running clients before we create a new one with these new credentials
 | ||||
|     stopMatrixClient(); | ||||
| 
 | ||||
|     MatrixClientPeg.replaceUsingCreds(credentials); | ||||
| 
 | ||||
|     teamPromise.then((teamToken) => { | ||||
|  |  | |||
|  | @ -0,0 +1,178 @@ | |||
| /* | ||||
| Copyright 2015, 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. | ||||
| 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"; | ||||
| 
 | ||||
| import q from 'q'; | ||||
| import url from 'url'; | ||||
| 
 | ||||
| export default class Login { | ||||
|     constructor(hsUrl, isUrl, fallbackHsUrl, opts) { | ||||
|         this._hsUrl = hsUrl; | ||||
|         this._isUrl = isUrl; | ||||
|         this._fallbackHsUrl = fallbackHsUrl; | ||||
|         this._currentFlowIndex = 0; | ||||
|         this._flows = []; | ||||
|         this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName; | ||||
|     } | ||||
| 
 | ||||
|     getHomeserverUrl() { | ||||
|         return this._hsUrl; | ||||
|     } | ||||
| 
 | ||||
|     getIdentityServerUrl() { | ||||
|         return this._isUrl; | ||||
|     } | ||||
| 
 | ||||
|     setHomeserverUrl(hsUrl) { | ||||
|         this._hsUrl = hsUrl; | ||||
|     } | ||||
| 
 | ||||
|     setIdentityServerUrl(isUrl) { | ||||
|         this._isUrl = isUrl; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a temporary MatrixClient, which can be used for login or register | ||||
|      * requests. | ||||
|      */ | ||||
|     _createTemporaryClient() { | ||||
|         return Matrix.createClient({ | ||||
|             baseUrl: this._hsUrl, | ||||
|             idBaseUrl: this._isUrl, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     getFlows() { | ||||
|         var self = this; | ||||
|         var client = this._createTemporaryClient(); | ||||
|         return client.loginFlows().then(function(result) { | ||||
|             self._flows = result.flows; | ||||
|             self._currentFlowIndex = 0; | ||||
|             // technically the UI should display options for all flows for the
 | ||||
|             // user to then choose one, so return all the flows here.
 | ||||
|             return self._flows; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     chooseFlow(flowIndex) { | ||||
|         this._currentFlowIndex = flowIndex; | ||||
|     } | ||||
| 
 | ||||
|     getCurrentFlowStep() { | ||||
|         // technically the flow can have multiple steps, but no one does this
 | ||||
|         // for login so we can ignore it.
 | ||||
|         var flowStep = this._flows[this._currentFlowIndex]; | ||||
|         return flowStep ? flowStep.type : null; | ||||
|     } | ||||
| 
 | ||||
|     loginAsGuest() { | ||||
|         var client = this._createTemporaryClient(); | ||||
|         return client.registerGuest({ | ||||
|             body: { | ||||
|                 initial_device_display_name: this._defaultDeviceDisplayName, | ||||
|             }, | ||||
|         }).then((creds) => { | ||||
|             return { | ||||
|                 userId: creds.user_id, | ||||
|                 deviceId: creds.device_id, | ||||
|                 accessToken: creds.access_token, | ||||
|                 homeserverUrl: this._hsUrl, | ||||
|                 identityServerUrl: this._isUrl, | ||||
|                 guest: true | ||||
|             }; | ||||
|         }, (error) => { | ||||
|             if (error.httpStatus === 403) { | ||||
|                 error.friendlyText = "Guest access is disabled on this Home Server."; | ||||
|             } else { | ||||
|                 error.friendlyText = "Failed to register as guest: " + error.data; | ||||
|             } | ||||
|             throw error; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     loginViaPassword(username, pass) { | ||||
|         var self = this; | ||||
|         var isEmail = username.indexOf("@") > 0; | ||||
|         var loginParams = { | ||||
|             password: pass, | ||||
|             initial_device_display_name: this._defaultDeviceDisplayName, | ||||
|         }; | ||||
|         if (isEmail) { | ||||
|             loginParams.medium = 'email'; | ||||
|             loginParams.address = username; | ||||
|         } else { | ||||
|             loginParams.user = username; | ||||
|         } | ||||
| 
 | ||||
|         var client = this._createTemporaryClient(); | ||||
|         return client.login('m.login.password', loginParams).then(function(data) { | ||||
|             return q({ | ||||
|                 homeserverUrl: self._hsUrl, | ||||
|                 identityServerUrl: self._isUrl, | ||||
|                 userId: data.user_id, | ||||
|                 deviceId: data.device_id, | ||||
|                 accessToken: data.access_token | ||||
|             }); | ||||
|         }, function(error) { | ||||
|             if (error.httpStatus == 400 && loginParams.medium) { | ||||
|                 error.friendlyText = ( | ||||
|                     'This Home Server does not support login using email address.' | ||||
|                 ); | ||||
|             } | ||||
|             else if (error.httpStatus === 403) { | ||||
|                 error.friendlyText = ( | ||||
|                     'Incorrect username and/or password.' | ||||
|                 ); | ||||
|                 if (self._fallbackHsUrl) { | ||||
|                     var fbClient = Matrix.createClient({ | ||||
|                         baseUrl: self._fallbackHsUrl, | ||||
|                         idBaseUrl: this._isUrl, | ||||
|                     }); | ||||
| 
 | ||||
|                     return fbClient.login('m.login.password', loginParams).then(function(data) { | ||||
|                         return q({ | ||||
|                             homeserverUrl: self._fallbackHsUrl, | ||||
|                             identityServerUrl: self._isUrl, | ||||
|                             userId: data.user_id, | ||||
|                             deviceId: data.device_id, | ||||
|                             accessToken: data.access_token | ||||
|                         }); | ||||
|                     }, function(fallback_error) { | ||||
|                         // throw the original error
 | ||||
|                         throw error; | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 error.friendlyText = ( | ||||
|                     'There was a problem logging in. (HTTP ' + error.httpStatus + ")" | ||||
|                 ); | ||||
|             } | ||||
|             throw error; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     redirectToCas() { | ||||
|       var client = this._createTemporaryClient(); | ||||
|       var parsedUrl = url.parse(window.location.href, true); | ||||
|       parsedUrl.query["homeserver"] = client.getHomeserverUrl(); | ||||
|       parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); | ||||
|       var casUrl = client.getCasLoginUrl(url.format(parsedUrl)); | ||||
|       window.location.href = casUrl; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										465
									
								
								src/Signup.js
								
								
								
								
							
							
						
						
									
										465
									
								
								src/Signup.js
								
								
								
								
							|  | @ -1,465 +0,0 @@ | |||
| "use strict"; | ||||
| 
 | ||||
| import Matrix from "matrix-js-sdk"; | ||||
| 
 | ||||
| var MatrixClientPeg = require("./MatrixClientPeg"); | ||||
| var SignupStages = require("./SignupStages"); | ||||
| var dis = require("./dispatcher"); | ||||
| var q = require("q"); | ||||
| var url = require("url"); | ||||
| 
 | ||||
| const EMAIL_STAGE_TYPE = "m.login.email.identity"; | ||||
| 
 | ||||
| /** | ||||
|  * A base class for common functionality between Registration and Login e.g. | ||||
|  * storage of HS/IS URLs. | ||||
|  */ | ||||
| class Signup { | ||||
|     constructor(hsUrl, isUrl, opts) { | ||||
|         this._hsUrl = hsUrl; | ||||
|         this._isUrl = isUrl; | ||||
|         this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName; | ||||
|     } | ||||
| 
 | ||||
|     getHomeserverUrl() { | ||||
|         return this._hsUrl; | ||||
|     } | ||||
| 
 | ||||
|     getIdentityServerUrl() { | ||||
|         return this._isUrl; | ||||
|     } | ||||
| 
 | ||||
|     setHomeserverUrl(hsUrl) { | ||||
|         this._hsUrl = hsUrl; | ||||
|     } | ||||
| 
 | ||||
|     setIdentityServerUrl(isUrl) { | ||||
|         this._isUrl = isUrl; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a temporary MatrixClient, which can be used for login or register | ||||
|      * requests. | ||||
|      */ | ||||
|     _createTemporaryClient() { | ||||
|         return Matrix.createClient({ | ||||
|             baseUrl: this._hsUrl, | ||||
|             idBaseUrl: this._isUrl, | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Registration logic class | ||||
|  * 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) { | ||||
|         super(hsUrl, isUrl, opts); | ||||
|         this.setStep("START"); | ||||
|         this.data = null; // from the server
 | ||||
|         // random other stuff (e.g. query params, NOT params from the server)
 | ||||
|         this.params = {}; | ||||
|         this.credentials = null; | ||||
|         this.activeStage = null; | ||||
|         this.registrationPromise = null; | ||||
|         // These values MUST be undefined else we'll send "username: null" which
 | ||||
|         // will error on Synapse rather than having the key absent.
 | ||||
|         this.username = undefined; // desired
 | ||||
|         this.email = undefined; // desired
 | ||||
|         this.password = undefined; // desired
 | ||||
|     } | ||||
| 
 | ||||
|     setClientSecret(secret) { | ||||
|         this.params.clientSecret = secret; | ||||
|     } | ||||
| 
 | ||||
|     setSessionId(sessionId) { | ||||
|         this.params.sessionId = sessionId; | ||||
|     } | ||||
| 
 | ||||
|     setRegistrationUrl(regUrl) { | ||||
|         this.params.registrationUrl = regUrl; | ||||
|     } | ||||
| 
 | ||||
|     setIdSid(idSid) { | ||||
|         this.params.idSid = idSid; | ||||
|     } | ||||
| 
 | ||||
|     setReferrer(referrer) { | ||||
|         this.params.referrer = referrer; | ||||
|     } | ||||
| 
 | ||||
|     setGuestAccessToken(token) { | ||||
|         this.guestAccessToken = token; | ||||
|     } | ||||
| 
 | ||||
|     getStep() { | ||||
|         return this._step; | ||||
|     } | ||||
| 
 | ||||
|     getCredentials() { | ||||
|         return this.credentials; | ||||
|     } | ||||
| 
 | ||||
|     getServerData() { | ||||
|         return this.data || {}; | ||||
|     } | ||||
| 
 | ||||
|     getPromise() { | ||||
|         return this.registrationPromise; | ||||
|     } | ||||
| 
 | ||||
|     setStep(step) { | ||||
|         this._step = 'Register.' + step; | ||||
|         // TODO:
 | ||||
|         // It's a shame this is going to the global dispatcher, we only really
 | ||||
|         // want things which have an instance of this class to be able to add
 | ||||
|         // listeners...
 | ||||
|         console.log("Dispatching 'registration_step_update' for step %s", this._step); | ||||
|         dis.dispatch({ | ||||
|             action: "registration_step_update" | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Starts the registration process from the first stage | ||||
|      */ | ||||
|     register(formVals) { | ||||
|         var {username, password, email} = formVals; | ||||
|         this.email = email; | ||||
|         this.username = username; | ||||
|         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); | ||||
|     } | ||||
| 
 | ||||
|     _tryRegister(client, authDict, poll_for_success) { | ||||
|         var self = this; | ||||
| 
 | ||||
|         var bindEmail; | ||||
| 
 | ||||
|         if (this.username && this.password) { | ||||
|             // only need to bind_email when sending u/p - sending it at other
 | ||||
|             // times clobbers the u/p resulting in M_MISSING_PARAM (password)
 | ||||
|             bindEmail = true; | ||||
|         } | ||||
| 
 | ||||
|         // TODO need to figure out how to send the device display name to /register.
 | ||||
|         return client.register( | ||||
|             this.username, this.password, this.params.sessionId, authDict, bindEmail, | ||||
|             this.guestAccessToken | ||||
|         ).then(function(result) { | ||||
|             self.credentials = result; | ||||
|             self.setStep("COMPLETE"); | ||||
|             return result; // contains the credentials
 | ||||
|         }, function(error) { | ||||
|             if (error.httpStatus === 401) { | ||||
|                 if (error.data && error.data.flows) { | ||||
|                     // Remember the session ID from the server:
 | ||||
|                     // Either this is our first 401 in which case we need to store the
 | ||||
|                     // session ID for future calls, or it isn't in which case this
 | ||||
|                     // is just a no-op since it ought to be the same (or if it isn't,
 | ||||
|                     // we should use the latest one from the server in any case).
 | ||||
|                     self.params.sessionId = error.data.session; | ||||
|                     self.data = error.data || {}; | ||||
|                     var flow = self.chooseFlow(error.data.flows); | ||||
| 
 | ||||
|                     if (flow) { | ||||
|                         console.log("Active flow => %s", JSON.stringify(flow)); | ||||
|                         var flowStage = self.firstUncompletedStage(flow); | ||||
|                         if (!self.activeStage || flowStage != self.activeStage.type) { | ||||
|                             return self._startStage(client, flowStage).catch(function(err) { | ||||
|                                 self.setStep('START'); | ||||
|                                 throw err; | ||||
|                             }); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 if (poll_for_success) { | ||||
|                     return q.delay(2000).then(function() { | ||||
|                         return self._tryRegister(client, authDict, poll_for_success); | ||||
|                     }); | ||||
|                 } else { | ||||
|                     throw new Error("Authorisation failed!"); | ||||
|                 } | ||||
|             } else { | ||||
|                 if (error.errcode === 'M_USER_IN_USE') { | ||||
|                     throw new Error("Username in use"); | ||||
|                 } else if (error.errcode == 'M_INVALID_USERNAME') { | ||||
|                     throw new Error("User names may only contain alphanumeric characters, underscores or dots!"); | ||||
|                 } else if (error.httpStatus >= 400 && error.httpStatus < 500) { | ||||
|                     let msg = null; | ||||
|                     if (error.message) { | ||||
|                         msg = error.message; | ||||
|                     } else if (error.errcode) { | ||||
|                         msg = error.errcode; | ||||
|                     } | ||||
|                     if (msg) { | ||||
|                         throw new Error(`Registration failed! (${error.httpStatus}) - ${msg}`); | ||||
|                     } else { | ||||
|                         throw new Error(`Registration failed! (${error.httpStatus}) - That's all we know.`); | ||||
|                     } | ||||
|                 } else if (error.httpStatus >= 500 && error.httpStatus < 600) { | ||||
|                     throw new Error( | ||||
|                         `Server error during registration! (${error.httpStatus})` | ||||
|                     ); | ||||
|                 } else if (error.name == "M_MISSING_PARAM") { | ||||
|                     // The HS hasn't remembered the login params from
 | ||||
|                     // the first try when the login email was sent.
 | ||||
|                     throw new Error( | ||||
|                         "This home server does not support resuming registration." | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     firstUncompletedStage(flow) { | ||||
|         for (var i = 0; i < flow.stages.length; ++i) { | ||||
|             if (!this.hasCompletedStage(flow.stages[i])) { | ||||
|                 return flow.stages[i]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     hasCompletedStage(stageType) { | ||||
|         var completed = (this.data || {}).completed || []; | ||||
|         return completed.indexOf(stageType) !== -1; | ||||
|     } | ||||
| 
 | ||||
|     _startStage(client, stageName) { | ||||
|         var self = this; | ||||
|         this.setStep(`STEP_${stageName}`); | ||||
|         var StageClass = SignupStages[stageName]; | ||||
|         if (!StageClass) { | ||||
|             // no idea how to handle this!
 | ||||
|             throw new Error("Unknown stage: " + stageName); | ||||
|         } | ||||
| 
 | ||||
|         var stage = new StageClass(client, this); | ||||
|         this.activeStage = stage; | ||||
|         return stage.complete().then(function(request) { | ||||
|             if (request.auth) { | ||||
|                 console.log("Stage %s is returning an auth dict", stageName); | ||||
|                 return self._tryRegister(client, request.auth, request.poll_for_success); | ||||
|             } | ||||
|             else { | ||||
|                 // never resolve the promise chain. This is for things like email auth
 | ||||
|                 // which display a "check your email" message and relies on the
 | ||||
|                 // link in the email to actually register you.
 | ||||
|                 console.log("Waiting for external action."); | ||||
|                 return q.defer().promise; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     chooseFlow(flows) { | ||||
|         // If the user gave us an email then we want to pick an email
 | ||||
|         // flow we can do, else any other flow.
 | ||||
|         var emailFlow = null; | ||||
|         var otherFlow = null; | ||||
|         flows.forEach(function(flow) { | ||||
|             var flowHasEmail = false; | ||||
|             for (var stageI = 0; stageI < flow.stages.length; ++stageI) { | ||||
|                 var stage = flow.stages[stageI]; | ||||
| 
 | ||||
|                 if (!SignupStages[stage]) { | ||||
|                     // we can't do this flow, don't have a Stage impl.
 | ||||
|                     return; | ||||
|                 } | ||||
| 
 | ||||
|                 if (stage === EMAIL_STAGE_TYPE) { | ||||
|                     flowHasEmail = true; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (flowHasEmail) { | ||||
|                 emailFlow = flow; | ||||
|             } else { | ||||
|                 otherFlow = flow; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         if (this.email || this.hasCompletedStage(EMAIL_STAGE_TYPE)) { | ||||
|             // we've been given an email or we've already done an email part
 | ||||
|             return emailFlow; | ||||
|         } else { | ||||
|             return otherFlow; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     recheckState() { | ||||
|         // We've been given a bunch of data from a previous register step,
 | ||||
|         // this only happens for email auth currently. It's kinda ming we need
 | ||||
|         // to know this though. A better solution would be to ask the stages if
 | ||||
|         // they are ready to do something rather than accepting that we know about
 | ||||
|         // email auth and its internals.
 | ||||
|         this.params.hasEmailInfo = ( | ||||
|             this.params.clientSecret && this.params.sessionId && this.params.idSid | ||||
|         ); | ||||
| 
 | ||||
|         if (this.params.hasEmailInfo) { | ||||
|             const client = this._createTemporaryClient(); | ||||
|             this.registrationPromise = this._startStage(client, EMAIL_STAGE_TYPE); | ||||
|         } | ||||
|         return this.registrationPromise; | ||||
|     } | ||||
| 
 | ||||
|     tellStage(stageName, data) { | ||||
|         if (this.activeStage && this.activeStage.type === stageName) { | ||||
|             console.log("Telling stage %s about something..", stageName); | ||||
|             this.activeStage.onReceiveData(data); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| class Login extends Signup { | ||||
|     constructor(hsUrl, isUrl, fallbackHsUrl, opts) { | ||||
|         super(hsUrl, isUrl, opts); | ||||
|         this._fallbackHsUrl = fallbackHsUrl; | ||||
|         this._currentFlowIndex = 0; | ||||
|         this._flows = []; | ||||
|     } | ||||
| 
 | ||||
|     getFlows() { | ||||
|         var self = this; | ||||
|         var client = this._createTemporaryClient(); | ||||
|         return client.loginFlows().then(function(result) { | ||||
|             self._flows = result.flows; | ||||
|             self._currentFlowIndex = 0; | ||||
|             // technically the UI should display options for all flows for the
 | ||||
|             // user to then choose one, so return all the flows here.
 | ||||
|             return self._flows; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     chooseFlow(flowIndex) { | ||||
|         this._currentFlowIndex = flowIndex; | ||||
|     } | ||||
| 
 | ||||
|     getCurrentFlowStep() { | ||||
|         // technically the flow can have multiple steps, but no one does this
 | ||||
|         // for login so we can ignore it.
 | ||||
|         var flowStep = this._flows[this._currentFlowIndex]; | ||||
|         return flowStep ? flowStep.type : null; | ||||
|     } | ||||
| 
 | ||||
|     loginAsGuest() { | ||||
|         var client = this._createTemporaryClient(); | ||||
|         return client.registerGuest({ | ||||
|             body: { | ||||
|                 initial_device_display_name: this._defaultDeviceDisplayName, | ||||
|             }, | ||||
|         }).then((creds) => { | ||||
|             return { | ||||
|                 userId: creds.user_id, | ||||
|                 deviceId: creds.device_id, | ||||
|                 accessToken: creds.access_token, | ||||
|                 homeserverUrl: this._hsUrl, | ||||
|                 identityServerUrl: this._isUrl, | ||||
|                 guest: true | ||||
|             }; | ||||
|         }, (error) => { | ||||
|             if (error.httpStatus === 403) { | ||||
|                 error.friendlyText = "Guest access is disabled on this Home Server."; | ||||
|             } else { | ||||
|                 error.friendlyText = "Failed to register as guest: " + error.data; | ||||
|             } | ||||
|             throw error; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     loginViaPassword(username, pass) { | ||||
|         var self = this; | ||||
|         var isEmail = username.indexOf("@") > 0; | ||||
|         var loginParams = { | ||||
|             password: pass, | ||||
|             initial_device_display_name: this._defaultDeviceDisplayName, | ||||
|         }; | ||||
|         if (isEmail) { | ||||
|             loginParams.medium = 'email'; | ||||
|             loginParams.address = username; | ||||
|         } else { | ||||
|             loginParams.user = username; | ||||
|         } | ||||
| 
 | ||||
|         var client = this._createTemporaryClient(); | ||||
|         return client.login('m.login.password', loginParams).then(function(data) { | ||||
|             return q({ | ||||
|                 homeserverUrl: self._hsUrl, | ||||
|                 identityServerUrl: self._isUrl, | ||||
|                 userId: data.user_id, | ||||
|                 deviceId: data.device_id, | ||||
|                 accessToken: data.access_token | ||||
|             }); | ||||
|         }, function(error) { | ||||
|             if (error.httpStatus == 400 && loginParams.medium) { | ||||
|                 error.friendlyText = ( | ||||
|                     'This Home Server does not support login using email address.' | ||||
|                 ); | ||||
|             } | ||||
|             else if (error.httpStatus === 403) { | ||||
|                 error.friendlyText = ( | ||||
|                     'Incorrect username and/or password.' | ||||
|                 ); | ||||
|                 if (self._fallbackHsUrl) { | ||||
|                     var fbClient = Matrix.createClient({ | ||||
|                         baseUrl: self._fallbackHsUrl, | ||||
|                         idBaseUrl: this._isUrl, | ||||
|                     }); | ||||
| 
 | ||||
|                     return fbClient.login('m.login.password', loginParams).then(function(data) { | ||||
|                         return q({ | ||||
|                             homeserverUrl: self._fallbackHsUrl, | ||||
|                             identityServerUrl: self._isUrl, | ||||
|                             userId: data.user_id, | ||||
|                             deviceId: data.device_id, | ||||
|                             accessToken: data.access_token | ||||
|                         }); | ||||
|                     }, function(fallback_error) { | ||||
|                         // throw the original error
 | ||||
|                         throw error; | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 error.friendlyText = ( | ||||
|                     'There was a problem logging in. (HTTP ' + error.httpStatus + ")" | ||||
|                 ); | ||||
|             } | ||||
|             throw error; | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     redirectToCas() { | ||||
|       var client = this._createTemporaryClient(); | ||||
|       var parsedUrl = url.parse(window.location.href, true); | ||||
|       parsedUrl.query["homeserver"] = client.getHomeserverUrl(); | ||||
|       parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); | ||||
|       var casUrl = client.getCasLoginUrl(url.format(parsedUrl)); | ||||
|       window.location.href = casUrl; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| module.exports.Register = Register; | ||||
| module.exports.Login = Login; | ||||
|  | @ -1,177 +0,0 @@ | |||
| "use strict"; | ||||
| var q = require("q"); | ||||
| 
 | ||||
| /** | ||||
|  * An interface class which login types should abide by. | ||||
|  */ | ||||
| class Stage { | ||||
|     constructor(type, matrixClient, signupInstance) { | ||||
|         this.type = type; | ||||
|         this.client = matrixClient; | ||||
|         this.signupInstance = signupInstance; | ||||
|     } | ||||
| 
 | ||||
|     complete() { | ||||
|         // Return a promise which is:
 | ||||
|         // RESOLVED => With an Object which has an 'auth' key which is the auth dict
 | ||||
|         //             to submit.
 | ||||
|         // REJECTED => With an Error if there was a problem with this stage.
 | ||||
|         //             Has a "message" string and an "isFatal" flag.
 | ||||
|         return q.reject("NOT IMPLEMENTED"); | ||||
|     } | ||||
| 
 | ||||
|     onReceiveData() { | ||||
|         // NOP
 | ||||
|     } | ||||
| } | ||||
| Stage.TYPE = "NOT IMPLEMENTED"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * This stage requires no auth. | ||||
|  */ | ||||
| class DummyStage extends Stage { | ||||
|     constructor(matrixClient, signupInstance) { | ||||
|         super(DummyStage.TYPE, matrixClient, signupInstance); | ||||
|     } | ||||
| 
 | ||||
|     complete() { | ||||
|         return q({ | ||||
|             auth: { | ||||
|                 type: DummyStage.TYPE | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| DummyStage.TYPE = "m.login.dummy"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * This stage uses Google's Recaptcha to do auth. | ||||
|  */ | ||||
| class RecaptchaStage extends Stage { | ||||
|     constructor(matrixClient, signupInstance) { | ||||
|         super(RecaptchaStage.TYPE, matrixClient, signupInstance); | ||||
|         this.authDict = { | ||||
|             auth: { | ||||
|                 type: 'm.login.recaptcha', | ||||
|                 // we'll add in the response param if we get one from the local user.
 | ||||
|             }, | ||||
|             poll_for_success: true, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     // called when the recaptcha has been completed.
 | ||||
|     onReceiveData(data) { | ||||
|         if (!data || !data.response) { | ||||
|             return; | ||||
|         } | ||||
|         this.authDict.auth.response = data.response; | ||||
|     } | ||||
| 
 | ||||
|     complete() { | ||||
|         // we return the authDict with no response, telling Signup to keep polling
 | ||||
|         // the server in case the captcha is filled in on another window (e.g. by
 | ||||
|         // following a nextlink from an email signup).  If the user completes the
 | ||||
|         // captcha locally, then we return at the next poll.
 | ||||
|         return q(this.authDict); | ||||
|     } | ||||
| } | ||||
| RecaptchaStage.TYPE = "m.login.recaptcha"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * This state uses the IS to verify email addresses. | ||||
|  */ | ||||
| class EmailIdentityStage extends Stage { | ||||
|     constructor(matrixClient, signupInstance) { | ||||
|         super(EmailIdentityStage.TYPE, matrixClient, signupInstance); | ||||
|     } | ||||
| 
 | ||||
|     _completeVerify() { | ||||
|         // pull out the host of the IS URL by creating an anchor element
 | ||||
|         var isLocation = document.createElement('a'); | ||||
|         isLocation.href = this.signupInstance.getIdentityServerUrl(); | ||||
| 
 | ||||
|         var clientSecret = this.clientSecret || this.signupInstance.params.clientSecret; | ||||
|         var sid = this.sid || this.signupInstance.params.idSid; | ||||
| 
 | ||||
|         return q({ | ||||
|             auth: { | ||||
|                 type: 'm.login.email.identity', | ||||
|                 threepid_creds: { | ||||
|                     sid: sid, | ||||
|                     client_secret: clientSecret, | ||||
|                     id_server: isLocation.host | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Complete the email stage. | ||||
|      * | ||||
|      * This is called twice under different circumstances: | ||||
|      *   1) When requesting an email token from the IS | ||||
|      *   2) When validating query parameters received from the link in the email | ||||
|      */ | ||||
|     complete() { | ||||
|         // TODO: The Registration class shouldn't really know this info.
 | ||||
|         if (this.signupInstance.params.hasEmailInfo) { | ||||
|             return this._completeVerify(); | ||||
|         } | ||||
| 
 | ||||
|         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) + | ||||
|                        "&hs_url=" + | ||||
|                        encodeURIComponent(this.signupInstance.getHomeserverUrl()) + | ||||
|                        "&is_url=" + | ||||
|                        encodeURIComponent(this.signupInstance.getIdentityServerUrl()) + | ||||
|                        "&session_id=" + | ||||
|                        encodeURIComponent(this.signupInstance.getServerData().session); | ||||
| 
 | ||||
|         // Add the user ID of the referring user, if set
 | ||||
|         if (this.signupInstance.params.referrer) { | ||||
|             nextLink += "&referrer=" + encodeURIComponent(this.signupInstance.params.referrer); | ||||
|         } | ||||
| 
 | ||||
|         var self = this; | ||||
|         return this.client.requestRegisterEmailToken( | ||||
|             this.signupInstance.email, | ||||
|             this.clientSecret, | ||||
|             1, // TODO: Multiple send attempts?
 | ||||
|             nextLink | ||||
|         ).then(function(response) { | ||||
|             self.sid = response.sid; | ||||
|             self.signupInstance.setIdSid(self.sid); | ||||
|             return self._completeVerify(); | ||||
|         }).then(function(request) { | ||||
|             request.poll_for_success = true; | ||||
|             return request; | ||||
|         }, function(error) { | ||||
|             console.error(error); | ||||
|             var e = { | ||||
|                 isFatal: true | ||||
|             }; | ||||
|             if (error.errcode == 'M_THREEPID_IN_USE') { | ||||
|                 e.message = "This email address is already registered"; | ||||
|             } else { | ||||
|                 e.message = 'Unable to contact the given identity server'; | ||||
|             } | ||||
|             throw e; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| EmailIdentityStage.TYPE = "m.login.email.identity"; | ||||
| 
 | ||||
| module.exports = { | ||||
|     [DummyStage.TYPE]: DummyStage, | ||||
|     [RecaptchaStage.TYPE]: RecaptchaStage, | ||||
|     [EmailIdentityStage.TYPE]: EmailIdentityStage | ||||
| }; | ||||
|  | @ -27,6 +27,9 @@ export default React.createClass({ | |||
|     displayName: 'InteractiveAuth', | ||||
| 
 | ||||
|     propTypes: { | ||||
|         // matrix client to use for UI auth requests
 | ||||
|         matrixClient: React.PropTypes.object.isRequired, | ||||
| 
 | ||||
|         // response from initial request. If not supplied, will do a request on
 | ||||
|         // mount.
 | ||||
|         authData: React.PropTypes.shape({ | ||||
|  | @ -38,11 +41,27 @@ export default React.createClass({ | |||
|         // callback
 | ||||
|         makeRequest: React.PropTypes.func.isRequired, | ||||
| 
 | ||||
|         // callback called when the auth process has finished
 | ||||
|         // callback called when the auth process has finished,
 | ||||
|         // successfully or unsuccessfully.
 | ||||
|         // @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, | ||||
|         onAuthFinished: React.PropTypes.func.isRequired, | ||||
| 
 | ||||
|         // Inputs provided by the user to the auth process
 | ||||
|         // and used by various stages. As passed to js-sdk
 | ||||
|         // interactive-auth
 | ||||
|         inputs: React.PropTypes.object, | ||||
| 
 | ||||
|         // As js-sdk interactive-auth
 | ||||
|         makeRegistrationUrl: React.PropTypes.func, | ||||
|         sessionId: React.PropTypes.string, | ||||
|         clientSecret: React.PropTypes.string, | ||||
|         emailSid: React.PropTypes.string, | ||||
| 
 | ||||
|         // If true, poll to see if the auth flow has been completed
 | ||||
|         // out-of-band
 | ||||
|         poll: React.PropTypes.bool, | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|  | @ -60,12 +79,18 @@ export default React.createClass({ | |||
|         this._authLogic = new InteractiveAuth({ | ||||
|             authData: this.props.authData, | ||||
|             doRequest: this._requestCallback, | ||||
|             startAuthStage: this._startAuthStage, | ||||
|             inputs: this.props.inputs, | ||||
|             stateUpdated: this._authStateUpdated, | ||||
|             matrixClient: this.props.matrixClient, | ||||
|             sessionId: this.props.sessionId, | ||||
|             clientSecret: this.props.clientSecret, | ||||
|             emailSid: this.props.emailSid, | ||||
|         }); | ||||
| 
 | ||||
|         this._authLogic.attemptAuth().then((result) => { | ||||
|             this.props.onFinished(true, result); | ||||
|             this.props.onAuthFinished(true, result); | ||||
|         }).catch((error) => { | ||||
|             this.props.onAuthFinished(false, error); | ||||
|             console.error("Error during user-interactive auth:", error); | ||||
|             if (this._unmounted) { | ||||
|                 return; | ||||
|  | @ -76,17 +101,32 @@ export default React.createClass({ | |||
|                 errorText: msg | ||||
|             }); | ||||
|         }).done(); | ||||
| 
 | ||||
|         this._intervalId = null; | ||||
|         if (this.props.poll) { | ||||
|             this._intervalId = setInterval(() => { | ||||
|                 this._authLogic.poll(); | ||||
|             }, 2000); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     componentWillUnmount: function() { | ||||
|         this._unmounted = true; | ||||
| 
 | ||||
|         if (this._intervalId !== null) { | ||||
|             clearInterval(this._intervalId); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _startAuthStage: function(stageType, error) { | ||||
|     _authStateUpdated: function(stageType, stageState) { | ||||
|         const oldStage = this.state.authStage; | ||||
|         this.setState({ | ||||
|             authStage: stageType, | ||||
|             errorText: error ? error.error : null, | ||||
|         }, this._setFocus); | ||||
|             stageState: stageState, | ||||
|             errorText: stageState.error, | ||||
|         }, () => { | ||||
|             if (oldStage != stageType) this._setFocus(); | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     _requestCallback: function(auth) { | ||||
|  | @ -117,19 +157,35 @@ export default React.createClass({ | |||
| 
 | ||||
|     _renderCurrentStage: function() { | ||||
|         const stage = this.state.authStage; | ||||
|         var StageComponent = getEntryComponentForLoginType(stage); | ||||
|         if (!stage) return null; | ||||
| 
 | ||||
|         const StageComponent = getEntryComponentForLoginType(stage); | ||||
|         return ( | ||||
|             <StageComponent ref="stageComponent" | ||||
|                 loginType={stage} | ||||
|                 matrixClient={this.props.matrixClient} | ||||
|                 authSessionId={this._authLogic.getSessionId()} | ||||
|                 clientSecret={this._authLogic.getClientSecret()} | ||||
|                 stageParams={this._authLogic.getStageParams(stage)} | ||||
|                 submitAuthDict={this._submitAuthDict} | ||||
|                 errorText={this.state.stageErrorText} | ||||
|                 busy={this.state.busy} | ||||
|                 inputs={this.props.inputs} | ||||
|                 stageState={this.state.stageState} | ||||
|                 fail={this._onAuthStageFailed} | ||||
|                 setEmailSid={this._setEmailSid} | ||||
|                 makeRegistrationUrl={this.props.makeRegistrationUrl} | ||||
|             /> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     _onAuthStageFailed: function(e) { | ||||
|         this.props.onAuthFinished(false, e); | ||||
|     }, | ||||
|     _setEmailSid: function(sid) { | ||||
|         this._authLogic.setEmailSid(sid); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         let error = null; | ||||
|         if (this.state.errorText) { | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| /* | ||||
| Copyright 2015, 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. | ||||
|  | @ -65,6 +66,9 @@ module.exports = React.createClass({ | |||
|         // displayname, if any, to set on the device when logging
 | ||||
|         // in/registering.
 | ||||
|         defaultDeviceDisplayName: React.PropTypes.string, | ||||
| 
 | ||||
|         // A function that makes a registration URL
 | ||||
|         makeRegistrationUrl: React.PropTypes.func.isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     childContextTypes: { | ||||
|  | @ -324,23 +328,19 @@ module.exports = React.createClass({ | |||
|                 Lifecycle.logout(); | ||||
|                 break; | ||||
|             case 'start_registration': | ||||
|                 var newState = payload.params || {}; | ||||
|                 newState.screen = 'register'; | ||||
|                 if ( | ||||
|                     payload.params && | ||||
|                     payload.params.client_secret && | ||||
|                     payload.params.session_id && | ||||
|                     payload.params.hs_url && | ||||
|                     payload.params.is_url && | ||||
|                     payload.params.sid | ||||
|                 ) { | ||||
|                     newState.register_client_secret = payload.params.client_secret; | ||||
|                     newState.register_session_id = payload.params.session_id; | ||||
|                     newState.register_hs_url = payload.params.hs_url; | ||||
|                     newState.register_is_url = payload.params.is_url; | ||||
|                     newState.register_id_sid = payload.params.sid; | ||||
|                 } | ||||
|                 this.setStateForNewScreen(newState); | ||||
|                 const params = payload.params || {}; | ||||
|                 this.setStateForNewScreen({ | ||||
|                     screen: 'register', | ||||
|                     // these params may be undefined, but if they are,
 | ||||
|                     // unset them from our state: we don't want to
 | ||||
|                     // resume a previous registration session if the
 | ||||
|                     // user just clicked 'register'
 | ||||
|                     register_client_secret: params.client_secret, | ||||
|                     register_session_id: params.session_id, | ||||
|                     register_hs_url: params.hs_url, | ||||
|                     register_is_url: params.is_url, | ||||
|                     register_id_sid: params.sid, | ||||
|                 }); | ||||
|                 this.notifyNewScreen('register'); | ||||
|                 break; | ||||
|             case 'start_login': | ||||
|  | @ -356,13 +356,22 @@ module.exports = React.createClass({ | |||
|                 }); | ||||
|                 break; | ||||
|             case 'start_upgrade_registration': | ||||
|                 // stash our guest creds so we can backout if needed
 | ||||
|                 // also stash our credentials, then if we restore the session,
 | ||||
|                 // we can just do it the same way whether we started upgrade
 | ||||
|                 // registration or explicitly logged out
 | ||||
|                 this.guestCreds = MatrixClientPeg.getCredentials(); | ||||
|                 this.setStateForNewScreen({ | ||||
|                     screen: "register", | ||||
|                     upgradeUsername: MatrixClientPeg.get().getUserIdLocalpart(), | ||||
|                     guestAccessToken: MatrixClientPeg.get().getAccessToken(), | ||||
|                 }); | ||||
| 
 | ||||
|                 // stop the client: if we are syncing whilst the registration
 | ||||
|                 // is completed in another browser, we'll be 401ed for using
 | ||||
|                 // a guest access token for a non-guest account.
 | ||||
|                 // It will be restarted in onReturnToGuestClick
 | ||||
|                 Lifecycle.stopMatrixClient(); | ||||
| 
 | ||||
|                 this.notifyNewScreen('register'); | ||||
|                 break; | ||||
|             case 'start_password_recovery': | ||||
|  | @ -1069,6 +1078,13 @@ module.exports = React.createClass({ | |||
|         this.setState({currentRoomId: room_id}); | ||||
|     }, | ||||
| 
 | ||||
|     _makeRegistrationUrl: function(params) { | ||||
|         if (this.props.startingFragmentQueryParams.referrer) { | ||||
|             params.referrer = this.props.startingFragmentQueryParams.referrer; | ||||
|         } | ||||
|         return this.props.makeRegistrationUrl(params); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); | ||||
|         var LoggedInView = sdk.getComponent('structures.LoggedInView'); | ||||
|  | @ -1132,7 +1148,7 @@ module.exports = React.createClass({ | |||
|                     teamServerConfig={this.props.config.teamServerConfig} | ||||
|                     customHsUrl={this.getCurrentHsUrl()} | ||||
|                     customIsUrl={this.getCurrentIsUrl()} | ||||
|                     registrationUrl={this.props.registrationUrl} | ||||
|                     makeRegistrationUrl={this._makeRegistrationUrl} | ||||
|                     defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} | ||||
|                     onLoggedIn={this.onRegistered} | ||||
|                     onLoginClick={this.onLoginClick} | ||||
|  |  | |||
|  | @ -19,13 +19,13 @@ limitations under the License. | |||
| var React = require('react'); | ||||
| var ReactDOM = require('react-dom'); | ||||
| var sdk = require('../../../index'); | ||||
| var Signup = require("../../../Signup"); | ||||
| var Login = require("../../../Login"); | ||||
| var PasswordLogin = require("../../views/login/PasswordLogin"); | ||||
| var CasLogin = require("../../views/login/CasLogin"); | ||||
| var ServerConfig = require("../../views/login/ServerConfig"); | ||||
| 
 | ||||
| /** | ||||
|  * A wire component which glues together login UI components and Signup logic | ||||
|  * A wire component which glues together login UI components and Login logic | ||||
|  */ | ||||
| module.exports = React.createClass({ | ||||
|     displayName: 'Login', | ||||
|  | @ -146,7 +146,7 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|         var fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null; | ||||
| 
 | ||||
|         var loginLogic = new Signup.Login(hsUrl, isUrl, fallbackHsUrl, { | ||||
|         var loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { | ||||
|             defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, | ||||
|         }); | ||||
|         this._loginLogic = loginLogic; | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| /* | ||||
| Copyright 2015, 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. | ||||
|  | @ -14,10 +15,9 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| import Matrix from 'matrix-js-sdk'; | ||||
| 
 | ||||
| import q from 'q'; | ||||
| 
 | ||||
| import React from 'react'; | ||||
| 
 | ||||
| import sdk from '../../../index'; | ||||
|  | @ -31,10 +31,6 @@ import RtsClient from '../../../RtsClient'; | |||
| 
 | ||||
| const 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', | ||||
| 
 | ||||
|  | @ -42,7 +38,7 @@ module.exports = React.createClass({ | |||
|         onLoggedIn: React.PropTypes.func.isRequired, | ||||
|         clientSecret: React.PropTypes.string, | ||||
|         sessionId: React.PropTypes.string, | ||||
|         registrationUrl: React.PropTypes.string, | ||||
|         makeRegistrationUrl: React.PropTypes.func.isRequired, | ||||
|         idSid: React.PropTypes.string, | ||||
|         customHsUrl: React.PropTypes.string, | ||||
|         customIsUrl: React.PropTypes.string, | ||||
|  | @ -83,27 +79,20 @@ module.exports = React.createClass({ | |||
|             formVals: { | ||||
|                 email: this.props.email, | ||||
|             }, | ||||
|             // true if we're waiting for the user to complete
 | ||||
|             // user-interactive auth
 | ||||
|             // If we've been given a session ID, we're resuming
 | ||||
|             // straight back into UI auth
 | ||||
|             doingUIAuth: Boolean(this.props.sessionId), | ||||
|             hsUrl: this.props.customHsUrl, | ||||
|             isUrl: this.props.customIsUrl, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentWillMount: function() { | ||||
|         this._unmounted = false; | ||||
|         this.dispatcherRef = dis.register(this.onAction); | ||||
|         // attach this to the instance rather than this.state since it isn't UI
 | ||||
|         this.registerLogic = new Signup.Register( | ||||
|             this.props.customHsUrl, this.props.customIsUrl, { | ||||
|                 defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, | ||||
|             } | ||||
|         ); | ||||
|         this.registerLogic.setClientSecret(this.props.clientSecret); | ||||
|         this.registerLogic.setSessionId(this.props.sessionId); | ||||
|         this.registerLogic.setRegistrationUrl(this.props.registrationUrl); | ||||
|         this.registerLogic.setIdSid(this.props.idSid); | ||||
|         this.registerLogic.setGuestAccessToken(this.props.guestAccessToken); | ||||
|         if (this.props.referrer) { | ||||
|             this.registerLogic.setReferrer(this.props.referrer); | ||||
|         } | ||||
|         this.registerLogic.recheckState(); | ||||
| 
 | ||||
|         this._replaceClient(); | ||||
| 
 | ||||
|         if ( | ||||
|             this.props.teamServerConfig && | ||||
|  | @ -135,154 +124,124 @@ module.exports = React.createClass({ | |||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     componentWillUnmount: function() { | ||||
|         dis.unregister(this.dispatcherRef); | ||||
|         this._unmounted = true; | ||||
|     }, | ||||
| 
 | ||||
|     componentDidMount: function() { | ||||
|         // may have already done an HTTP hit (e.g. redirect from an email) so
 | ||||
|         // check for any pending response
 | ||||
|         var promise = this.registerLogic.getPromise(); | ||||
|         if (promise) { | ||||
|             this.onProcessingRegistration(promise); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     onHsUrlChanged: function(newHsUrl) { | ||||
|         this.registerLogic.setHomeserverUrl(newHsUrl); | ||||
|         this.setState({ | ||||
|             hsUrl: newHsUrl, | ||||
|         }); | ||||
|         this._replaceClient(); | ||||
|     }, | ||||
| 
 | ||||
|     onIsUrlChanged: function(newIsUrl) { | ||||
|         this.registerLogic.setIdentityServerUrl(newIsUrl); | ||||
|         this.setState({ | ||||
|             isUrl: newIsUrl, | ||||
|         }); | ||||
|         this._replaceClient(); | ||||
|     }, | ||||
| 
 | ||||
|     onAction: function(payload) { | ||||
|         if (payload.action !== "registration_step_update") { | ||||
|             return; | ||||
|         } | ||||
|         // If the registration state has changed, this means the
 | ||||
|         // user now needs to do something. It would be better
 | ||||
|         // to expose the explicitly in the register logic.
 | ||||
|         this.setState({ | ||||
|             busy: false | ||||
|     _replaceClient: function() { | ||||
|         this._matrixClient = Matrix.createClient({ | ||||
|             baseUrl: this.state.hsUrl, | ||||
|             idBaseUrl: this.state.isUrl, | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onFormSubmit: function(formVals) { | ||||
|         var self = this; | ||||
|         this.setState({ | ||||
|             errorText: "", | ||||
|             busy: true, | ||||
|             formVals: formVals, | ||||
|             doingUIAuth: true, | ||||
|         }); | ||||
| 
 | ||||
|         if (formVals.username !== this.props.username) { | ||||
|             // don't try to upgrade if we changed our username
 | ||||
|             this.registerLogic.setGuestAccessToken(null); | ||||
|         } | ||||
| 
 | ||||
|         this.onProcessingRegistration(this.registerLogic.register(formVals)); | ||||
|     }, | ||||
| 
 | ||||
|     // Promise is resolved when the registration process is FULLY COMPLETE
 | ||||
|     onProcessingRegistration: function(promise) { | ||||
|         var self = this; | ||||
|         promise.done(function(response) { | ||||
|             self.setState({ | ||||
|                 busy: false | ||||
|     _onUIAuthFinished: function(success, response) { | ||||
|         if (!success) { | ||||
|             this.setState({ | ||||
|                 busy: false, | ||||
|                 doingUIAuth: false, | ||||
|                 errorText: response.message || response.toString(), | ||||
|             }); | ||||
|             if (!response || !response.access_token) { | ||||
|                 console.warn( | ||||
|                     "FIXME: Register fulfilled without a final response, " + | ||||
|                     "did you break the promise chain?" | ||||
|                 ); | ||||
|                 // no matter, we'll grab it direct
 | ||||
|                 response = self.registerLogic.getCredentials(); | ||||
|             } | ||||
|             if (!response || !response.user_id || !response.access_token) { | ||||
|                 console.error("Final response is missing keys."); | ||||
|                 self.setState({ | ||||
|                     errorText: "Registration failed on server" | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|             // Done regardless of `teamSelected`. People registering with non-team emails
 | ||||
|             // will just nop. The point of this being we might not have the email address
 | ||||
|             // that the user registered with at this stage (depending on whether this
 | ||||
|             // is the client they initiated registration).
 | ||||
|             let trackPromise = q(null); | ||||
|             if (self._rtsClient) { | ||||
|                 // Track referral if self.props.referrer set, get team_token in order to
 | ||||
|                 // retrieve team config and see welcome page etc.
 | ||||
|                 trackPromise = self._rtsClient.trackReferral( | ||||
|                     self.props.referrer || '', // Default to empty string = not referred
 | ||||
|                     self.registerLogic.params.idSid, | ||||
|                     self.registerLogic.params.clientSecret | ||||
|                 ).then((data) => { | ||||
|                     const teamToken = data.team_token; | ||||
|                     // Store for use /w welcome pages
 | ||||
|                     window.localStorage.setItem('mx_team_token', teamToken); | ||||
| 
 | ||||
|                     self._rtsClient.getTeam(teamToken).then((team) => { | ||||
|                         console.log( | ||||
|                             `User successfully registered with team ${team.name}` | ||||
|                         ); | ||||
|                         if (!team.rooms) { | ||||
|                             return; | ||||
|                         } | ||||
|                         // Auto-join rooms
 | ||||
|                         team.rooms.forEach((room) => { | ||||
|                             if (room.auto_join && room.room_id) { | ||||
|                                 console.log(`Auto-joining ${room.room_id}`); | ||||
|                                 MatrixClientPeg.get().joinRoom(room.room_id); | ||||
|                             } | ||||
|                         }); | ||||
|                     }, (err) => { | ||||
|                         console.error('Error getting team config', err); | ||||
|                     }); | ||||
| 
 | ||||
|                     return teamToken; | ||||
|                 }, (err) => { | ||||
|                     console.error('Error tracking referral', err); | ||||
|                 }); | ||||
|             } | ||||
| 
 | ||||
|             return trackPromise.then((teamToken) => { | ||||
|                 console.info('Team token promise',teamToken); | ||||
|                 self.props.onLoggedIn({ | ||||
|                     userId: response.user_id, | ||||
|                     deviceId: response.device_id, | ||||
|                     homeserverUrl: self.registerLogic.getHomeserverUrl(), | ||||
|                     identityServerUrl: self.registerLogic.getIdentityServerUrl(), | ||||
|                     accessToken: response.access_token | ||||
|                 }, teamToken); | ||||
|             }).then(() => { | ||||
|                 self._setupPushers(); | ||||
|             }); | ||||
|         }, function(err) { | ||||
|             if (err.message) { | ||||
|                 self.setState({ | ||||
|                     errorText: err.message | ||||
|                 }); | ||||
|             } | ||||
|             self.setState({ | ||||
|                 busy: false | ||||
|             }); | ||||
|             console.log(err); | ||||
|         this.setState({ | ||||
|             // we're still busy until we get unmounted: don't show the registration form again
 | ||||
|             busy: true, | ||||
|             doingUIAuth: false, | ||||
|         }); | ||||
|         this.props.onLoggedIn({ | ||||
|             userId: response.user_id, | ||||
|             deviceId: response.device_id, | ||||
|             homeserverUrl: this.state.hsUrl, | ||||
|             identityServerUrl: this.state.isUrl, | ||||
|             accessToken: response.access_token, | ||||
|         }); | ||||
| 
 | ||||
|         // Done regardless of `teamSelected`. People registering with non-team emails
 | ||||
|         // will just nop. The point of this being we might not have the email address
 | ||||
|         // that the user registered with at this stage (depending on whether this
 | ||||
|         // is the client they initiated registration).
 | ||||
|         let trackPromise = q(null); | ||||
|         if (this._rtsClient) { | ||||
|             // Track referral if this.props.referrer set, get team_token in order to
 | ||||
|             // retrieve team config and see welcome page etc.
 | ||||
|             trackPromise = this._rtsClient.trackReferral( | ||||
|                 this.props.referrer || '', // Default to empty string = not referred
 | ||||
|                 this.registerLogic.params.idSid, | ||||
|                 this.registerLogic.params.clientSecret | ||||
|             ).then((data) => { | ||||
|                 const teamToken = data.team_token; | ||||
|                 // Store for use /w welcome pages
 | ||||
|                 window.localStorage.setItem('mx_team_token', teamToken); | ||||
|                 this.props.onTeamMemberRegistered(teamToken); | ||||
| 
 | ||||
|                 this._rtsClient.getTeam(teamToken).then((team) => { | ||||
|                     console.log( | ||||
|                         `User successfully registered with team ${team.name}` | ||||
|                     ); | ||||
|                     if (!team.rooms) { | ||||
|                         return; | ||||
|                     } | ||||
|                     // Auto-join rooms
 | ||||
|                     team.rooms.forEach((room) => { | ||||
|                         if (room.auto_join && room.room_id) { | ||||
|                             console.log(`Auto-joining ${room.room_id}`); | ||||
|                             MatrixClientPeg.get().joinRoom(room.room_id); | ||||
|                         } | ||||
|                     }); | ||||
|                 }, (err) => { | ||||
|                     console.error('Error getting team config', err); | ||||
|                 }); | ||||
| 
 | ||||
|                 return teamToken; | ||||
|             }, (err) => { | ||||
|                 console.error('Error tracking referral', err); | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         trackPromise.then((teamToken) => { | ||||
|             console.info('Team token promise',teamToken); | ||||
|             this.props.onLoggedIn({ | ||||
|                 userId: response.user_id, | ||||
|                 deviceId: response.device_id, | ||||
|                 homeserverUrl: this.registerLogic.getHomeserverUrl(), | ||||
|                 identityServerUrl: this.registerLogic.getIdentityServerUrl(), | ||||
|                 accessToken: response.access_token | ||||
|             }, teamToken); | ||||
|         }).then(() => { | ||||
|             return this._setupPushers(); | ||||
|         }).done(); | ||||
|     }, | ||||
| 
 | ||||
|     _setupPushers: function() { | ||||
|         if (!this.props.brand) { | ||||
|             return; | ||||
|             return q(); | ||||
|         } | ||||
|         MatrixClientPeg.get().getPushers().done((resp)=>{ | ||||
|             var pushers = resp.pushers; | ||||
|             for (var i = 0; i < pushers.length; ++i) { | ||||
|         return MatrixClientPeg.get().getPushers().then((resp)=>{ | ||||
|             const pushers = resp.pushers; | ||||
|             for (let i = 0; i < pushers.length; ++i) { | ||||
|                 if (pushers[i].kind == 'email') { | ||||
|                     var emailPusher = pushers[i]; | ||||
|                     const emailPusher = pushers[i]; | ||||
|                     emailPusher.data = { brand: this.props.brand }; | ||||
|                     MatrixClientPeg.get().setPusher(emailPusher).done(() => { | ||||
|                         console.log("Set email branding to " + this.props.brand); | ||||
|  | @ -327,116 +286,114 @@ module.exports = React.createClass({ | |||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onCaptchaResponse: function(response) { | ||||
|         this.registerLogic.tellStage("m.login.recaptcha", { | ||||
|             response: response | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     onTeamSelected: function(teamSelected) { | ||||
|         if (!this._unmounted) { | ||||
|             this.setState({ teamSelected }); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _getRegisterContentJsx: function() { | ||||
|         const Spinner = sdk.getComponent("elements.Spinner"); | ||||
|     _makeRegisterRequest: function(auth) { | ||||
|         let guestAccessToken = this.props.guestAccessToken; | ||||
| 
 | ||||
|         var currStep = this.registerLogic.getStep(); | ||||
|         var registerStep; | ||||
|         switch (currStep) { | ||||
|             case "Register.COMPLETE": | ||||
|                 break; // NOP
 | ||||
|             case "Register.START": | ||||
|             case "Register.STEP_m.login.dummy": | ||||
|                 // NB. Our 'username' prop is specifically for upgrading
 | ||||
|                 // a guest account
 | ||||
|                 if (this.state.teamServerBusy) { | ||||
|                     registerStep = <Spinner />; | ||||
|                     break; | ||||
|                 } | ||||
|                 registerStep = ( | ||||
|         if ( | ||||
|             this.state.formVals.username !== this.props.username || | ||||
|             this.state.hsUrl != this.props.defaultHsUrl | ||||
|         ) { | ||||
|             // don't try to upgrade if we changed our username
 | ||||
|             // or are registering on a different HS
 | ||||
|             guestAccessToken = null; | ||||
|         } | ||||
| 
 | ||||
|         return this._matrixClient.register( | ||||
|             this.state.formVals.username, | ||||
|             this.state.formVals.password, | ||||
|             undefined, // session id: included in the auth dict already
 | ||||
|             auth, | ||||
|             // Only send the bind_email param if we're sending username / pw params
 | ||||
|             // (Since we need to send no params at all to use the ones saved in the
 | ||||
|             // session).
 | ||||
|             Boolean(this.state.formVals.username) || undefined, | ||||
|             guestAccessToken, | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     _getUIAuthInputs() { | ||||
|         return { | ||||
|             emailAddress: this.state.formVals.email, | ||||
|             phoneCountry: this.state.formVals.phoneCountry, | ||||
|             phoneNumber: this.state.formVals.phoneNumber, | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         const LoginHeader = sdk.getComponent('login.LoginHeader'); | ||||
|         const LoginFooter = sdk.getComponent('login.LoginFooter'); | ||||
|         const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); | ||||
|         const Spinner = sdk.getComponent("elements.Spinner"); | ||||
|         const ServerConfig = sdk.getComponent('views.login.ServerConfig'); | ||||
| 
 | ||||
|         let registerBody; | ||||
|         if (this.state.doingUIAuth) { | ||||
|             registerBody = ( | ||||
|                 <InteractiveAuth | ||||
|                     matrixClient={this._matrixClient} | ||||
|                     makeRequest={this._makeRegisterRequest} | ||||
|                     onAuthFinished={this._onUIAuthFinished} | ||||
|                     inputs={this._getUIAuthInputs()} | ||||
|                     makeRegistrationUrl={this.props.makeRegistrationUrl} | ||||
|                     sessionId={this.props.sessionId} | ||||
|                     clientSecret={this.props.clientSecret} | ||||
|                     emailSid={this.props.idSid} | ||||
|                     poll={true} | ||||
|                 /> | ||||
|             ); | ||||
|         } else if (this.state.busy || this.state.teamServerBusy) { | ||||
|             registerBody = <Spinner />; | ||||
|         } else { | ||||
|             let guestUsername = this.props.username; | ||||
|             if (this.state.hsUrl != this.props.defaultHsUrl) { | ||||
|                 guestUsername = null; | ||||
|             } | ||||
|             let errorSection; | ||||
|             if (this.state.errorText) { | ||||
|                 errorSection = <div className="mx_Login_error">{this.state.errorText}</div>; | ||||
|             } | ||||
|             registerBody = ( | ||||
|                 <div> | ||||
|                     <RegistrationForm | ||||
|                         showEmail={true} | ||||
|                         defaultUsername={this.state.formVals.username} | ||||
|                         defaultEmail={this.state.formVals.email} | ||||
|                         defaultPassword={this.state.formVals.password} | ||||
|                         teamsConfig={this.state.teamsConfig} | ||||
|                         guestUsername={this.props.username} | ||||
|                         guestUsername={guestUsername} | ||||
|                         minPasswordLength={MIN_PASSWORD_LENGTH} | ||||
|                         onError={this.onFormValidationFailed} | ||||
|                         onRegisterClick={this.onFormSubmit} | ||||
|                         onTeamSelected={this.onTeamSelected} | ||||
|                     /> | ||||
|                 ); | ||||
|                 break; | ||||
|             case "Register.STEP_m.login.email.identity": | ||||
|                 registerStep = ( | ||||
|                     <div> | ||||
|                         Please check your email to continue registration. | ||||
|                     </div> | ||||
|                 ); | ||||
|                 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 sitePublicKey={publicKey} | ||||
|                         onCaptchaResponse={this.onCaptchaResponse} | ||||
|                     {errorSection} | ||||
|                     <ServerConfig ref="serverConfig" | ||||
|                         withToggleButton={true} | ||||
|                         customHsUrl={this.props.customHsUrl} | ||||
|                         customIsUrl={this.props.customIsUrl} | ||||
|                         defaultHsUrl={this.props.defaultHsUrl} | ||||
|                         defaultIsUrl={this.props.defaultIsUrl} | ||||
|                         onHsUrlChanged={this.onHsUrlChanged} | ||||
|                         onIsUrlChanged={this.onIsUrlChanged} | ||||
|                         delayTimeMs={1000} | ||||
|                     /> | ||||
|                 ); | ||||
|                 break; | ||||
|             default: | ||||
|                 console.error("Unknown register state: %s", currStep); | ||||
|                 break; | ||||
|         } | ||||
|         var busySpinner; | ||||
|         if (this.state.busy) { | ||||
|             busySpinner = ( | ||||
|                 <Spinner /> | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         var returnToAppJsx; | ||||
|         let returnToAppJsx; | ||||
|         if (this.props.onCancelClick) { | ||||
|             returnToAppJsx = | ||||
|             returnToAppJsx = ( | ||||
|                 <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> | ||||
|                     Return to app | ||||
|                 </a>; | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <div> | ||||
|                 <h2>Create an account</h2> | ||||
|                 {registerStep} | ||||
|                 <div className="mx_Login_error">{this.state.errorText}</div> | ||||
|                 {busySpinner} | ||||
|                 <ServerConfig ref="serverConfig" | ||||
|                     withToggleButton={ this.registerLogic.getStep() === "Register.START" } | ||||
|                     customHsUrl={this.props.customHsUrl} | ||||
|                     customIsUrl={this.props.customIsUrl} | ||||
|                     defaultHsUrl={this.props.defaultHsUrl} | ||||
|                     defaultIsUrl={this.props.defaultIsUrl} | ||||
|                     onHsUrlChanged={this.onHsUrlChanged} | ||||
|                     onIsUrlChanged={this.onIsUrlChanged} | ||||
|                     delayTimeMs={1000} /> | ||||
|                 <div className="mx_Login_error"> | ||||
|                 </div> | ||||
|                 <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> | ||||
|                     I already have an account | ||||
|                 </a> | ||||
|                 { returnToAppJsx } | ||||
|             </div> | ||||
|         ); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         var LoginHeader = sdk.getComponent('login.LoginHeader'); | ||||
|         var LoginFooter = sdk.getComponent('login.LoginFooter'); | ||||
|             ); | ||||
|         } | ||||
|         return ( | ||||
|             <div className="mx_Login"> | ||||
|                 <div className="mx_Login_box"> | ||||
|  | @ -446,7 +403,12 @@ module.exports = React.createClass({ | |||
|                             this.state.teamSelected.domain + "/icon.png" : | ||||
|                             null} | ||||
|                     /> | ||||
|                     {this._getRegisterContentJsx()} | ||||
|                     <h2>Create an account</h2> | ||||
|                     {registerBody} | ||||
|                     <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> | ||||
|                         I already have an account | ||||
|                     </a> | ||||
|                     {returnToAppJsx} | ||||
|                     <LoginFooter /> | ||||
|                 </div> | ||||
|             </div> | ||||
|  |  | |||
|  | @ -27,6 +27,9 @@ export default React.createClass({ | |||
|     displayName: 'InteractiveAuthDialog', | ||||
| 
 | ||||
|     propTypes: { | ||||
|         // matrix client to use for UI auth requests
 | ||||
|         matrixClient: React.PropTypes.object.isRequired, | ||||
| 
 | ||||
|         // response from initial request. If not supplied, will do a request on
 | ||||
|         // mount.
 | ||||
|         authData: React.PropTypes.shape({ | ||||
|  | @ -49,22 +52,62 @@ export default React.createClass({ | |||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|         return { | ||||
|             authError: null, | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _onAuthFinished: function(success, result) { | ||||
|         if (success) { | ||||
|             this.props.onFinished(true); | ||||
|         } else { | ||||
|             this.setState({ | ||||
|                 authError: result, | ||||
|             }); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _onDismissClick: function() { | ||||
|         this.props.onFinished(false); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         const InteractiveAuth = sdk.getComponent("structures.InteractiveAuth"); | ||||
|         const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); | ||||
| 
 | ||||
|         let content; | ||||
|         if (this.state.authError) { | ||||
|             content = ( | ||||
|                 <div> | ||||
|                     <div>{this.state.authError.message || this.state.authError.toString()}</div> | ||||
|                     <br /> | ||||
|                     <AccessibleButton onClick={this._onDismissClick} | ||||
|                         className="mx_UserSettings_button" | ||||
|                     > | ||||
|                         Dismiss | ||||
|                     </AccessibleButton> | ||||
|                 </div> | ||||
|             ); | ||||
|         } else { | ||||
|             content = ( | ||||
|                 <div> | ||||
|                     <InteractiveAuth ref={this._collectInteractiveAuth} | ||||
|                         matrixClient={this.props.matrixClient} | ||||
|                         authData={this.props.authData} | ||||
|                         makeRequest={this.props.makeRequest} | ||||
|                         onAuthFinished={this._onAuthFinished} | ||||
|                     /> | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return ( | ||||
|             <BaseDialog className="mx_InteractiveAuthDialog" | ||||
|                 onFinished={this.props.onFinished} | ||||
|                 title={this.props.title} | ||||
|                 title={this.state.authError ? 'Error' : this.props.title} | ||||
|             > | ||||
|                 <div> | ||||
|                     <InteractiveAuth ref={this._collectInteractiveAuth} | ||||
|                         authData={this.props.authData} | ||||
|                         makeRequest={this.props.makeRequest} | ||||
|                         onFinished={this.props.onFinished} | ||||
|                     /> | ||||
|                 </div> | ||||
|                 {content} | ||||
|             </BaseDialog> | ||||
|         ); | ||||
|     }, | ||||
|  |  | |||
|  | @ -18,7 +18,6 @@ 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 | ||||
|  * InteractiveAuth to prompt the user to enter the information needed | ||||
|  | @ -28,13 +27,32 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; | |||
|  * Call getEntryComponentForLoginType() to get a component suitable for a | ||||
|  * particular login type. Each component requires the same properties: | ||||
|  * | ||||
|  * matrixClient:           A matrix client. May be a different one to the one | ||||
|  *                         currently being used generally (eg. to register with | ||||
|  *                         one HS whilst beign a guest on another). | ||||
|  * loginType:              the login type of the auth stage being attempted | ||||
|  * authSessionId:          session id from the server | ||||
|  * clientSecret:           The client secret in use for ID server auth sessions | ||||
|  * 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 | ||||
|  * busy:                   a boolean indicating whether the auth logic is doing something | ||||
|  *                         the user needs to wait for. | ||||
|  * inputs:                 Object of inputs provided by the user, as in js-sdk | ||||
|  *                         interactive-auth | ||||
|  * stageState:             Stage-specific object used for communicating state information | ||||
|  *                         to the UI from the state-specific auth logic. | ||||
|  *                         Defined keys for stages are: | ||||
|  *                             m.login.email.identity: | ||||
|  *                              * emailSid: string representing the sid of the active | ||||
|  *                                          verification session from the ID server, or | ||||
|  *                                          null if no session is active. | ||||
|  * fail:                   a function which should be called with an error object if an | ||||
|  *                         error occurred during the auth stage. This will cause the auth | ||||
|  *                         session to be failed and the process to go back to the start. | ||||
|  * setEmailSid:            m.login.email.identity only: a function to be called with the | ||||
|  *                         email sid after a token is requested. | ||||
|  * makeRegistrationUrl     A function that makes a registration URL | ||||
|  * | ||||
|  * Each component may also provide the following functions (beyond the standard React ones): | ||||
|  *    focus: set the input focus appropriately in the form. | ||||
|  | @ -48,6 +66,7 @@ export const PasswordAuthEntry = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     propTypes: { | ||||
|         matrixClient: React.PropTypes.object.isRequired, | ||||
|         submitAuthDict: React.PropTypes.func.isRequired, | ||||
|         errorText: React.PropTypes.string, | ||||
|         // is the auth logic currently waiting for something to
 | ||||
|  | @ -73,7 +92,7 @@ export const PasswordAuthEntry = React.createClass({ | |||
| 
 | ||||
|         this.props.submitAuthDict({ | ||||
|             type: PasswordAuthEntry.LOGIN_TYPE, | ||||
|             user: MatrixClientPeg.get().credentials.userId, | ||||
|             user: this.props.matrixClient.credentials.userId, | ||||
|             password: this.refs.passwordField.value, | ||||
|         }); | ||||
|     }, | ||||
|  | @ -164,10 +183,83 @@ export const RecaptchaAuthEntry = React.createClass({ | |||
|     }, | ||||
| }); | ||||
| 
 | ||||
| export const EmailIdentityAuthEntry = React.createClass({ | ||||
|     displayName: 'EmailIdentityAuthEntry', | ||||
| 
 | ||||
|     statics: { | ||||
|         LOGIN_TYPE: "m.login.email.identity", | ||||
|     }, | ||||
| 
 | ||||
|     propTypes: { | ||||
|         matrixClient: React.PropTypes.object.isRequired, | ||||
|         submitAuthDict: React.PropTypes.func.isRequired, | ||||
|         authSessionId: React.PropTypes.string.isRequired, | ||||
|         clientSecret: React.PropTypes.string.isRequired, | ||||
|         inputs: React.PropTypes.object.isRequired, | ||||
|         stageState: React.PropTypes.object.isRequired, | ||||
|         fail: React.PropTypes.func.isRequired, | ||||
|         setEmailSid: React.PropTypes.func.isRequired, | ||||
|         makeRegistrationUrl: React.PropTypes.func.isRequired, | ||||
|     }, | ||||
| 
 | ||||
|     getInitialState: function() { | ||||
|         return { | ||||
|             requestingToken: false, | ||||
|         }; | ||||
|     }, | ||||
| 
 | ||||
|     componentWillMount: function() { | ||||
|         if (this.props.stageState.emailSid === null) { | ||||
|             this.setState({requestingToken: true}); | ||||
|             this._requestEmailToken().catch((e) => { | ||||
|                 this.props.fail(e); | ||||
|             }).finally(() => { | ||||
|                 this.setState({requestingToken: false}); | ||||
|             }).done(); | ||||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     /* | ||||
|      * Requests a verification token by email. | ||||
|      */ | ||||
|     _requestEmailToken: function() { | ||||
|         const nextLink = this.props.makeRegistrationUrl({ | ||||
|             client_secret: this.props.clientSecret, | ||||
|             hs_url: this.props.matrixClient.getHomeserverUrl(), | ||||
|             is_url: this.props.matrixClient.getIdentityServerUrl(), | ||||
|             session_id: this.props.authSessionId, | ||||
|         }); | ||||
| 
 | ||||
|         return this.props.matrixClient.requestRegisterEmailToken( | ||||
|             this.props.inputs.emailAddress, | ||||
|             this.props.clientSecret, | ||||
|             1, // TODO: Multiple send attempts?
 | ||||
|             nextLink, | ||||
|         ).then((result) => { | ||||
|             this.props.setEmailSid(result.sid); | ||||
|         }); | ||||
|     }, | ||||
| 
 | ||||
|     render: function() { | ||||
|         if (this.state.requestingToken) { | ||||
|             const Loader = sdk.getComponent("elements.Spinner"); | ||||
|             return <Loader />; | ||||
|         } else { | ||||
|             return ( | ||||
|                 <div> | ||||
|                     <p>An email has been sent to <i>{this.props.inputs.emailAddress}</i></p> | ||||
|                     <p>Please check your email to continue registration.</p> | ||||
|                 </div> | ||||
|             ); | ||||
|         } | ||||
|     }, | ||||
| }); | ||||
| 
 | ||||
| export const FallbackAuthEntry = React.createClass({ | ||||
|     displayName: 'FallbackAuthEntry', | ||||
| 
 | ||||
|     propTypes: { | ||||
|         matrixClient: React.PropTypes.object.isRequired, | ||||
|         authSessionId: React.PropTypes.string.isRequired, | ||||
|         loginType: React.PropTypes.string.isRequired, | ||||
|         submitAuthDict: React.PropTypes.func.isRequired, | ||||
|  | @ -189,7 +281,7 @@ export const FallbackAuthEntry = React.createClass({ | |||
|     }, | ||||
| 
 | ||||
|     _onShowFallbackClick: function() { | ||||
|         var url = MatrixClientPeg.get().getFallbackAuthUrl( | ||||
|         var url = this.props.matrixClient.getFallbackAuthUrl( | ||||
|             this.props.loginType, | ||||
|             this.props.authSessionId | ||||
|         ); | ||||
|  | @ -199,7 +291,7 @@ export const FallbackAuthEntry = React.createClass({ | |||
|     _onReceiveMessage: function(event) { | ||||
|         if ( | ||||
|             event.data === "authDone" && | ||||
|             event.origin === MatrixClientPeg.get().getHomeserverUrl() | ||||
|             event.origin === this.props.matrixClient.getHomeserverUrl() | ||||
|         ) { | ||||
|             this.props.submitAuthDict({}); | ||||
|         } | ||||
|  | @ -220,6 +312,7 @@ export const FallbackAuthEntry = React.createClass({ | |||
| const AuthEntryComponents = [ | ||||
|     PasswordAuthEntry, | ||||
|     RecaptchaAuthEntry, | ||||
|     EmailIdentityAuthEntry, | ||||
| ]; | ||||
| 
 | ||||
| export function getEntryComponentForLoginType(loginType) { | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| /* | ||||
| Copyright 2015, 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. | ||||
|  | @ -14,18 +15,16 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| import React from 'react'; | ||||
| import { field_input_incorrect } from '../../../UiEffects'; | ||||
| import sdk from '../../../index'; | ||||
| import Email from '../../../email'; | ||||
| import Modal from '../../../Modal'; | ||||
| 
 | ||||
| var React = require('react'); | ||||
| var UiEffects = require('../../../UiEffects'); | ||||
| var sdk = require('../../../index'); | ||||
| var Email = require('../../../email'); | ||||
| var Modal = require("../../../Modal"); | ||||
| 
 | ||||
| var FIELD_EMAIL = 'field_email'; | ||||
| var FIELD_USERNAME = 'field_username'; | ||||
| var FIELD_PASSWORD = 'field_password'; | ||||
| var FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; | ||||
| const FIELD_EMAIL = 'field_email'; | ||||
| const FIELD_USERNAME = 'field_username'; | ||||
| const FIELD_PASSWORD = 'field_password'; | ||||
| const FIELD_PASSWORD_CONFIRM = 'field_password_confirm'; | ||||
| 
 | ||||
| /** | ||||
|  * A pure UI component which displays a registration form. | ||||
|  | @ -54,15 +53,13 @@ module.exports = React.createClass({ | |||
|         // a different username will cause a fresh account to be generated.
 | ||||
|         guestUsername: React.PropTypes.string, | ||||
| 
 | ||||
|         showEmail: React.PropTypes.bool, | ||||
|         minPasswordLength: React.PropTypes.number, | ||||
|         onError: React.PropTypes.func, | ||||
|         onRegisterClick: React.PropTypes.func // onRegisterClick(Object) => ?Promise
 | ||||
|         onRegisterClick: React.PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
 | ||||
|     }, | ||||
| 
 | ||||
|     getDefaultProps: function() { | ||||
|         return { | ||||
|             showEmail: false, | ||||
|             minPasswordLength: 6, | ||||
|             onError: function(e) { | ||||
|                 console.error(e); | ||||
|  | @ -174,8 +171,8 @@ module.exports = React.createClass({ | |||
|                         showSupportEmail: false, | ||||
|                     }); | ||||
|                 } | ||||
|                 const valid = email === '' || Email.looksValid(email); | ||||
|                 this.markFieldValid(field_id, valid, "RegistrationForm.ERR_EMAIL_INVALID"); | ||||
|                 const emailValid = email === '' || Email.looksValid(email); | ||||
|                 this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); | ||||
|                 break; | ||||
|             case FIELD_USERNAME: | ||||
|                 // XXX: SPEC-1
 | ||||
|  | @ -227,7 +224,7 @@ module.exports = React.createClass({ | |||
|         fieldValid[field_id] = val; | ||||
|         this.setState({fieldValid: fieldValid}); | ||||
|         if (!val) { | ||||
|             UiEffects.field_input_incorrect(this.fieldElementById(field_id)); | ||||
|             field_input_incorrect(this.fieldElementById(field_id)); | ||||
|             this.props.onError(error_code); | ||||
|         } | ||||
|     }, | ||||
|  | @ -245,8 +242,8 @@ module.exports = React.createClass({ | |||
|         } | ||||
|     }, | ||||
| 
 | ||||
|     _classForField: function(field_id, baseClass) { | ||||
|         let cls = baseClass || ''; | ||||
|     _classForField: function(field_id, ...baseClasses) { | ||||
|         let cls = baseClasses.join(' '); | ||||
|         if (this.state.fieldValid[field_id] === false) { | ||||
|             if (cls) cls += ' '; | ||||
|             cls += 'error'; | ||||
|  | @ -256,44 +253,44 @@ module.exports = React.createClass({ | |||
| 
 | ||||
|     render: function() { | ||||
|         var self = this; | ||||
|         var emailSection, belowEmailSection, registerButton; | ||||
|         if (this.props.showEmail) { | ||||
|             emailSection = ( | ||||
| 
 | ||||
|         const emailSection = ( | ||||
|             <div> | ||||
|                 <input type="text" ref="email" | ||||
|                     autoFocus={true} placeholder="Email address (optional)" | ||||
|                     defaultValue={this.props.defaultEmail} | ||||
|                     className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} | ||||
|                     onBlur={function() {self.validateField(FIELD_EMAIL);}} | ||||
|                     value={self.state.email}/> | ||||
|             ); | ||||
|             if (this.props.teamsConfig) { | ||||
|                 if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { | ||||
|                     belowEmailSection = ( | ||||
|                         <p className="mx_Login_support"> | ||||
|                             Sorry, but your university is not registered with us just yet.  | ||||
|                             Email us on  | ||||
|                             <a href={"mailto:" + this.props.teamsConfig.supportEmail}> | ||||
|                                 {this.props.teamsConfig.supportEmail} | ||||
|                             </a>  | ||||
|                             to get your university signed up. Or continue to register with Riot to enjoy our open source platform. | ||||
|                         </p> | ||||
|                     ); | ||||
|                 } else if (this.state.selectedTeam) { | ||||
|                     belowEmailSection = ( | ||||
|                         <p className="mx_Login_support"> | ||||
|                             You are registering with {this.state.selectedTeam.name} | ||||
|                         </p> | ||||
|                     ); | ||||
|                 } | ||||
|             </div> | ||||
|         ); | ||||
|         let belowEmailSection; | ||||
|         if (this.props.teamsConfig) { | ||||
|             if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { | ||||
|                 belowEmailSection = ( | ||||
|                     <p className="mx_Login_support"> | ||||
|                         Sorry, but your university is not registered with us just yet.  | ||||
|                         Email us on  | ||||
|                         <a href={"mailto:" + this.props.teamsConfig.supportEmail}> | ||||
|                             {this.props.teamsConfig.supportEmail} | ||||
|                         </a>  | ||||
|                         to get your university signed up. Or continue to register with Riot to enjoy our open source platform. | ||||
|                     </p> | ||||
|                 ); | ||||
|             } else if (this.state.selectedTeam) { | ||||
|                 belowEmailSection = ( | ||||
|                     <p className="mx_Login_support"> | ||||
|                         You are registering with {this.state.selectedTeam.name} | ||||
|                     </p> | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|         if (this.props.onRegisterClick) { | ||||
|             registerButton = ( | ||||
|                 <input className="mx_Login_submit" type="submit" value="Register" /> | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         var placeholderUserName = "User name"; | ||||
|         const registerButton = ( | ||||
|             <input className="mx_Login_submit" type="submit" value="Register" /> | ||||
|         ); | ||||
| 
 | ||||
|         let placeholderUserName = "User name"; | ||||
|         if (this.props.guestUsername) { | ||||
|             placeholderUserName += " (default: " + this.props.guestUsername + ")"; | ||||
|         } | ||||
|  |  | |||
|  | @ -71,6 +71,7 @@ export default class DevicesPanelEntry extends React.Component { | |||
|             var InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); | ||||
| 
 | ||||
|             Modal.createDialog(InteractiveAuthDialog, { | ||||
|                 matrixClient: MatrixClientPeg.get(), | ||||
|                 authData: error.data, | ||||
|                 makeRequest: this._makeDeleteRequest, | ||||
|             }); | ||||
|  |  | |||
|  | @ -57,6 +57,7 @@ describe('InteractiveAuthDialog', function () { | |||
| 
 | ||||
|         const dlg = ReactDOM.render( | ||||
|             <InteractiveAuthDialog | ||||
|                 matrixClient={client} | ||||
|                 authData={{ | ||||
|                     session: "sess", | ||||
|                     flows: [ | ||||
|  |  | |||
|  | @ -92,6 +92,7 @@ export function createTestClient() { | |||
|         sendTextMessage: () => q({}), | ||||
|         sendHtmlMessage: () => q({}), | ||||
|         getSyncState: () => "SYNCING", | ||||
|         generateClientSecret: () => "t35tcl1Ent5ECr3T", | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 David Baker
						David Baker