From 991a96cfc5b4e3875b0bcf21006e582eea26e9ad Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Wed, 18 Nov 2015 17:13:43 +0000 Subject: [PATCH] Get dummy registrations working This means you can now register on localhost without needing an email. Email and Recaptcha are still broken. --- src/Signup.js | 192 +++++++++++++++++++++++++++++++++++++++++++- src/SignupStages.js | 125 ++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 src/SignupStages.js diff --git a/src/Signup.js b/src/Signup.js index 2446130405..b679df4e8a 100644 --- a/src/Signup.js +++ b/src/Signup.js @@ -1,8 +1,11 @@ "use strict"; var MatrixClientPeg = require("./MatrixClientPeg"); +var SignupStages = require("./SignupStages"); var dis = require("./dispatcher"); var q = require("q"); +const EMAIL_STAGE_TYPE = "m.login.email.identity"; + class Signup { constructor(hsUrl, isUrl) { this._hsUrl = hsUrl; @@ -26,17 +29,200 @@ class Signup { } } + class Register extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); - this._state = "Register.START"; + this.setStep("START"); + this.data = null; // from the server + this.username = null; // desired + this.email = null; // desired + this.password = null; // desired + this.params = {}; // random other stuff (e.g. query params) + this.credentials = null; } - getState() { - return this._state; + setClientSecret(secret) { + this.params.clientSecret = secret; + } + + setSessionId(sessionId) { + this.params.sessionId = sessionId; + } + + setRegistrationUrl(regUrl) { + this.params.registrationUrl = regUrl; + } + + setIdSid(idSid) { + this.params.idSid = idSid; + } + + getStep() { + return this._step; + } + + getCredentials() { + return this.credentials; + } + + 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" + }); + } + + register(formVals) { + var {username, password, email} = formVals; + this.email = email; + this.username = username; + this.password = password; + + // feels a bit wrong to be clobbering the global client for something we + // don't even know if it'll work, but we'll leave this here for now to + // not complicate matters further. It would be nicer to isolate this + // logic entirely from the rest of the app though. + MatrixClientPeg.replaceUsingUrls( + this._hsUrl, + this._isUrl + ); + return this._tryRegister(); + } + + _tryRegister(authDict) { + console.log("_tryRegister %s", JSON.stringify(authDict)); + var self = this; + return MatrixClientPeg.get().register( + this.username, this.password, this._sessionId, authDict + ).then(function(result) { + console.log("Got a final response"); + self.credentials = result; + self.setStep("COMPLETE"); + return result; // contains the credentials + }, function(error) { + console.error(error); + if (error.httpStatus === 401 && error.data && error.data.flows) { + self.data = error.data || {}; + var flow = self.chooseFlow(error.data.flows); + + if (flow) { + var flowStage = self.firstUncompletedStageIndex(flow); + return self.startStage(flow.stages[flowStage]); + } + else { + throw new Error("Unable to register - missing email address?"); + } + } else { + if (error.errcode === 'M_USER_IN_USE') { + throw new Error("Username in use"); + } else if (error.httpStatus == 401) { + throw new Error("Authorisation failed!"); + } else if (error.httpStatus >= 400 && error.httpStatus < 500) { + throw new Error(`Registration failed! (${error.httpStatus})`); + } 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." + ); + } + } + }); + } + + firstUncompletedStageIndex(flow) { + if (!this.completedStages) { + return 0; + } + for (var i = 0; i < flow.stages.length; ++i) { + if (this.completedStages.indexOf(flow.stages[i]) == -1) { + return i; + } + } + } + + numCompletedStages(flow) { + if (!this.completedStages) { + return 0; + } + var nCompleted = 0; + for (var i = 0; i < flow.stages.length; ++i) { + if (this.completedStages.indexOf(flow.stages[i]) > -1) { + ++nCompleted; + } + } + return nCompleted; + } + + startStage(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(MatrixClientPeg.get(), this); + return stage.complete().then(function(request) { + if (request.auth) { + return self._tryRegister(request.auth); + } + }); + } + + hasCompletedStage(stageType) { + var completed = (this.data || {}).completed || []; + return completed.indexOf(stageType) !== -1; + } + + 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; + } } } + class Login extends Signup { constructor(hsUrl, isUrl) { super(hsUrl, isUrl); diff --git a/src/SignupStages.js b/src/SignupStages.js new file mode 100644 index 0000000000..a4b51f0abb --- /dev/null +++ b/src/SignupStages.js @@ -0,0 +1,125 @@ +"use strict"; +var q = require("q"); + +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"); + } +} +Stage.TYPE = "NOT IMPLEMENTED"; + + +class DummyStage extends Stage { + constructor(matrixClient, signupInstance) { + super(DummyStage.TYPE, matrixClient, signupInstance); + } + + complete() { + return q({ + auth: { + type: DummyStage.TYPE + } + }); + } +} +DummyStage.TYPE = "m.login.dummy"; + + +class RecaptchaStage extends Stage { + constructor(matrixClient, signupInstance) { + super(RecaptchaStage.TYPE, matrixClient, signupInstance); + } + + complete() { + var publicKey; + if (this.signupInstance.params['m.login.recaptcha']) { + publicKey = this.signupInstance.params['m.login.recaptcha'].public_key; + } + if (!publicKey) { + return q.reject({ + message: "This server has not supplied enough information for Recaptcha " + + "authentication", + isFatal: true + }); + } + + var defer = q.defer(); + global.grecaptcha.render('mx_recaptcha', { + sitekey: publicKey, + callback: function(response) { + return defer.resolve({ + auth: { + type: 'm.login.recaptcha', + response: response + } + }); + } + }); + + return defer.promise; + } +} +RecaptchaStage.TYPE = "m.login.recaptcha"; + + +class EmailIdentityStage extends Stage { + constructor(matrixClient, signupInstance) { + super(EmailIdentityStage.TYPE, matrixClient, signupInstance); + } + + complete() { + var config = { + clientSecret: this.client.generateClientSecret(), + sendAttempt: 1 + }; + this.signupInstance.params[EmailIdentityStage.TYPE] = config; + + var nextLink = this.signupInstance.params.registrationUrl + + '?client_secret=' + + encodeURIComponent(config.clientSecret) + + "&hs_url=" + + encodeURIComponent(this.signupInstance.getHomeserverUrl()) + + "&is_url=" + + encodeURIComponent(this.signupInstance.getIdentityServerUrl()) + + "&session_id=" + + encodeURIComponent(this.signupInstance.getSessionId()); + + return this.client.requestEmailToken( + this.signupInstance.email, + config.clientSecret, + config.sendAttempt, + nextLink + ).then(function(response) { + return {}; // don't want to make a request + }, function(error) { + console.error(error); + var e = { + isFatal: true + }; + if (error.errcode == 'THREEPID_IN_USE') { + e.message = "Email in use"; + } else { + e.message = 'Unable to contact the given identity server'; + } + return e; + }); + } +} +EmailIdentityStage.TYPE = "m.login.email.identity"; + +module.exports = { + [DummyStage.TYPE]: DummyStage, + [RecaptchaStage.TYPE]: RecaptchaStage, + [EmailIdentityStage.TYPE]: EmailIdentityStage +}; \ No newline at end of file