diff --git a/src/PasswordReset.js b/src/PasswordReset.js
new file mode 100644
index 0000000000..1029b07b70
--- /dev/null
+++ b/src/PasswordReset.js
@@ -0,0 +1,104 @@
+/*
+Copyright 2015, 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 Matrix = require("matrix-js-sdk");
+
+/**
+ * Allows a user to reset their password on a homeserver.
+ *
+ * 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 password
+ * API on the homeserver in question with the new password.
+ */
+class PasswordReset {
+
+ /**
+ * Configure the endpoints for password resetting.
+ * @param {string} homeserverUrl The URL to the HS which has the account to reset.
+ * @param {string} identityUrl The URL to the IS which has linked the email -> mxid mapping.
+ */
+ constructor(homeserverUrl, identityUrl) {
+ this.client = Matrix.createClient({
+ baseUrl: homeserverUrl,
+ idBaseUrl: identityUrl
+ });
+ this.clientSecret = generateClientSecret();
+ this.identityServerDomain = identityUrl.split("://")[1];
+ }
+
+ /**
+ * Attempt to reset the user's password. This will trigger a side-effect of
+ * sending an email to the provided email address.
+ * @param {string} emailAddress The email address
+ * @param {string} newPassword The new password for the account.
+ * @return {Promise} Resolves when the email has been sent. Then call checkEmailLinkClicked().
+ */
+ resetPassword(emailAddress, newPassword) {
+ this.password = newPassword;
+ return this.client.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 change the password
+ * for the mxid linked to the email.
+ * @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() {
+ return this.client.setPassword({
+ type: "m.login.email.identity",
+ threepid_creds: {
+ sid: this.sessionId,
+ client_secret: this.clientSecret,
+ id_server: this.identityServerDomain
+ }
+ }, this.password).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 === 404) {
+ err.message = "Your email address does not appear to be associated with a Matrix ID on this Homeserver.";
+ }
+ else if (err.httpStatus) {
+ err.message += ` (Status ${err.httpStatus})`;
+ }
+ throw err;
+ });
+ }
+}
+
+// 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;
diff --git a/src/component-index.js b/src/component-index.js
index 0c08d70b73..ac9a83346a 100644
--- a/src/component-index.js
+++ b/src/component-index.js
@@ -23,14 +23,15 @@ limitations under the License.
module.exports.components = {};
module.exports.components['structures.CreateRoom'] = require('./components/structures/CreateRoom');
+module.exports.components['structures.login.ForgotPassword'] = require('./components/structures/login/ForgotPassword');
+module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
+module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
+module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
module.exports.components['structures.MatrixChat'] = require('./components/structures/MatrixChat');
module.exports.components['structures.RoomView'] = require('./components/structures/RoomView');
module.exports.components['structures.ScrollPanel'] = require('./components/structures/ScrollPanel');
module.exports.components['structures.UploadBar'] = require('./components/structures/UploadBar');
module.exports.components['structures.UserSettings'] = require('./components/structures/UserSettings');
-module.exports.components['structures.login.Login'] = require('./components/structures/login/Login');
-module.exports.components['structures.login.PostRegistration'] = require('./components/structures/login/PostRegistration');
-module.exports.components['structures.login.Registration'] = require('./components/structures/login/Registration');
module.exports.components['views.avatars.MemberAvatar'] = require('./components/views/avatars/MemberAvatar');
module.exports.components['views.avatars.RoomAvatar'] = require('./components/views/avatars/RoomAvatar');
module.exports.components['views.create_room.CreateRoomButton'] = require('./components/views/create_room/CreateRoomButton');
@@ -51,10 +52,10 @@ module.exports.components['views.login.LoginHeader'] = require('./components/vie
module.exports.components['views.login.PasswordLogin'] = require('./components/views/login/PasswordLogin');
module.exports.components['views.login.RegistrationForm'] = require('./components/views/login/RegistrationForm');
module.exports.components['views.login.ServerConfig'] = require('./components/views/login/ServerConfig');
+module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
module.exports.components['views.messages.MFileBody'] = require('./components/views/messages/MFileBody');
module.exports.components['views.messages.MImageBody'] = require('./components/views/messages/MImageBody');
module.exports.components['views.messages.MVideoBody'] = require('./components/views/messages/MVideoBody');
-module.exports.components['views.messages.MessageEvent'] = require('./components/views/messages/MessageEvent');
module.exports.components['views.messages.TextualBody'] = require('./components/views/messages/TextualBody');
module.exports.components['views.messages.TextualEvent'] = require('./components/views/messages/TextualEvent');
module.exports.components['views.messages.UnknownBody'] = require('./components/views/messages/UnknownBody');
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 320dad09b3..ede2ab8f46 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -233,6 +233,13 @@ module.exports = React.createClass({
});
this.notifyNewScreen('register');
break;
+ case 'start_password_recovery':
+ if (this.state.logged_in) return;
+ this.replaceState({
+ screen: 'forgot_password'
+ });
+ this.notifyNewScreen('forgot_password');
+ break;
case 'token_login':
if (this.state.logged_in) return;
@@ -559,6 +566,11 @@ module.exports = React.createClass({
action: 'token_login',
params: params
});
+ } else if (screen == 'forgot_password') {
+ dis.dispatch({
+ action: 'start_password_recovery',
+ params: params
+ });
} else if (screen == 'new') {
dis.dispatch({
action: 'view_create_room',
@@ -668,6 +680,10 @@ module.exports = React.createClass({
this.showScreen("login");
},
+ onForgotPasswordClick: function() {
+ this.showScreen("forgot_password");
+ },
+
onRegistered: function(credentials) {
this.onLoggedIn(credentials);
// do post-registration stuff
@@ -706,6 +722,7 @@ module.exports = React.createClass({
var CreateRoom = sdk.getComponent('structures.CreateRoom');
var RoomDirectory = sdk.getComponent('structures.RoomDirectory');
var MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
+ var ForgotPassword = sdk.getComponent('structures.login.ForgotPassword');
// needs to be before normal PageTypes as you are logged in technically
if (this.state.screen == 'post_registration') {
@@ -801,13 +818,21 @@ module.exports = React.createClass({
onLoggedIn={this.onRegistered}
onLoginClick={this.onLoginClick} />
);
+ } else if (this.state.screen == 'forgot_password') {
+ return (
+
Your password has been reset.
+You have been logged out of all devices and will no longer receive push notifications. + To re-enable notifications, re-log in on each device.
+ +