diff --git a/src/Lifecycle.js b/src/Lifecycle.js index e44f33a731..fc8087e12d 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -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) => { diff --git a/src/Login.js b/src/Login.js new file mode 100644 index 0000000000..96f953c130 --- /dev/null +++ b/src/Login.js @@ -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; + } +} diff --git a/src/Signup.js b/src/Signup.js deleted file mode 100644 index 022a93524c..0000000000 --- a/src/Signup.js +++ /dev/null @@ -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; diff --git a/src/SignupStages.js b/src/SignupStages.js deleted file mode 100644 index 1441682c85..0000000000 --- a/src/SignupStages.js +++ /dev/null @@ -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 -}; diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 70b3c2e306..4f050cc246 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -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 ( ); }, + _onAuthStageFailed: function(e) { + this.props.onAuthFinished(false, e); + }, + _setEmailSid: function(sid) { + this._authLogic.setEmailSid(sid); + }, + render: function() { let error = null; if (this.state.errorText) { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 35f6abb9c5..44fdfcf23e 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -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} diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index fe9b544751..69195fc715 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -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; diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 737dd5939f..c25d74f566 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -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 = ; - 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 = ( + + ); + } else if (this.state.busy || this.state.teamServerBusy) { + registerBody = ; + } else { + let guestUsername = this.props.username; + if (this.state.hsUrl != this.props.defaultHsUrl) { + guestUsername = null; + } + let errorSection; + if (this.state.errorText) { + errorSection =
{this.state.errorText}
; + } + registerBody = ( +
- ); - break; - case "Register.STEP_m.login.email.identity": - registerStep = ( -
- Please check your email to continue registration. -
- ); - 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 = ( - - ); - break; - default: - console.error("Unknown register state: %s", currStep); - break; - } - var busySpinner; - if (this.state.busy) { - busySpinner = ( - +
); } - var returnToAppJsx; + let returnToAppJsx; if (this.props.onCancelClick) { - returnToAppJsx = + returnToAppJsx = ( Return to app - ; - } - - return ( -
-

Create an account

- {registerStep} -
{this.state.errorText}
- {busySpinner} - -
-
- - I already have an account - { returnToAppJsx } -
- ); - }, - - render: function() { - var LoginHeader = sdk.getComponent('login.LoginHeader'); - var LoginFooter = sdk.getComponent('login.LoginFooter'); + ); + } return (
@@ -446,7 +403,12 @@ module.exports = React.createClass({ this.state.teamSelected.domain + "/icon.png" : null} /> - {this._getRegisterContentJsx()} +

Create an account

+ {registerBody} + + I already have an account + + {returnToAppJsx}
diff --git a/src/components/views/dialogs/InteractiveAuthDialog.js b/src/components/views/dialogs/InteractiveAuthDialog.js index 66b662b23d..068a6cdde2 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.js +++ b/src/components/views/dialogs/InteractiveAuthDialog.js @@ -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 = ( +
+
{this.state.authError.message || this.state.authError.toString()}
+
+ + Dismiss + +
+ ); + } else { + content = ( +
+ +
+ ); + } + return ( -
- -
+ {content}
); }, diff --git a/src/components/views/login/InteractiveAuthEntryComponents.js b/src/components/views/login/InteractiveAuthEntryComponents.js index e18e60d7bc..e75cb082d4 100644 --- a/src/components/views/login/InteractiveAuthEntryComponents.js +++ b/src/components/views/login/InteractiveAuthEntryComponents.js @@ -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 ; + } else { + return ( +
+

An email has been sent to {this.props.inputs.emailAddress}

+

Please check your email to continue registration.

+
+ ); + } + }, +}); + 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) { diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 1a448fa84e..93e3976834 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -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 = ( +
- ); - if (this.props.teamsConfig) { - if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { - belowEmailSection = ( -

- Sorry, but your university is not registered with us just yet.  - Email us on  - - {this.props.teamsConfig.supportEmail} -   - to get your university signed up. Or continue to register with Riot to enjoy our open source platform. -

- ); - } else if (this.state.selectedTeam) { - belowEmailSection = ( -

- You are registering with {this.state.selectedTeam.name} -

- ); - } +
+ ); + let belowEmailSection; + if (this.props.teamsConfig) { + if (this.props.teamsConfig.supportEmail && this.state.showSupportEmail) { + belowEmailSection = ( +

+ Sorry, but your university is not registered with us just yet.  + Email us on  + + {this.props.teamsConfig.supportEmail} +   + to get your university signed up. Or continue to register with Riot to enjoy our open source platform. +

+ ); + } else if (this.state.selectedTeam) { + belowEmailSection = ( +

+ You are registering with {this.state.selectedTeam.name} +

+ ); } } - if (this.props.onRegisterClick) { - registerButton = ( - - ); - } - var placeholderUserName = "User name"; + const registerButton = ( + + ); + + let placeholderUserName = "User name"; if (this.props.guestUsername) { placeholderUserName += " (default: " + this.props.guestUsername + ")"; } diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.js index 60501e326f..51f3a83010 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.js @@ -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, }); diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.js b/test/components/views/dialogs/InteractiveAuthDialog-test.js index 80f027ab44..da8fc17001 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.js +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.js @@ -57,6 +57,7 @@ describe('InteractiveAuthDialog', function () { const dlg = ReactDOM.render( q({}), sendHtmlMessage: () => q({}), getSyncState: () => "SYNCING", + generateClientSecret: () => "t35tcl1Ent5ECr3T", }; }