From e68972fb1ac731bac113fc203c0b4076b2b74d26 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 18 Jan 2016 17:50:27 +0000 Subject: [PATCH 1/3] Remove generateClientSecret and use the one the js sdk gives us. --- src/PasswordReset.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/PasswordReset.js b/src/PasswordReset.js index 1029b07b70..bbafa0ef33 100644 --- a/src/PasswordReset.js +++ b/src/PasswordReset.js @@ -35,7 +35,7 @@ class PasswordReset { baseUrl: homeserverUrl, idBaseUrl: identityUrl }); - this.clientSecret = generateClientSecret(); + this.clientSecret = this.client.generateClientSecret(); this.identityServerDomain = identityUrl.split("://")[1]; } @@ -89,16 +89,4 @@ class PasswordReset { } } -// from Angular SDK -function generateClientSecret() { - var ret = ""; - var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (var i = 0; i < 32; i++) { - ret += chars.charAt(Math.floor(Math.random() * chars.length)); - } - - return ret; -} - module.exports = PasswordReset; From 4ed130ceac44b4eecb2ee9b6c1deb4b85b8da1de Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Jan 2016 16:36:54 +0000 Subject: [PATCH 2/3] Implement adding email addresses to your profile. --- src/AddThreepid.js | 76 ++++++++++ src/components/structures/UserSettings.js | 133 ++++++++++++++++-- .../views/login/RegistrationForm.js | 3 +- src/email.js | 23 +++ 4 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 src/AddThreepid.js create mode 100644 src/email.js diff --git a/src/AddThreepid.js b/src/AddThreepid.js new file mode 100644 index 0000000000..31805aad11 --- /dev/null +++ b/src/AddThreepid.js @@ -0,0 +1,76 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var MatrixClientPeg = require("./MatrixClientPeg"); + +/** + * Allows a user to add a third party identifier to their Home Server and, + * optionally, the identity servers. + * + * This involves getting an email token from the identity server to "prove" that + * the client owns the given email address, which is then passed to the + * add threepid API on the homeserver. + */ +class AddThreepid { + constructor() { + this.clientSecret = MatrixClientPeg.get().generateClientSecret(); + } + + /** + * Attempt to add an email threepid. This will trigger a side-effect of + * sending an email to the provided email address. + * @param {string} emailAddress The email address to add + * @param {boolean} bind If True, bind this email to this mxid on the Identity Server + * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked(). + */ + addEmailAddress(emailAddress, bind) { + this.bind = bind; + return MatrixClientPeg.get().requestEmailToken(emailAddress, this.clientSecret, 1).then((res) => { + this.sessionId = res.sid; + return res; + }, function(err) { + if (err.httpStatus) { + err.message = err.message + ` (Status ${err.httpStatus})`; + } + throw err; + }); + } + + /** + * Checks if the email link has been clicked by attempting to add the threepid + * @return {Promise} Resolves if the password was reset. Rejects with an object + * with a "message" property which contains a human-readable message detailing why + * the reset failed, e.g. "There is no mapped matrix user ID for the given email address". + */ + checkEmailLinkClicked() { + var identityServerDomain = MatrixClientPeg.get().idBaseUrl.split("://")[1]; + return MatrixClientPeg.get().addThreePid({ + sid: this.sessionId, + client_secret: this.clientSecret, + id_server: identityServerDomain + }, this.bind).catch(function(err) { + if (err.httpStatus === 401) { + err.message = "Failed to verify email address: make sure you clicked the link in the email"; + } + else if (err.httpStatus) { + err.message += ` (Status ${err.httpStatus})`; + } + throw err; + }); + } +} + +module.exports = AddThreepid; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 44b7b9a973..8bb3cb5ecb 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ var React = require('react'); +var ReactDOM = require('react-dom'); var sdk = require('../../index'); var MatrixClientPeg = require("../../MatrixClientPeg"); var Modal = require('../../Modal'); @@ -22,6 +23,8 @@ var q = require('q'); var version = require('../../../package.json').version; var UserSettingsStore = require('../../UserSettingsStore'); var GeminiScrollbar = require('react-gemini-scrollbar'); +var Email = require('../../email'); +var AddThreepid = require('../../AddThreepid'); module.exports = React.createClass({ displayName: 'UserSettings', @@ -42,6 +45,7 @@ module.exports = React.createClass({ threePids: [], clientVersion: version, phase: "UserSettings.LOADING", // LOADING, DISPLAY + email_add_pending: false, }; }, @@ -152,10 +156,85 @@ module.exports = React.createClass({ this.logoutModal.closeDialog(); }, + onEnableNotificationsChange: function(event) { + UserSettingsStore.setEnableNotifications(event.target.checked); + }, + + onAddThreepidClicked: function(value, shouldSubmit) { + if (!shouldSubmit) return; + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + + var email_address = this.refs.add_threepid_input.value; + if (!Email.looksValid(email_address)) { + Modal.createDialog(ErrorDialog, { + title: "Invalid Email Address", + description: "This doesn't appear to be a valid email address", + }); + return; + } + this.add_threepid = new AddThreepid(); + // we always bind emails when registering, so let's do the + // same here. + this.add_threepid.addEmailAddress(email_address, true).then(() => { + Modal.createDialog(QuestionDialog, { + title: "Verification Pending", + description: "Please check your email and click on the link it contains. Once this is done, click continue.", + button: 'Continue', + onFinished: this.onEmailDialogFinished, + }); + }, (err) => { + Modal.createDialog(ErrorDialog, { + title: "Unable to add email address", + description: err.toString() + }); + }); + ReactDOM.findDOMNode(this.refs.add_threepid_input).blur(); + this.setState({email_add_pending: true}); + }, + + onEmailDialogFinished: function(ok) { + if (ok) { + this.verifyEmailAddress(); + } else { + this.setState({email_add_pending: false}); + } + }, + + verifyEmailAddress: function() { + this.add_threepid.checkEmailLinkClicked().done(() => { + this.add_threepid = undefined; + this.setState({ + phase: "UserSettings.LOADING", + }); + this._refreshFromServer(); + this.setState({email_add_pending: false}); + }, (err) => { + if (err.errcode == 'M_THREEPID_AUTH_FAILED') { + var QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + var message = "Unable to verify email address. " + message += "Please check your email and click on the link it contains. Once this is done, click continue." + Modal.createDialog(QuestionDialog, { + title: "Verification Pending", + description: message, + button: 'Continue', + onFinished: this.onEmailDialogFinished, + }); + } else { + var ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createDialog(ErrorDialog, { + title: "Unable to verify email address", + description: err.toString(), + }); + } + }); + }, + render: function() { + var self = this; + var Loader = sdk.getComponent("elements.Spinner"); switch (this.state.phase) { case "UserSettings.LOADING": - var Loader = sdk.getComponent("elements.Spinner"); return ( ); @@ -170,10 +249,47 @@ module.exports = React.createClass({ var ChangePassword = sdk.getComponent("views.settings.ChangePassword"); var ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); var Notifications = sdk.getComponent("settings.Notifications"); + var EditableText = sdk.getComponent('elements.EditableText'); var avatarUrl = ( this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null ); + var threepidsSection = this.state.threepids.map(function(val, pidIndex) { + var id = "email-" + val.address; + return ( +
+
+ +
+
+ +
+
+ ); + }); + var addThreepidSection; + if (this.state.email_add_pending) { + addThreepidSection = ; + } else { + addThreepidSection = ( +
+
+
+ +
+ Add +
+
+ ); + } + threepidsSection.push(addThreepidSection); + var accountJsx; if (MatrixClientPeg.get().isGuest()) { @@ -214,20 +330,7 @@ module.exports = React.createClass({ - - {this.state.threepids.map(function(val, pidIndex) { - var id = "email-" + val.address; - return ( -
-
- -
-
- -
-
- ); - })} + {threepidsSection}
diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index 58d7ca3aab..469deb890a 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -20,6 +20,7 @@ var React = require('react'); var Velocity = require('velocity-animate'); require('velocity-ui-pack'); var sdk = require('../../../index'); +var Email = require('../../../email'); var FIELD_EMAIL = 'field_email'; var FIELD_USERNAME = 'field_username'; @@ -113,7 +114,7 @@ module.exports = React.createClass({ case FIELD_EMAIL: this.markFieldValid( field_id, - this.refs.email.value == '' || !!this.refs.email.value.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i), + this.refs.email.value == '' || Email.looksValid(this.refs.email.value), "RegistrationForm.ERR_EMAIL_INVALID" ); break; diff --git a/src/email.js b/src/email.js new file mode 100644 index 0000000000..c4375079d7 --- /dev/null +++ b/src/email.js @@ -0,0 +1,23 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +var EMAIL_ADDRESS_REGEX = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i; + +module.exports = { + looksValid: function(email) { + return EMAIL_ADDRESS_REGEX.test(email); + } +}; From c4d4e9c46e0dddc3a600435b363c31d2b694a6a4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Jan 2016 17:20:23 +0000 Subject: [PATCH 3/3] Terminate promise chain --- src/components/structures/UserSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 8bb3cb5ecb..488e7368d1 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -176,7 +176,7 @@ module.exports = React.createClass({ this.add_threepid = new AddThreepid(); // we always bind emails when registering, so let's do the // same here. - this.add_threepid.addEmailAddress(email_address, true).then(() => { + this.add_threepid.addEmailAddress(email_address, true).done(() => { Modal.createDialog(QuestionDialog, { title: "Verification Pending", description: "Please check your email and click on the link it contains. Once this is done, click continue.",