diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index fcc3b4d92..5a9d1bde2 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -73,13 +73,12 @@ angular.module('app.frontend') } Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){ - var mk = keys.mk; var requestUrl = url + "/auth/sign_in"; var request = Restangular.oneUrl(requestUrl, requestUrl); var params = {password: keys.pw, email: email}; _.merge(request, params); request.post().then(function(response){ - this.handleAuthResponse(response, email, url, authParams, mk, keys.pw); + this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw); callback(response); }.bind(this)) .catch(function(response){ @@ -91,7 +90,9 @@ angular.module('app.frontend') } this.handleAuthResponse = function(response, email, url, authParams, mk, pw) { - localStorage.setItem("server", url); + if(url) { + localStorage.setItem("server", url); + } localStorage.setItem("user", JSON.stringify(response.plain().user)); localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); localStorage.setItem("mk", mk); @@ -101,13 +102,12 @@ angular.module('app.frontend') this.register = function(url, email, password, callback) { Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){ - var mk = keys.mk; var requestUrl = url + "/auth"; var request = Restangular.oneUrl(requestUrl, requestUrl); var params = _.merge({password: keys.pw, email: email}, authParams); _.merge(request, params); request.post().then(function(response){ - this.handleAuthResponse(response, email, url, authParams, mk, keys.pw); + this.handleAuthResponse(response, email, url, authParams, keys.mk, keys.pw); callback(response); }.bind(this)) .catch(function(response){ @@ -117,55 +117,26 @@ angular.module('app.frontend') }.bind(this)); } - // this.changePassword = function(current_password, new_password) { - // this.getAuthParamsForEmail(email, function(authParams){ - // if(!authParams) { - // callback(null); - // return; - // } - // Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: current_password, email: user.email}, authParams), function(currentKeys) { - // Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: new_password, email: user.email}, authParams), function(newKeys){ - // var data = {}; - // data.current_password = currentKeys.pw; - // data.password = newKeys.pw; - // data.password_confirmation = newKeys.pw; - // - // var user = this.user; - // - // this._performPasswordChange(currentKeys, newKeys, function(response){ - // if(response && !response.error) { - // // this.showNewPasswordForm = false; - // // reencrypt data with new mk - // this.reencryptAllItemsAndSave(user, newKeys.mk, currentKeys.mk, function(success){ - // if(success) { - // this.setMk(newKeys.mk); - // alert("Your password has been changed and your data re-encrypted."); - // } else { - // // rollback password - // this._performPasswordChange(newKeys, currentKeys, function(response){ - // alert("There was an error changing your password. Your password has been rolled back."); - // window.location.reload(); - // }) - // } - // }.bind(this)); - // } else { - // // this.showNewPasswordForm = false; - // alert("There was an error changing your password. Please try again."); - // } - // }.bind(this)) - // }.bind(this)); - // }.bind(this)); - // }.bind(this)); - // } + this.changePassword = function(email, new_password, callback) { + Neeto.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){ + var requestUrl = localStorage.getItem("server") + "/auth/change_pw"; + var request = Restangular.oneUrl(requestUrl, requestUrl); + var params = _.merge({new_password: keys.pw}, authParams); + _.merge(request, params); - this._performPasswordChange = function(url, email, current_keys, new_keys, callback) { - var requestUrl = url + "/auth"; - var request = Restangular.oneUrl(requestUrl, requestUrl); - var params = {password: new_keys.pw, password_confirmation: new_keys.pw, current_password: current_keys.pw, email: email}; - _.merge(request, params); - request.patch().then(function(response){ - callback(response); - }) + request.post().then(function(response){ + this.handleAuthResponse(response, email, null, authParams, keys.mk, keys.pw); + callback(response.plain()); + }.bind(this)) + .catch(function(response){ + var error = response.data; + if(!error) { + error = {message: "Something went wrong while changing your password. Your password was not changed. Please try again."} + } + console.log("Change pw error", response); + callback({error: error}); + }) + }.bind(this)); } this.staticifyObject = function(object) { diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index 27c491280..7364b51c4 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -15,10 +15,6 @@ class AccountMenu { $scope.syncStatus = syncManager.syncStatus; - $scope.changePasswordPressed = function() { - $scope.showNewPasswordForm = !$scope.showNewPasswordForm; - } - $scope.encryptionKey = function() { return syncManager.masterKey; } @@ -31,19 +27,50 @@ class AccountMenu { return `${$scope.server}/dashboard/?server=${$scope.server}&id=${$scope.user.email}&pw=${$scope.serverPassword()}`; } - $scope.submitPasswordChange = function() { - $scope.passwordChangeData.status = "Generating New Keys..."; + $scope.newPasswordData = {}; - $timeout(function(){ - if(data.password != data.password_confirmation) { - alert("Your new password does not match its confirmation."); + $scope.showPasswordChangeForm = function() { + $scope.newPasswordData.showNewPasswordForm = true; + } + + $scope.submitPasswordChange = function() { + + if($scope.newPasswordData.newPassword != $scope.newPasswordData.newPasswordConfirmation) { + alert("Your new password does not match its confirmation."); + $scope.newPasswordData.status = null; + return; + } + + var email = $scope.user.email; + if(!email) { + alert("We don't have your email stored. Please log out then log back in to fix this issue."); + $scope.newPasswordData.status = null; + return; + } + + $scope.newPasswordData.status = "Generating New Keys..."; + + authManager.changePassword(email, $scope.newPasswordData.newPassword, function(response){ + if(response.error) { + alert("There was an error changing your password. Please try again."); + $scope.newPasswordData.status = null; return; } - authManager.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){ - - }) - + // re-encrypt all items + $scope.newPasswordData.status = "Re-encrypting all items with your new key..."; + modelManager.setAllItemsDirty(); + syncManager.sync(function(response){ + if(response.error) { + alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.") + return; + } + $scope.newPasswordData.status = "Successfully changed password and re-encrypted all items."; + alert("Your password has been changed, and your items successfully re-encrypted and synced. Be sure to log out on all other signed in applications.") + $timeout(function(){ + $scope.newPasswordData = {}; + }, 1000) + }); }) } diff --git a/app/assets/javascripts/app/services/helpers/encryptionHelper.js b/app/assets/javascripts/app/services/helpers/encryptionHelper.js index 7cf011ac8..08095d4f0 100644 --- a/app/assets/javascripts/app/services/helpers/encryptionHelper.js +++ b/app/assets/javascripts/app/services/helpers/encryptionHelper.js @@ -2,13 +2,13 @@ class EncryptionHelper { static encryptItem(item, key) { var item_key = null; - if(item.enc_item_key) { - // we reuse the key, but this is optional - item_key = Neeto.crypto.decryptText(item.enc_item_key, key); - } else { + // if(item.enc_item_key) { + // // we reuse the key, but this is optional + // item_key = Neeto.crypto.decryptText(item.enc_item_key, key); + // } else { item_key = Neeto.crypto.generateRandomEncryptionKey(); item.enc_item_key = Neeto.crypto.encryptText(item_key, key); - } + // } var ek = Neeto.crypto.firstHalfOfKey(item_key); var ak = Neeto.crypto.secondHalfOfKey(item_key); diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index aa436b6e2..1d920c018 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -225,6 +225,13 @@ class ModelManager { item.removeAllRelationships(); } + /* Used when changing encryption key */ + setAllItemsDirty() { + for(var item of this.allItems) { + item.setDirty(true); + } + } + removeItemLocally(item, callback) { _.pull(this.items, item); diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index d2d4b2fd3..851d1c0a5 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -98,10 +98,38 @@ class SyncManager { return this._cursorToken; } + get queuedCallbacks() { + if(!this._queuedCallbacks) { + this._queuedCallbacks = []; + } + return this._queuedCallbacks; + } + + clearQueuedCallbacks() { + this._queuedCallbacks = []; + } + + callQueuedCallbacksAndCurrent(currentCallback, response) { + var allCallbacks = this.queuedCallbacks; + if(currentCallback) { + allCallbacks.push(currentCallback); + } + if(allCallbacks.length) { + console.log(allCallbacks.length, "queued callbacks"); + for(var eachCallback of allCallbacks) { + eachCallback(response); + } + this.clearQueuedCallbacks(); + } + } + sync(callback, options = {}) { if(this.syncStatus.syncOpInProgress) { this.repeatOnCompletion = true; + if(callback) { + this.queuedCallbacks.push(callback); + } console.log("Sync op in progress; returning."); return; } @@ -118,7 +146,6 @@ class SyncManager { var isContinuationSync = this.needsMoreSync; - this.repeatOnCompletion = false; this.syncStatus.syncOpInProgress = true; let submitLimit = 100; @@ -172,14 +199,17 @@ class SyncManager { this.syncToken = response.sync_token; this.cursorToken = response.cursor_token; - if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) { + if(this.cursorToken || this.needsMoreSync) { setTimeout(function () { this.sync(callback, options); }.bind(this), 10); // wait 10ms to allow UI to update + } else if(this.repeatOnCompletion) { + this.repeatOnCompletion = false; + setTimeout(function () { + this.sync(null, options); + }.bind(this), 10); // wait 10ms to allow UI to update } else { - if(callback) { - callback(response); - } + this.callQueuedCallbacksAndCurrent(callback, response); } }.bind(this)) @@ -193,9 +223,7 @@ class SyncManager { this.$rootScope.$broadcast("sync:error", error); - if(callback) { - callback({error: "Sync error"}); - } + this.callQueuedCallbacksAndCurrent(callback, {error: "Sync error"}); }.bind(this)) } diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index 74efb241a..5ba6cab79 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -124,6 +124,10 @@ font-weight: bold !important; } +.italic { + font-style: italic !important; +} + .normal { font-weight: normal !important; } diff --git a/app/assets/templates/frontend/directives/account-menu.html.haml b/app/assets/templates/frontend/directives/account-menu.html.haml index 62ffe0db9..1db3d3669 100644 --- a/app/assets/templates/frontend/directives/account-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-menu.html.haml @@ -36,6 +36,24 @@ %label.block.mt-5.mb-0 Server password: .wrap.normal.mt-1 {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}} + + %a.block.mt-5{"ng-click" => "newPasswordData.changePassword = !newPasswordData.changePassword"} Change Password + %section.gray-bg.mt-10.medium-padding{"ng-if" => "newPasswordData.changePassword"} + %p.bold Change Password (Beta) + %p.mt-10 Since your encrpytion key is based on your password, changing your password requires all your notes and tags to be re-encrypted using your new key. + %p.mt-5 If you have thousands of items, this can take several minutes — you must keep the application window open during this process. + %p.mt-5 After changing your password, you must log out of all other applications currently signed in to your account. + %p.bold.mt-5 It is highly recommended you download a backup of your data before proceeding. + %div.mt-10 + %a.red.mr-5{"ng-if" => "!newPasswordData.showNewPasswordForm", "ng-click" => "showPasswordChangeForm()"} Continue + %a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showNewPasswordForm = false"} Cancel + %div.mt-10{"ng-if" => "newPasswordData.showNewPasswordForm"} + %form + %input.form-control{"type" => "text", "ng-model" => "newPasswordData.newPassword", "placeholder" => "Enter new password"} + %input.form-control{"type" => "text", "ng-model" => "newPasswordData.newPasswordConfirmation", "placeholder" => "Confirm new password"} + %button.btn.dark-button.btn-block{"ng-click" => "submitPasswordChange()"} Submit + %p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}} + %a.block.mt-5{"href" => "{{dashboardURL()}}", "target" => "_blank"} Standard File Dashboard %div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress", "delay" => "1000"}