From 13e6ac59a90623619ba489a749b3127e124728be Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 24 Jan 2017 20:55:55 -0600 Subject: [PATCH 01/25] sync providers wip --- .../app/frontend/controllers/header.js | 34 ++- .../app/frontend/models/app/extension.js | 6 +- .../app/frontend/models/sync/syncProvider.js | 36 ++++ .../javascripts/app/services/apiController.js | 202 +++++++++++++----- app/assets/stylesheets/app/_header.scss | 12 +- .../templates/frontend/header.html.haml | 46 ++-- 6 files changed, 258 insertions(+), 78 deletions(-) create mode 100644 app/assets/javascripts/app/frontend/models/sync/syncProvider.js diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js index 61ed0f955..2d8164a83 100644 --- a/app/assets/javascripts/app/frontend/controllers/header.js +++ b/app/assets/javascripts/app/frontend/controllers/header.js @@ -71,15 +71,47 @@ angular.module('app.frontend') }) } + this.syncProviderActionIsEnabled = function(action) { + var provider = apiController.syncProviderForURL(action.url); + if(!provider) { + return null; + } + return provider.status; + } + + this.enableSyncProvider = function(action, extension, primary) { + if(extension.encrypted && !extension.ek) { + alert("You must set an encryption key for this extension before enabling this action."); + return; + } + var provider = apiController.findOrCreateSyncProviderForUrl(action.url); + provider.primary = primary; + provider.enabled = true; + provider.ek = extension.ek; + apiController.addSyncProvider(provider); + } + + this.disableSyncProvider = function(action, extension) { + apiController.removeSyncProvider(apiController.syncProviderForURL(action.url)); + } + this.deleteExtension = function(extension) { if(confirm("Are you sure you want to delete this extension?")) { extensionManager.deleteExtension(extension); + var syncProviderAction = extension.syncProviderAction; + if(syncProviderAction) { + apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url)); + } } } this.reloadExtensionsPressed = function() { - if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) { + if(confirm("For your security, reloading extensions will disable any currently enabled sync providers and repeat actions.")) { extensionManager.refreshExtensionsFromServer(); + var syncProviderAction = extension.syncProviderAction; + if(syncProviderAction) { + apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url)); + } } } diff --git a/app/assets/javascripts/app/frontend/models/app/extension.js b/app/assets/javascripts/app/frontend/models/app/extension.js index acd42b4e2..2e44cfd09 100644 --- a/app/assets/javascripts/app/frontend/models/app/extension.js +++ b/app/assets/javascripts/app/frontend/models/app/extension.js @@ -56,9 +56,13 @@ class Extension extends Item { this.content_type = "Extension"; } + syncProviderAction() { + return _.find(this.actions, {sync_provider: true}) + } + actionsInGlobalContext() { return this.actions.filter(function(action){ - return action.context == "global"; + return action.context == "global" || action.sync_provider == true; }) } diff --git a/app/assets/javascripts/app/frontend/models/sync/syncProvider.js b/app/assets/javascripts/app/frontend/models/sync/syncProvider.js new file mode 100644 index 000000000..115ec20b1 --- /dev/null +++ b/app/assets/javascripts/app/frontend/models/sync/syncProvider.js @@ -0,0 +1,36 @@ +class SyncProvider { + constructor(obj) { + _.merge(this, obj); + } + + addPendingItems(items) { + if(!this.pendingItems) { + this.pendingItems = []; + } + + this.pendingItems = this.pendingItems.concat(items); + } + + removePendingItems(items) { + this.pendingItems = _.difference(this.pendingItems, items); + } + + get status() { + if(!this.enabled) { + return null; + } + + if(this.primary) return "primary"; + else return "secondary"; + } + + asJSON() { + return { + enabled: this.enabled, + url: this.url, + encrypted: this.encrypted, + ek: this.ek + } + } + +} diff --git a/app/assets/javascripts/app/services/apiController.js b/app/assets/javascripts/app/services/apiController.js index 7c4e163b1..f7ea39895 100644 --- a/app/assets/javascripts/app/services/apiController.js +++ b/app/assets/javascripts/app/services/apiController.js @@ -201,9 +201,71 @@ angular.module('app.frontend') /* - Items + Sync */ + this.syncProviderForURL = function(url) { + return _.find(this.syncProviders, {url: url}); + } + + this.findOrCreateSyncProviderForUrl = function(url) { + var provider = _.find(this.syncProviders, {url: url}); + if(!provider) { + provider = new SyncProvider({url: url}) + } + return provider; + } + + this.setEncryptionStatusForProviderURL = function(providerURL, encrypted) { + this.providerForURL(providerURL).encrypted = encrypted; + this.persistSyncProviders(); + } + + this.loadSyncProviders = function() { + var providers = []; + var saved = localStorage.getItem("syncProviders"); + if(saved) { + var parsed = JSON.parse(saved); + for(var p of parsed) { + providers.push(new SyncProvider(p)); + } + } else { + // no providers saved, use default + if(this.isUserSignedIn()) { + var defaultProvider = new SyncProvider(this.getServer() + "/items/sync", true); + providers.push(defaultProvider); + } + } + + this.syncProviders = providers; + } + this.loadSyncProviders(); + + this.addSyncProvider = function(syncProvider) { + if(syncProvider.primary) { + for(var provider of this.syncProviders) { + provider.primary = false; + } + } + + // since we're adding a new provider, we need to send it EVERYTHING we have now. + syncProvider.addPendingItems(modelManager.allItems); + + this.syncProviders.push(syncProvider); + this.persistSyncProviders(); + } + + this.removeSyncProvider = function(provider) { + _.pull(this.syncProviders, provider); + this.persistSyncProviders(); + } + + this.persistSyncProviders = function() { + localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) { + return provider.asJSON() + }))); + } + this.setSyncToken = function(syncToken) { this.syncToken = syncToken; localStorage.setItem("syncToken", this.syncToken); @@ -211,14 +273,6 @@ angular.module('app.frontend') this.syncWithOptions = function(callback, options = {}) { - if(this.syncOpInProgress) { - // will perform anoter sync after current completes - this.repeatSync = true; - return; - } - - this.syncOpInProgress = true; - var allDirtyItems = modelManager.getDirtyItems(); // we want to write all dirty items to disk only if the user is not signed in, or if the sync op fails @@ -243,55 +297,94 @@ angular.module('app.frontend') modelManager.clearDirtyItems(allDirtyItems); }.bind(this)) + this.syncOpInProgress = false; - if(callback) { + + if(this.syncProviders.length == 0 && callback) { callback(); } + } + + for(let provider of this.syncProviders) { + if(provider.enabled == false) { + continue; + } + provider.addPendingItems(allDirtyItems); + this.__performSyncWithProvider(provider, options, function(response){ + if(provider.primary) { + if(callback) { + callback(response) + } + } + }) + } + + modelManager.clearDirtyItems(allDirtyItems); + } + + this.__performSyncWithProvider = function(provider, options, callback) { + if(provider.syncOpInProgress) { + provider.repeatOnCompletion = true; + console.log("Sync op in progress for provider; returning.", provider); return; } + + provider.syncOpInProgress = true; + let submitLimit = 100; - var dirtyItems = allDirtyItems.slice(0, submitLimit); - if(dirtyItems.length < allDirtyItems.length) { + var allItems = provider.pendingItems; + var subItems = allItems.slice(0, submitLimit); + if(subItems.length < allItems.length) { // more items left to be synced, repeat - this.repeatSync = true; + provider.repeatOnCompletion = true; } else { - this.repeatSync = false; + provider.repeatOnCompletion = false; } - var request = Restangular.one("items/sync"); + console.log("Syncing with provider", provider, subItems); + + // Remove dirty items now. If this operation fails, we'll re-add them. + // This allows us to queue changes on the same item + provider.removePendingItems(subItems); + + var request = Restangular.oneUrl(provider.url, provider.url); request.limit = 150; - request.sync_token = this.syncToken; - request.cursor_token = this.cursorToken; - request.items = _.map(dirtyItems, function(item){ - return this.createRequestParamsForItem(item, options.additionalFields); + request.items = _.map(subItems, function(item){ + return this.paramsForItem(item, provider.encrypted, provider.ek, options.additionalFields, false); }.bind(this)); + if(provider.primary) { + // only primary providers receive items (or care about received items) + request.sync_token = this.syncToken; + request.cursor_token = provider.cursorToken; + } + request.post().then(function(response) { - modelManager.clearDirtyItems(dirtyItems); + if(provider.primary) { + // handle sync token + this.setSyncToken(response.sync_token); + $rootScope.$broadcast("sync:updated_token", this.syncToken); - // handle sync token - this.setSyncToken(response.sync_token); - $rootScope.$broadcast("sync:updated_token", this.syncToken); + // handle cursor token (more results waiting, perform another sync) + provider.cursorToken = response.cursor_token; - // handle cursor token (more results waiting, perform another sync) - this.cursorToken = response.cursor_token; + var retrieved = this.handleItemsResponse(response.retrieved_items, null, provider); + // merge only metadata for saved items + var omitFields = ["content", "auth_hash"]; + var saved = this.handleItemsResponse(response.saved_items, omitFields, provider); - var retrieved = this.handleItemsResponse(response.retrieved_items, null); - // merge only metadata for saved items - var omitFields = ["content", "auth_hash"]; - var saved = this.handleItemsResponse(response.saved_items, omitFields); + this.handleUnsavedItemsResponse(response.unsaved, provider) - this.handleUnsavedItemsResponse(response.unsaved) + this.writeItemsToLocalStorage(saved, null); + this.writeItemsToLocalStorage(retrieved, null); + } - this.writeItemsToLocalStorage(saved, null); - this.writeItemsToLocalStorage(retrieved, null); + provider.syncOpInProgress = false; - this.syncOpInProgress = false; - - if(this.cursorToken || this.repeatSync == true) { - this.syncWithOptions(callback, options); + if(provider.cursorToken || provider.repeatOnCompletion == true) { + this.__performSyncWithProvider(provider, options, callback); } else { if(callback) { callback(response); @@ -302,8 +395,13 @@ angular.module('app.frontend') .catch(function(response){ console.log("Sync error: ", response); - writeAllDirtyItemsToDisk(); - this.syncOpInProgress = false; + // Re-add subItems since this operation failed. We'll have to try again. + provider.addPendingItems(subItems); + provider.syncOpInProgress = false; + + if(provider.primary) { + this.writeItemsToLocalStorage(allItems, null); + } if(callback) { callback({error: "Sync error"}); @@ -315,7 +413,7 @@ angular.module('app.frontend') this.syncWithOptions(callback, undefined); } - this.handleUnsavedItemsResponse = function(unsaved) { + this.handleUnsavedItemsResponse = function(unsaved, provider) { if(unsaved.length == 0) { return; } @@ -332,19 +430,15 @@ angular.module('app.frontend') } } - this.syncWithOptions(null, {additionalFields: ["created_at", "updated_at"]}); + this.__performSyncWithProvider(provider, {additionalFields: ["created_at", "updated_at"]}, null); } - this.handleItemsResponse = function(responseItems, omitFields) { - this.decryptItems(responseItems); + this.handleItemsResponse = function(responseItems, omitFields, syncProvider) { + this.decryptItemsWithKey(responseItems, syncProvider ? syncProvider.ek : null); return modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); } - this.createRequestParamsForItem = function(item, additionalFields) { - return this.paramsForItem(item, true, additionalFields, false); - } - - this.paramsForExportFile = function(item, encrypted) { + this.paramsForExportFile = function(item, ek, encrypted) { return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]); } @@ -352,15 +446,18 @@ angular.module('app.frontend') return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]); } - this.paramsForItem = function(item, encrypted, additionalFields, forExportFile) { + this.paramsForItem = function(item, encrypted, ek, additionalFields, forExportFile) { var itemCopy = _.cloneDeep(item); + if(encrypted) { + console.assert(ek.length, "Attempting to encrypt without encryption key."); + } console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy) var params = {uuid: item.uuid, content_type: item.content_type, deleted: item.deleted}; if(encrypted) { - this.encryptSingleItem(itemCopy, this.retrieveMk()); + this.encryptSingleItem(itemCopy, ek); params.content = itemCopy.content; params.enc_item_key = itemCopy.enc_item_key; params.auth_hash = itemCopy.auth_hash; @@ -475,7 +572,7 @@ angular.module('app.frontend') this.loadLocalItems = function(callback) { var params = dbManager.getAllItems(function(items){ - var items = this.handleItemsResponse(items, null); + var items = this.handleItemsResponse(items, null, null); Item.sortItemsByDate(items); callback(items); }.bind(this)) @@ -560,11 +657,6 @@ angular.module('app.frontend') item.content = content; } - this.decryptItems = function(items) { - var masterKey = this.retrieveMk(); - this.decryptItemsWithKey(items, masterKey); - } - this.decryptItemsWithKey = function(items, key) { for (var item of items) { if(item.deleted == true) { diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index 5e5e5208f..c888e18d9 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -312,7 +312,7 @@ Extensions font-weight: bold !important; } - > .name { + .extension-name { font-weight: bold; font-size: 16px; margin-bottom: 6px; @@ -331,12 +331,12 @@ Extensions } } - > .subtitle { + .extension-subtitle { font-size: 14px; margin-bottom: 10px; } - > .actions { + .extension-actions { margin-top: 15px; font-size: 12px; @@ -346,18 +346,18 @@ Extensions background-color: rgba(white, 0.9); border: 1px solid rgba(gray, 0.15); - > .name { + .action-name { font-weight: bold; } - > .permissions { + .action-permissions { margin-top: 2px; a { font-weight: normal !important; } } - > .execute { + .execute { font-weight: bold; margin-bottom: 0px; font-size: 12px; diff --git a/app/assets/templates/frontend/header.html.haml b/app/assets/templates/frontend/header.html.haml index 5bdc562cb..ad14a0fee 100644 --- a/app/assets/templates/frontend/header.html.haml +++ b/app/assets/templates/frontend/header.html.haml @@ -103,7 +103,7 @@ %div{"style" => "font-size: 18px;", "ng-if" => "!ctrl.extensionManager.extensions.length"} No extensions installed .registered-extensions{"ng-if" => "ctrl.extensionManager.extensions.length"} .extension{"ng-repeat" => "extension in ctrl.extensionManager.extensions"} - .name {{extension.name}} + .extension-name {{extension.name}} .encryption-format .title Send data: %label @@ -112,21 +112,37 @@ %label %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(false, extension)"} Decrypted - .actions + .encryption-key{"ng-if" => "extension.encrypted"} + %input{"ng-model" => "extension.ek", "placeholder" => "Set encryption key"} + .extension-actions .action{"ng-repeat" => "action in extension.actionsInGlobalContext()"} - .name {{action.label}} - .desc{"style" => "font-style: italic;"} {{action.desc}} - .execute-type{"ng-if" => "action.repeat_mode == 'watch'"} - Repeats when a change is made to your items. - .execute-type{"ng-if" => "action.repeat_mode == 'loop'"} - Repeats at most once every {{action.repeat_timeout}} seconds - .permissions - %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} - %div{"ng-if" => "action.showPermissions"} - {{action.permissionsString}} - .encryption-type - %span {{action.encryptionModeString}} - .execute + %div{"ng-if" => "!action.sync_provider"} + .action-name {{action.label}} + .action-desc{"style" => "font-style: italic;"} {{action.desc}} + .execute-type{"ng-if" => "action.repeat_mode == 'watch'"} + Repeats when a change is made to your items. + .execute-type{"ng-if" => "action.repeat_mode == 'loop'"} + Repeats at most once every {{action.repeat_timeout}} seconds + .action-permissions + %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} + %div{"ng-if" => "action.showPermissions"} + {{action.permissionsString}} + .encryption-type + %span {{action.encryptionModeString}} + %div{"ng-if" => "action.sync_provider"} + .action-name This is a sync provider action. + .action-desc{"style" => "margin-top: -5px;"} + %p Enabling this sync provider as a primary provider will replace your current sync provider. + %p Enabling this sync provider as a secondary provider will save your data with this provider as a backup, but will not be used to pull changes. + %p You can have only one primary provider, and multiple secondary providers. + + %div{"ng-if" => "!ctrl.syncProviderActionIsEnabled(action)"} + .execute{"ng-click" => "ctrl.enableSyncProvider(action, extension, true)"} Enable as primary sync provider + .execute{"ng-click" => "ctrl.enableSyncProvider(action, extension, false)"} Enable as backup sync provider + %div{"ng-if" => "ctrl.syncProviderActionIsEnabled(action)"} + .execute{"ng-click" => "ctrl.disableSyncProvider(action, extension)"} Remove as sync provider + + .execute{"ng-if" => "!action.sync_provider"} %div{"ng-if" => "action.repeat_mode"} %div{"ng-if" => "ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.disableRepeatAction(action, extension)"} Disable %div{"ng-if" => "!ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.enableRepeatAction(action, extension)"} Enable From a76f725f7fa1917510f6242bdd81276c618bcf46 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 25 Jan 2017 15:52:03 -0600 Subject: [PATCH 02/25] keymanager, syncmanager --- app/assets/javascripts/app/app.frontend.js | 3 - .../app/frontend/controllers/header.js | 235 +-------------- .../app/frontend/models/app/extension.js | 2 +- .../app/frontend/models/sync/syncProvider.js | 6 +- .../javascripts/app/services/apiController.js | 198 ++++--------- .../services/directives/accountDataMenu.js | 17 ++ .../services/directives/accountKeysSection.js | 23 ++ .../services/directives/accountSyncSection.js | 36 +++ .../directives/accountVendorAccountSection.js | 113 ++++++++ .../directives/globalExtensionsMenu.js | 109 +++++++ .../services/directives/importExportMenu.js | 79 ++++++ .../app/services/extensionManager.js | 11 + .../javascripts/app/services/keyManager.js | 26 ++ .../javascripts/app/services/syncManager.js | 111 ++++++++ app/assets/stylesheets/app/_directives.scss | 202 ++++++++++++- app/assets/stylesheets/app/_header.scss | 268 ++++++------------ .../directives/account-data-menu.html.haml | 18 ++ .../directives/account-keys-section.html.haml | 11 + .../directives/account-sync-section.html.haml | 22 ++ .../account-vendor-account-section.html.haml | 42 +++ .../global-extensions-menu.html.haml | 82 ++++++ .../directives/import-export-menu.html.haml | 26 ++ .../templates/frontend/header.html.haml | 169 +---------- 23 files changed, 1088 insertions(+), 721 deletions(-) create mode 100644 app/assets/javascripts/app/services/directives/accountDataMenu.js create mode 100644 app/assets/javascripts/app/services/directives/accountKeysSection.js create mode 100644 app/assets/javascripts/app/services/directives/accountSyncSection.js create mode 100644 app/assets/javascripts/app/services/directives/accountVendorAccountSection.js create mode 100644 app/assets/javascripts/app/services/directives/globalExtensionsMenu.js create mode 100644 app/assets/javascripts/app/services/directives/importExportMenu.js create mode 100644 app/assets/javascripts/app/services/keyManager.js create mode 100644 app/assets/javascripts/app/services/syncManager.js create mode 100644 app/assets/templates/frontend/directives/account-data-menu.html.haml create mode 100644 app/assets/templates/frontend/directives/account-keys-section.html.haml create mode 100644 app/assets/templates/frontend/directives/account-sync-section.html.haml create mode 100644 app/assets/templates/frontend/directives/account-vendor-account-section.html.haml create mode 100644 app/assets/templates/frontend/directives/global-extensions-menu.html.haml create mode 100644 app/assets/templates/frontend/directives/import-export-menu.html.haml diff --git a/app/assets/javascripts/app/app.frontend.js b/app/assets/javascripts/app/app.frontend.js index f19dfe4fd..9563cc6fe 100644 --- a/app/assets/javascripts/app/app.frontend.js +++ b/app/assets/javascripts/app/app.frontend.js @@ -20,9 +20,6 @@ angular.module('app.frontend', [ .config(function (RestangularProvider, apiControllerProvider) { RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"}); - var url = apiControllerProvider.defaultServerURL(); - RestangularProvider.setBaseUrl(url + "/api"); - RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) { var token = localStorage.getItem("jwt"); if(token) { diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js index 2d8164a83..f0f3975eb 100644 --- a/app/assets/javascripts/app/frontend/controllers/header.js +++ b/app/assets/javascripts/app/frontend/controllers/header.js @@ -1,5 +1,5 @@ angular.module('app.frontend') - .directive("header", function(apiController, extensionManager){ + .directive("header", function(apiController){ return { restrict: 'E', scope: {}, @@ -16,15 +16,9 @@ angular.module('app.frontend') } } }) - .controller('HeaderCtrl', function ($state, apiController, modelManager, $timeout, extensionManager, dbManager) { + .controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager) { this.user = apiController.user; - this.extensionManager = extensionManager; - this.loginData = {mergeLocal: true}; - - this.changePasswordPressed = function() { - this.showNewPasswordForm = !this.showNewPasswordForm; - } this.accountMenuPressed = function() { this.serverData = {url: apiController.getServer()}; @@ -32,126 +26,19 @@ angular.module('app.frontend') this.showFaq = false; this.showNewPasswordForm = false; this.showExtensionsMenu = false; + this.showIOMenu = false; } this.toggleExtensions = function() { this.showAccountMenu = false; + this.showIOMenu = false; this.showExtensionsMenu = !this.showExtensionsMenu; } - this.toggleExtensionForm = function() { - this.newExtensionData = {}; - this.showNewExtensionForm = !this.showNewExtensionForm; - } - - this.submitNewExtensionForm = function() { - if(this.newExtensionData.url) { - extensionManager.addExtension(this.newExtensionData.url, function(response){ - if(!response) { - alert("Unable to register this extension. Make sure the link is valid and try again."); - } else { - this.newExtensionData.url = ""; - this.showNewExtensionForm = false; - } - }.bind(this)) - } - } - - this.selectedAction = function(action, extension) { - action.running = true; - extensionManager.executeAction(action, extension, null, function(response){ - action.running = false; - if(response && response.error) { - action.error = true; - alert("There was an error performing this action. Please try again."); - } else { - action.error = false; - apiController.sync(null); - } - }) - } - - this.syncProviderActionIsEnabled = function(action) { - var provider = apiController.syncProviderForURL(action.url); - if(!provider) { - return null; - } - return provider.status; - } - - this.enableSyncProvider = function(action, extension, primary) { - if(extension.encrypted && !extension.ek) { - alert("You must set an encryption key for this extension before enabling this action."); - return; - } - var provider = apiController.findOrCreateSyncProviderForUrl(action.url); - provider.primary = primary; - provider.enabled = true; - provider.ek = extension.ek; - apiController.addSyncProvider(provider); - } - - this.disableSyncProvider = function(action, extension) { - apiController.removeSyncProvider(apiController.syncProviderForURL(action.url)); - } - - this.deleteExtension = function(extension) { - if(confirm("Are you sure you want to delete this extension?")) { - extensionManager.deleteExtension(extension); - var syncProviderAction = extension.syncProviderAction; - if(syncProviderAction) { - apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url)); - } - } - } - - this.reloadExtensionsPressed = function() { - if(confirm("For your security, reloading extensions will disable any currently enabled sync providers and repeat actions.")) { - extensionManager.refreshExtensionsFromServer(); - var syncProviderAction = extension.syncProviderAction; - if(syncProviderAction) { - apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url)); - } - } - } - - this.changeServer = function() { - apiController.setServer(this.serverData.url, true); - } - - this.signOutPressed = function() { + this.toggleIO = function() { + this.showIOMenu = !this.showIOMenu; + this.showExtensionsMenu = false; this.showAccountMenu = false; - apiController.signout(function(){ - window.location.reload(); - }) - } - - this.submitPasswordChange = function() { - this.passwordChangeData.status = "Generating New Keys..."; - - $timeout(function(){ - if(data.password != data.password_confirmation) { - alert("Your new password does not match its confirmation."); - return; - } - - apiController.changePassword(this.passwordChangeData.current_password, this.passwordChangeData.new_password, function(response){ - - }) - - }.bind(this)) - } - - this.localNotesCount = function() { - return modelManager.filteredNotes.length; - } - - this.mergeLocalChanged = function() { - if(!this.loginData.mergeLocal) { - if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) { - this.loginData.mergeLocal = true; - } - } } this.refreshData = function() { @@ -171,110 +58,4 @@ angular.module('app.frontend') this.syncUpdated = function() { this.lastSyncDate = new Date(); } - - this.loginSubmitPressed = function() { - this.loginData.status = "Generating Login Keys..."; - - $timeout(function(){ - apiController.login(this.loginData.email, this.loginData.user_password, function(response){ - if(!response || response.error) { - var error = response ? response.error : {message: "An unknown error occured."} - this.loginData.status = null; - if(!response || (response && !response.didDisplayAlert)) { - alert(error.message); - } - } else { - this.onAuthSuccess(response.user); - } - }.bind(this)); - }.bind(this)) - } - - this.submitRegistrationForm = function() { - this.loginData.status = "Generating Account Keys..."; - - $timeout(function(){ - apiController.register(this.loginData.email, this.loginData.user_password, function(response){ - if(!response || response.error) { - var error = response ? response.error : {message: "An unknown error occured."} - this.loginData.status = null; - alert(error.message); - } else { - this.onAuthSuccess(response.user); - } - }.bind(this)); - }.bind(this)) - } - - this.encryptionStatusForNotes = function() { - var allNotes = modelManager.filteredNotes; - return allNotes.length + "/" + allNotes.length + " notes encrypted"; - } - - this.archiveEncryptionFormat = {encrypted: true}; - - this.downloadDataArchive = function() { - var link = document.createElement('a'); - link.setAttribute('download', 'notes.json'); - link.href = apiController.itemsDataFile(this.archiveEncryptionFormat.encrypted); - link.click(); - } - - this.performImport = function(data, password) { - this.importData.loading = true; - // allow loading indicator to come up with timeout - $timeout(function(){ - apiController.importJSONData(data, password, function(success, response){ - console.log("Import response:", success, response); - this.importData.loading = false; - if(success) { - this.importData = null; - } else { - alert("There was an error importing your data. Please try again."); - } - }.bind(this)) - }.bind(this)) - } - - this.submitImportPassword = function() { - this.performImport(this.importData.data, this.importData.password); - } - - this.importFileSelected = function(files) { - this.importData = {}; - - var file = files[0]; - var reader = new FileReader(); - reader.onload = function(e) { - var data = JSON.parse(e.target.result); - $timeout(function(){ - if(data.auth_params) { - // request password - this.importData.requestPassword = true; - this.importData.data = data; - } else { - this.performImport(data, null); - } - }.bind(this)) - }.bind(this) - - reader.readAsText(file); - } - - this.onAuthSuccess = function(user) { - var block = function(){ - window.location.reload(); - this.showLogin = false; - this.showRegistration = false; - }.bind(this); - - if(!this.loginData.mergeLocal) { - dbManager.clearAllItems(function(){ - block(); - }); - } else { - block(); - } - } - - }); +}); diff --git a/app/assets/javascripts/app/frontend/models/app/extension.js b/app/assets/javascripts/app/frontend/models/app/extension.js index 2e44cfd09..36f0a8a54 100644 --- a/app/assets/javascripts/app/frontend/models/app/extension.js +++ b/app/assets/javascripts/app/frontend/models/app/extension.js @@ -56,7 +56,7 @@ class Extension extends Item { this.content_type = "Extension"; } - syncProviderAction() { + get syncProviderAction() { return _.find(this.actions, {sync_provider: true}) } diff --git a/app/assets/javascripts/app/frontend/models/sync/syncProvider.js b/app/assets/javascripts/app/frontend/models/sync/syncProvider.js index 115ec20b1..a21bcce8d 100644 --- a/app/assets/javascripts/app/frontend/models/sync/syncProvider.js +++ b/app/assets/javascripts/app/frontend/models/sync/syncProvider.js @@ -1,5 +1,6 @@ class SyncProvider { constructor(obj) { + this.encrypted = true; _.merge(this, obj); } @@ -28,8 +29,9 @@ class SyncProvider { return { enabled: this.enabled, url: this.url, - encrypted: this.encrypted, - ek: this.ek + primary: this.primary, + keyName: this.keyName, + syncToken: this.syncToken } } diff --git a/app/assets/javascripts/app/services/apiController.js b/app/assets/javascripts/app/services/apiController.js index f7ea39895..a134a31f5 100644 --- a/app/assets/javascripts/app/services/apiController.js +++ b/app/assets/javascripts/app/services/apiController.js @@ -7,24 +7,11 @@ angular.module('app.frontend') return domain; } - var url; - - this.defaultServerURL = function() { - if(!url) { - url = localStorage.getItem("server"); - if(!url) { - url = "https://n3.standardnotes.org"; - } - } - return url; + this.$get = function($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) { + return new ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager); } - - this.$get = function($rootScope, Restangular, modelManager, dbManager) { - return new ApiController($rootScope, Restangular, modelManager, dbManager); - } - - function ApiController($rootScope, Restangular, modelManager, dbManager) { + function ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) { var userData = localStorage.getItem("user"); if(userData) { @@ -36,35 +23,15 @@ angular.module('app.frontend') this.user = {uuid: idData}; } } - this.syncToken = localStorage.getItem("syncToken"); - - /* - Config - */ - - this.getServer = function() { - if(!url) { - url = localStorage.getItem("server"); - if(!url) { - url = "https://n3.standardnotes.org"; - this.setServer(url); - } - } - return url; - } - - this.setServer = function(url, refresh) { - localStorage.setItem("server", url); - if(refresh) { - window.location.reload(); - } - } - /* Auth */ + this.defaultServerURL = function() { + return localStorage.getItem("server") || "https://n3.standardnotes.org"; + } + this.getAuthParams = function() { return JSON.parse(localStorage.getItem("auth_params")); } @@ -73,8 +40,9 @@ angular.module('app.frontend') return localStorage.getItem("jwt"); } - this.getAuthParamsForEmail = function(email, callback) { - var request = Restangular.one("auth", "params"); + this.getAuthParamsForEmail = function(url, email, callback) { + var requestUrl = url + "/auth/params"; + var request = Restangular.oneUrl(requestUrl, requestUrl); request.get({email: email}).then(function(response){ callback(response.plain()); }) @@ -96,10 +64,10 @@ angular.module('app.frontend') } } - this.login = function(email, password, callback) { - this.getAuthParamsForEmail(email, function(authParams){ + this.login = function(url, email, password, callback) { + this.getAuthParamsForEmail(url, email, function(authParams){ if(!authParams) { - callback(null); + callback({error: "Unable to get authentication parameters."}); return; } @@ -113,36 +81,44 @@ angular.module('app.frontend') } Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){ - this.setMk(keys.mk); - var request = Restangular.one("auth/sign_in"); + 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){ + localStorage.setItem("server", url); localStorage.setItem("jwt", response.token); localStorage.setItem("user", JSON.stringify(response.user)); localStorage.setItem("auth_params", JSON.stringify(authParams)); + keyManager.addKey(SNKeyName, mk); + this.addStandardFileSyncProvider(url); callback(response); - }) + }.bind(this)) .catch(function(response){ + console.log("Error logging in", response); callback(response.data); }) }.bind(this)); }.bind(this)) } - this.register = function(email, password, callback) { + this.register = function(url, email, password, callback) { Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){ - this.setMk(keys.mk); - keys.mk = null; - var request = Restangular.one("auth"); + 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){ + localStorage.setItem("server", url); localStorage.setItem("jwt", response.token); localStorage.setItem("user", JSON.stringify(response.user)); localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); + keyManager.addKey(SNKeyName, mk); + this.addStandardFileSyncProvider(url); callback(response); - }) + }.bind(this)) .catch(function(response){ callback(response.data); }) @@ -190,8 +166,9 @@ angular.module('app.frontend') // }.bind(this)); // } - this._performPasswordChange = function(email, current_keys, new_keys, callback) { - var request = Restangular.one("auth"); + 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){ @@ -204,73 +181,6 @@ angular.module('app.frontend') Sync */ - this.syncProviderForURL = function(url) { - return _.find(this.syncProviders, {url: url}); - } - - this.findOrCreateSyncProviderForUrl = function(url) { - var provider = _.find(this.syncProviders, {url: url}); - if(!provider) { - provider = new SyncProvider({url: url}) - } - return provider; - } - - this.setEncryptionStatusForProviderURL = function(providerURL, encrypted) { - this.providerForURL(providerURL).encrypted = encrypted; - this.persistSyncProviders(); - } - - this.loadSyncProviders = function() { - var providers = []; - var saved = localStorage.getItem("syncProviders"); - if(saved) { - var parsed = JSON.parse(saved); - for(var p of parsed) { - providers.push(new SyncProvider(p)); - } - } else { - // no providers saved, use default - if(this.isUserSignedIn()) { - var defaultProvider = new SyncProvider(this.getServer() + "/items/sync", true); - providers.push(defaultProvider); - } - } - - this.syncProviders = providers; - } - this.loadSyncProviders(); - - this.addSyncProvider = function(syncProvider) { - if(syncProvider.primary) { - for(var provider of this.syncProviders) { - provider.primary = false; - } - } - - // since we're adding a new provider, we need to send it EVERYTHING we have now. - syncProvider.addPendingItems(modelManager.allItems); - - this.syncProviders.push(syncProvider); - this.persistSyncProviders(); - } - - this.removeSyncProvider = function(provider) { - _.pull(this.syncProviders, provider); - this.persistSyncProviders(); - } - - this.persistSyncProviders = function() { - localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) { - return provider.asJSON() - }))); - } - - this.setSyncToken = function(syncToken) { - this.syncToken = syncToken; - localStorage.setItem("syncToken", this.syncToken); - } - this.syncWithOptions = function(callback, options = {}) { var allDirtyItems = modelManager.getDirtyItems(); @@ -306,7 +216,7 @@ angular.module('app.frontend') } for(let provider of this.syncProviders) { - if(provider.enabled == false) { + if(!provider.enabled) { continue; } provider.addPendingItems(allDirtyItems); @@ -354,18 +264,17 @@ angular.module('app.frontend') return this.paramsForItem(item, provider.encrypted, provider.ek, options.additionalFields, false); }.bind(this)); - if(provider.primary) { - // only primary providers receive items (or care about received items) - request.sync_token = this.syncToken; - request.cursor_token = provider.cursorToken; - } + request.sync_token = provider.syncToken; + request.cursor_token = provider.cursorToken; request.post().then(function(response) { + console.log("Sync completion", response); + + provider.syncToken = response.sync_token; + if(provider.primary) { - // handle sync token - this.setSyncToken(response.sync_token); - $rootScope.$broadcast("sync:updated_token", this.syncToken); + $rootScope.$broadcast("sync:updated_token", provider.syncToken); // handle cursor token (more results waiting, perform another sync) provider.cursorToken = response.cursor_token; @@ -382,6 +291,7 @@ angular.module('app.frontend') } provider.syncOpInProgress = false; + this.didMakeChangesToSyncProviders(); if(provider.cursorToken || provider.repeatOnCompletion == true) { this.__performSyncWithProvider(provider, options, callback); @@ -434,12 +344,13 @@ angular.module('app.frontend') } this.handleItemsResponse = function(responseItems, omitFields, syncProvider) { - this.decryptItemsWithKey(responseItems, syncProvider ? syncProvider.ek : null); + var ek = syncProvider ? keyManager.keyForName(syncProvider.keyName).key : null; + this.decryptItemsWithKey(responseItems, ek); return modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); } this.paramsForExportFile = function(item, ek, encrypted) { - return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]); + return _.omit(this.paramsForItem(item, encrypted, ek, ["created_at", "updated_at"], true), ["deleted"]); } this.paramsForExtension = function(item, encrypted) { @@ -482,8 +393,10 @@ angular.module('app.frontend') */ this.clearSyncToken = function() { - this.syncToken = null; - localStorage.removeItem("syncToken"); + var primary = this.primarySyncProvider(); + if(primary) { + primary.syncToken = null; + } } this.importJSONData = function(data, password, callback) { @@ -526,7 +439,7 @@ angular.module('app.frontend') Export */ - this.itemsDataFile = function(encrypted) { + this.itemsDataFile = function(encrypted, custom_ek) { var textFile = null; var makeTextFile = function (text) { var data = new Blob([text], {type: 'text/json'}); @@ -543,15 +456,21 @@ angular.module('app.frontend') return textFile; }.bind(this); + var ek = custom_ek; + if(encrypted && !custom_ek) { + ek = this.retrieveMk(); + } + var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){ - return this.paramsForExportFile(item, encrypted); + return this.paramsForExportFile(item, ek, encrypted); }.bind(this)); var data = { items: items } - if(encrypted) { + if(encrypted && !custom_ek) { + // auth params are only needed when encrypted with a standard file key data["auth_params"] = this.getAuthParams(); } @@ -616,7 +535,8 @@ angular.module('app.frontend') localStorage.setItem('mk', mk); } - this.signout = function(callback) { + this.signoutOfStandardFile = function(callback) { + this.removeStandardFileSyncProvider(); dbManager.clearAllItems(function(){ localStorage.clear(); callback(); diff --git a/app/assets/javascripts/app/services/directives/accountDataMenu.js b/app/assets/javascripts/app/services/directives/accountDataMenu.js new file mode 100644 index 000000000..91a032c2a --- /dev/null +++ b/app/assets/javascripts/app/services/directives/accountDataMenu.js @@ -0,0 +1,17 @@ +class AccountDataMenu { + + constructor() { + this.restrict = "E"; + this.templateUrl = "frontend/directives/account-data-menu.html"; + this.scope = { + }; + } + + controller($scope, apiController, modelManager) { + 'ngInject'; + + + } +} + +angular.module('app.frontend').directive('accountDataMenu', () => new AccountDataMenu); diff --git a/app/assets/javascripts/app/services/directives/accountKeysSection.js b/app/assets/javascripts/app/services/directives/accountKeysSection.js new file mode 100644 index 000000000..f1d30d1fe --- /dev/null +++ b/app/assets/javascripts/app/services/directives/accountKeysSection.js @@ -0,0 +1,23 @@ +class AccountKeysSection { + + constructor() { + this.restrict = "E"; + this.templateUrl = "frontend/directives/account-keys-section.html"; + this.scope = { + }; + } + + controller($scope, apiController, keyManager) { + 'ngInject'; + + $scope.newKeyData = {}; + $scope.keys = keyManager.keys; + + $scope.submitNewKeyForm = function() { + keyManager.addKey($scope.newKeyData.name, $scope.newKeyData.key); + $scope.newKeyData.showForm = false; + } + } +} + +angular.module('app.frontend').directive('accountKeysSection', () => new AccountKeysSection); diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js new file mode 100644 index 000000000..e2c5fb08d --- /dev/null +++ b/app/assets/javascripts/app/services/directives/accountSyncSection.js @@ -0,0 +1,36 @@ +class AccountSyncSection { + + constructor() { + this.restrict = "E"; + this.templateUrl = "frontend/directives/account-sync-section.html"; + this.scope = { + }; + } + + controller($scope, apiController, modelManager, keyManager) { + 'ngInject'; + + $scope.syncProviders = apiController.syncProviders; + $scope.newSyncData = {showAddSyncForm: false} + $scope.keys = keyManager.keys; + + $scope.submitExternalSyncURL = function() { + apiController.addSyncProviderFromURL($scope.newSyncData.url); + $scope.newSyncData.showAddSyncForm = false; + } + + $scope.enableSyncProvider = function(provider, primary) { + if(!provider.keyName) { + alert("You must choose an encryption key for this provider before enabling it."); + return; + } + apiController.enableSyncProvider(provider, primary); + } + + $scope.removeSyncProvider = function(provider) { + apiController.removeSyncProvider(provider); + } + } +} + +angular.module('app.frontend').directive('accountSyncSection', () => new AccountSyncSection); diff --git a/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js b/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js new file mode 100644 index 000000000..0b73f6c5e --- /dev/null +++ b/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js @@ -0,0 +1,113 @@ +class AccountVendorAccountSection { + + constructor() { + this.restrict = "E"; + this.templateUrl = "frontend/directives/account-vendor-account-section.html"; + this.scope = { + }; + } + + controller($scope, apiController, modelManager, $timeout, dbManager) { + 'ngInject'; + + $scope.loginData = {mergeLocal: true, url: apiController.defaultServerURL()}; + $scope.user = apiController.user; + + $scope.changePasswordPressed = function() { + $scope.showNewPasswordForm = !$scope.showNewPasswordForm; + } + + $scope.signOutPressed = function() { + $scope.showAccountMenu = false; + apiController.signoutOfStandardFile(function(){ + window.location.reload(); + }) + } + + $scope.submitPasswordChange = function() { + $scope.passwordChangeData.status = "Generating New Keys..."; + + $timeout(function(){ + if(data.password != data.password_confirmation) { + alert("Your new password does not match its confirmation."); + return; + } + + apiController.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){ + + }) + + }) + } + + $scope.localNotesCount = function() { + return modelManager.filteredNotes.length; + } + + $scope.mergeLocalChanged = function() { + if(!$scope.loginData.mergeLocal) { + if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) { + $scope.loginData.mergeLocal = true; + } + } + } + + $scope.loginSubmitPressed = function() { + $scope.loginData.status = "Generating Login Keys..."; + console.log("logging in with url", $scope.loginData.url); + $timeout(function(){ + apiController.login($scope.loginData.url, $scope.loginData.email, $scope.loginData.user_password, function(response){ + if(!response || response.error) { + var error = response ? response.error : {message: "An unknown error occured."} + $scope.loginData.status = null; + if(!response || (response && !response.didDisplayAlert)) { + alert(error.message); + } + } else { + $scope.onAuthSuccess(response.user); + } + }); + }) + } + + $scope.submitRegistrationForm = function() { + $scope.loginData.status = "Generating Account Keys..."; + + $timeout(function(){ + apiController.register($scope.loginData.url, $scope.loginData.email, $scope.loginData.user_password, function(response){ + if(!response || response.error) { + var error = response ? response.error : {message: "An unknown error occured."} + $scope.loginData.status = null; + alert(error.message); + } else { + $scope.onAuthSuccess(response.user); + } + }); + }) + } + + $scope.encryptionStatusForNotes = function() { + var allNotes = modelManager.filteredNotes; + return allNotes.length + "/" + allNotes.length + " notes encrypted"; + } + + $scope.onAuthSuccess = function(user) { + var block = function(){ + window.location.reload(); + $scope.showLogin = false; + $scope.showRegistration = false; + }; + + if(!$scope.loginData.mergeLocal) { + dbManager.clearAllItems(function(){ + block(); + }); + } else { + block(); + } + } + + } +} + +angular.module('app.frontend').directive('accountVendorAccountSection', () => new AccountVendorAccountSection); diff --git a/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js new file mode 100644 index 000000000..579471118 --- /dev/null +++ b/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js @@ -0,0 +1,109 @@ +class GlobalExtensionsMenu { + + constructor() { + this.restrict = "E"; + this.templateUrl = "frontend/directives/global-extensions-menu.html"; + this.scope = { + }; + } + + controller($scope, apiController, extensionManager) { + 'ngInject'; + + $scope.extensionManager = extensionManager; + + $scope.toggleExtensionForm = function() { + $scope.newExtensionData = {}; + $scope.showNewExtensionForm = !$scope.showNewExtensionForm; + } + + $scope.submitNewExtensionForm = function() { + if($scope.newExtensionData.url) { + extensionManager.addExtension($scope.newExtensionData.url, function(response){ + if(!response) { + alert("Unable to register this extension. Make sure the link is valid and try again."); + } else { + $scope.newExtensionData.url = ""; + $scope.showNewExtensionForm = false; + } + }) + } + } + + $scope.selectedAction = function(action, extension) { + action.running = true; + extensionManager.executeAction(action, extension, null, function(response){ + action.running = false; + if(response && response.error) { + action.error = true; + alert("There was an error performing this action. Please try again."); + } else { + action.error = false; + apiController.sync(null); + } + }) + } + + $scope.changeExtensionEncryptionFormat = function(encrypted, extension) { + var provider = $scope.syncProviderForExtension(extension); + if(provider) { + if(confirm("Changing encryption status will update all your items and re-sync them back to the server. This can take several minutes. Are you sure you want to continue?")) { + extensionManager.changeExtensionEncryptionFormat(encrypted, extension); + apiController.resyncAllDataForProvider(provider); + } else { + // revert setting + console.log("reverting"); + extension.encrypted = extensionManager.extensionUsesEncryptedData(extension); + } + } else { + extensionManager.changeExtensionEncryptionFormat(encrypted, extension); + } + } + + $scope.deleteExtension = function(extension) { + if(confirm("Are you sure you want to delete this extension?")) { + extensionManager.deleteExtension(extension); + var syncProviderAction = extension.syncProviderAction; + if(syncProviderAction) { + apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url)); + } + } + } + + $scope.reloadExtensionsPressed = function() { + if(confirm("For your security, reloading extensions will disable any currently enabled sync providers and repeat actions.")) { + extensionManager.refreshExtensionsFromServer(); + var syncProviderAction = extension.syncProviderAction; + if(syncProviderAction) { + apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url)); + } + } + } + + $scope.setEncryptionKeyForExtension = function(extension) { + extension.formData.changingKey = false; + var ek = extension.formData.ek; + extensionManager.setEkForExtension(extension, ek); + if(extension.formData.changingKey) { + var syncAction = extension.syncProviderAction; + if(syncAction) { + var provider = apiController.syncProviderForURL(syncAction.url); + provider.ek = ek; + apiController.didMakeChangesToSyncProviders(); + apiController.resyncAllDataForProvider(provider); + } + } + } + + $scope.changeEncryptionKeyPressed = function(extension) { + if(!confirm("Changing your encryption key will re-encrypt all your notes with the new key and sync them back to the server. This can take several minutes. We strongly recommend downloading a backup of your notes before continuing.")) { + return; + } + + extension.formData.changingKey = true; + } + } + +} + +angular.module('app.frontend').directive('globalExtensionsMenu', () => new GlobalExtensionsMenu); diff --git a/app/assets/javascripts/app/services/directives/importExportMenu.js b/app/assets/javascripts/app/services/directives/importExportMenu.js new file mode 100644 index 000000000..59654399a --- /dev/null +++ b/app/assets/javascripts/app/services/directives/importExportMenu.js @@ -0,0 +1,79 @@ +class ImportExportMenu { + + constructor() { + this.restrict = "E"; + this.templateUrl = "frontend/directives/import-export-menu.html"; + this.scope = { + }; + } + + controller($scope, apiController, $timeout) { + 'ngInject'; + + $scope.archiveFormData = {encryption_type: $scope.user ? 'mk' : 'ek'}; + $scope.user = apiController.user; + + $scope.downloadDataArchive = function() { + if(!apiController.isUserSignedIn() && $scope.archiveFormData.encryption_type == 'ek') { + if(!$scope.archiveFormData.ek) { + alert("You must set an encryption key to export the data encrypted.") + return; + } + } + + var link = document.createElement('a'); + link.setAttribute('download', 'notes.json'); + + var ek = $scope.archiveFormData.encryption_type == 'ek' ? $scope.archiveFormData.ek : null; + var encrypted = $scope.archiveFormData.encryption_type != 'none'; + + link.href = apiController.itemsDataFile(encrypted, ek); + link.click(); + } + + $scope.performImport = function(data, password) { + $scope.importData.loading = true; + // allow loading indicator to come up with timeout + $timeout(function(){ + apiController.importJSONData(data, password, function(success, response){ + console.log("Import response:", success, response); + $scope.importData.loading = false; + if(success) { + $scope.importData = null; + } else { + alert("There was an error importing your data. Please try again."); + } + }) + }) + } + + $scope.submitImportPassword = function() { + $scope.performImport($scope.importData.data, $scope.importData.password); + } + + $scope.importFileSelected = function(files) { + $scope.importData = {}; + + var file = files[0]; + var reader = new FileReader(); + reader.onload = function(e) { + var data = JSON.parse(e.target.result); + $timeout(function(){ + if(data.auth_params) { + // request password + $scope.importData.requestPassword = true; + $scope.importData.data = data; + } else { + $scope.performImport(data, null); + } + }) + } + + reader.readAsText(file); + } + + } + +} + +angular.module('app.frontend').directive('importExportMenu', () => new ImportExportMenu); diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index 4be4ecff0..28f9710b4 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -6,6 +6,7 @@ class ExtensionManager { this.apiController = apiController; this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || []; this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || []; + this.extensionEks = JSON.parse(localStorage.getItem("extensionEks")) || {}; modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){ for (var ext of items) { @@ -31,6 +32,15 @@ class ExtensionManager { }) } + ekForExtension(extension) { + return this.extensionEks[extension.url]; + } + + setEkForExtension(extension, ek) { + this.extensionEks[extension.url] = ek; + localStorage.setItem("extensionEks", JSON.stringify(this.extensionEks)); + } + actionWithURL(url) { for (var extension of this.extensions) { return _.find(extension.actions, {url: url}) @@ -42,6 +52,7 @@ class ExtensionManager { } changeExtensionEncryptionFormat(encrypted, extension) { + console.log("changing encryption status"); if(encrypted) { _.pull(this.decryptedExtensions, extension.url); } else { diff --git a/app/assets/javascripts/app/services/keyManager.js b/app/assets/javascripts/app/services/keyManager.js new file mode 100644 index 000000000..8a874ecf4 --- /dev/null +++ b/app/assets/javascripts/app/services/keyManager.js @@ -0,0 +1,26 @@ +class KeyManager { + + constructor() { + this.keys = JSON.parse(localStorage.getItem("keys")) || []; + } + + addKey(name, key) { + this.keys.push({name: name, key: key}); + this.persist(); + } + + keyForName(name) { + return _.find(this.keys, {name: name}); + } + + deleteKey(name) { + _.pull(this.keys, {name: name}); + this.persist(); + } + + persist() { + localStorage.setItem("keys", JSON.stringify(this.keys)); + } +} + +angular.module('app.frontend').service('keyManager', KeyManager); diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js new file mode 100644 index 000000000..bfa5de223 --- /dev/null +++ b/app/assets/javascripts/app/services/syncManager.js @@ -0,0 +1,111 @@ +class SyncManager { + + let SNKeyName = "Standard Notes Key"; + + constructor(modelManager) { + this.modelManager = modelManager; + } + + syncProviderForURL(url) { + var provider = _.find(this.syncProviders, {url: url}); + return provider; + } + + findOrCreateSyncProviderForUrl(url) { + var provider = _.find(this.syncProviders, {url: url}); + if(!provider) { + provider = new SyncProvider({url: url}) + } + return provider; + } + + setEncryptionStatusForProviderURL(providerURL, encrypted) { + this.providerForURL(providerURL).encrypted = encrypted; + this.didMakeChangesToSyncProviders(); + } + + primarySyncProvider() { + return _.find(this.syncProviders, {primary: true}); + } + + removeStandardFileSyncProvider() { + var sfProvider = _.find(this.syncProviders, {url: this.defaultServerURL() + "/items/sync"}) + _.pull(this.syncProviders, sfProvider); + this.didMakeChangesToSyncProviders(); + } + + addStandardFileSyncProvider(url) { + var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.syncProviders.length == 0}); + defaultProvider.keyName = SNKeyName; + defaultProvider.enabled = this.syncProviders.length == 0; + this.syncProviders.push(defaultProvider); + return defaultProvider; + } + + didMakeChangesToSyncProviders() { + localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) { + return provider.asJSON() + }))); + } + + loadSyncProviders() { + this.syncProviders = []; + var saved = localStorage.getItem("syncProviders"); + if(saved) { + var parsed = JSON.parse(saved); + for(var p of parsed) { + this.syncProviders.push(new SyncProvider(p)); + } + } else { + // no providers saved, use default + if(this.isUserSignedIn()) { + var defaultProvider = this.addStandardFileSyncProvider(this.defaultServerURL()); + defaultProvider.syncToken = localStorage.getItem("syncToken"); + // migrate old key structure to new + var mk = localStorage.getItem("mk"); + if(mk) { + keyManager.addKey(SNKeyName, mk); + localStorage.removeItem("mk"); + } + this.didMakeChangesToSyncProviders(); + } + } + } + + this.loadSyncProviders(); + + addSyncProviderFromURL(url) { + var provider = new SyncProvider({url: url}); + this.syncProviders.push(provider); + this.didMakeChangesToSyncProviders(); + } + + enableSyncProvider(syncProvider, primary) { + if(primary) { + for(var provider of this.syncProviders) { + provider.primary = false; + } + } + + syncProvider.enabled = true; + syncProvider.primary = primary; + + // since we're enabling a new provider, we need to send it EVERYTHING we have now. + syncProvider.addPendingItems(this.modelManager.allItems); + this.didMakeChangesToSyncProviders(); + } + + resyncAllDataForProvider(syncProvider) { + syncProvider.addPendingItems(this.modelManager.allItems); + this.sync(); + } + + removeSyncProvider(provider) { + _.pull(this.syncProviders, provider); + this.didMakeChangesToSyncProviders(); + } + + +} + +angular.module('app.frontend').service('syncManager', SyncManager); diff --git a/app/assets/stylesheets/app/_directives.scss b/app/assets/stylesheets/app/_directives.scss index 4ca0da65b..6037d8051 100644 --- a/app/assets/stylesheets/app/_directives.scss +++ b/app/assets/stylesheets/app/_directives.scss @@ -64,9 +64,207 @@ margin-top: 1px; font-size: 12px; } - } - + } + } + } +} + + +/** +Extensions +*/ + +.extensions-panel { + font-size: 14px; + + .extension-link { + margin-top: 8px; + + a { + color: $blue-color !important; + font-weight: bold; + } + } +} + +.extension-form { + margin-top: 8px; +} + +.registered-extensions { + + .extension { + margin-bottom: 18px; + background-color: #f6f6f6; + border: 1px solid #f2f2f2; + padding: 14px 6px; + padding-bottom: 8px; + color: black; + + .ek-input-wrapper { + text-align: center; + margin-top: 14px; + height: 30px; + + > input { + height: 100%; + border: 1px solid rgba(gray, 0.15); + padding: 5px; + width: 78%; + margin-right: 0; + } + + > button { + width: 20% !important; + height: 100% !important; + display: inline-block !important; + margin: 0 !important; + } + } + + a.option-link { + margin-top: 6px; + display: block; + text-align: center; + } + + .show-ek { + > .ek { + font-weight: bold; + margin-top: 2px; + } + + > .disclaimer { + margin-top: 2px; + font-size: 10px; + font-style: italic; + width: 60%; + margin-left: auto; + margin-right: auto; + } + } + + a { + color: $blue-color !important; + font-size: 12px !important; + font-weight: bold !important; + } + + .extension-name { + font-weight: bold; + font-size: 16px; + margin-bottom: 6px; + text-align: center; + } + + .encryption-format { + margin-top: 4px; + font-size: 12px; + text-align: center; + + > .title { + font-size: 13px; + // font-weight: bold; + margin-bottom: 2px; + } + } + + .extension-subtitle { + font-size: 14px; + margin-bottom: 10px; + } + + .extension-actions { + margin-top: 15px; + font-size: 12px; + + .action { + padding: 13px; + margin-bottom: 10px; + background-color: rgba(white, 0.9); + border: 1px solid rgba(gray, 0.15); + + .action-name { + font-weight: bold; + } + + .action-permissions { + margin-top: 2px; + a { + font-weight: normal !important; + } + } + + > .execute-type { + font-size: 12px; + margin-bottom: 1px; + } + + > .error { + color: red; + margin-top: 6px; + } + + > .last-run { + opacity: 0.5; + font-size: 11px; + margin-top: 6px; + } + } + + } + } +} + +.account-data-menu { + padding: 5px !important; + + .providers { + font-size: 12px; + + .provider { + background-color: #f6f6f6; + border: 1px solid #f2f2f2; + padding: 10px 10px; + padding-bottom: 8px; + margin-bottom: 10px; + + > .type { + font-weight: bold; + } + + > .url { + // white-space: nowrap; + // text-overflow: ellipsis; + // overflow: hidden; + word-wrap: break-word; + margin-top: 4px; + } + + > .options { + margin-top: 10px; + } + } + } +} + +.account-keys-section { + .keys { + .key { + background-color: #f6f6f6; + border: 1px solid #f2f2f2; + padding: 10px 10px; + padding-bottom: 8px; + + > .name { + font-size: 13px; + font-weight: bold; + margin-bottom: 2px; + } + + > .value { + word-wrap: break-word; } } } diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index c888e18d9..6d71631c9 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -12,6 +12,7 @@ a { color: $dark-gray; } + } .header-content { @@ -50,50 +51,99 @@ .login-panel .login-input { border-radius: 0px; } +} - .items { +.items { - .item { + .item { - display: inline-block; - margin-right: 7px; - position: relative; - cursor: pointer; - font-weight: bold; + display: inline-block; + margin-right: 7px; + position: relative; + cursor: pointer; + font-weight: bold; - a { - color: #515263; - } + a { + color: #515263; + } - .panel { - position: absolute; - right: 0px; - bottom: 20px; - min-width: 300px; - z-index: 1000; - margin-top: 15px; - box-shadow: 0px 0px 15px rgba(black, 0.2); - border: none; - cursor: default; - max-height: 85vh; - overflow: auto; + .panel { + position: absolute; + right: 0px; + bottom: 20px; + min-width: 300px; + z-index: 1000; + margin-top: 15px; + box-shadow: 0px 0px 15px rgba(black, 0.2); + border: none; + cursor: default; + max-height: 85vh; + overflow: auto; + background-color: white; + font-weight: normal; + + section { + margin-bottom: 10px; + } + + h3 { + font-weight: bold; + margin-top: 0px; + margin-bottom: 4px; + font-size: 16px; + } + + a { + font-size: 12px; + color: $blue-color; + font-weight: bold; + } + + button.light { + font-weight: bold; + margin-bottom: 0px; + font-size: 12px; + height: 30px; + padding-top: 3px; + text-align: center; + margin-bottom: 6px; background-color: white; - font-weight: normal; + display: block; + width: 100%; + border: 1px solid rgba(gray, 0.15); + cursor: pointer; + color: $blue-color; - - .storage-text { - font-size: 14px; + &:hover { + background-color: rgba(gray, 0.10); } - .checkbox { - font-size: 14px; - font-weight: normal; + .execution-spinner { margin-left: auto; margin-right: auto; + text-align: center; + margin-top: 3px; } - } - } - } + } + + .storage-text { + font-size: 14px; + } + + .checkbox { + font-size: 14px; + font-weight: normal; + margin-left: auto; + margin-right: auto; + } + } + } +} + +.item.io { + .enc-option { + display: block; + } } .half-button { @@ -127,11 +177,7 @@ .link-item { margin-bottom: 8px; - a { - font-size: 12px; - color: $blue-color; - font-weight: bold; - } + } input { @@ -157,17 +203,12 @@ cursor: pointer; } - > .icon-container { - display: block; - margin-bottom: 10px; - } - - > .meta-container { + .meta-container { display: block; font-size: 10px; } - > .action-container { + .action-container { font-size: 12px; margin-top: 6px; @@ -209,16 +250,6 @@ margin-bottom: 8px !important; } - > .icon-container { - margin-bottom: 10px; - .icon { - height: 35px; - &.archive { - height: 30px; - } - } - } - .meta-container { > .title { font-size: 13px; @@ -273,132 +304,3 @@ a.disabled { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } - -/** -Extensions -*/ - -.extensions-panel { - font-size: 14px; - - .extension-link { - margin-top: 8px; - - a { - color: $blue-color !important; - font-weight: bold; - } - } -} - -.extension-form { - margin-top: 8px; -} - -.registered-extensions { - - - .extension { - margin-bottom: 18px; - background-color: #f6f6f6; - border: 1px solid #f2f2f2; - padding: 14px 6px; - padding-bottom: 8px; - color: black; - - a { - color: $blue-color !important; - font-size: 12px !important; - font-weight: bold !important; - } - - .extension-name { - font-weight: bold; - font-size: 16px; - margin-bottom: 6px; - text-align: center; - } - - .encryption-format { - margin-top: 4px; - font-size: 12px; - text-align: center; - - > .title { - font-size: 13px; - // font-weight: bold; - margin-bottom: 2px; - } - } - - .extension-subtitle { - font-size: 14px; - margin-bottom: 10px; - } - - .extension-actions { - margin-top: 15px; - font-size: 12px; - - .action { - padding: 13px; - margin-bottom: 10px; - background-color: rgba(white, 0.9); - border: 1px solid rgba(gray, 0.15); - - .action-name { - font-weight: bold; - } - - .action-permissions { - margin-top: 2px; - a { - font-weight: normal !important; - } - } - - .execute { - font-weight: bold; - margin-bottom: 0px; - font-size: 12px; - height: 30px; - padding-top: 7px; - text-align: center; - margin-top: 6px; - border: 1px solid rgba(gray, 0.15); - cursor: pointer; - color: $blue-color; - - &:hover { - background-color: rgba(gray, 0.10); - } - - .execution-spinner { - margin-left: auto; - margin-right: auto; - text-align: center; - margin-top: 3px; - } - } - - > .execute-type { - font-size: 12px; - margin-bottom: 1px; - } - - > .error { - color: red; - margin-top: 6px; - } - - > .last-run { - opacity: 0.5; - font-size: 11px; - margin-top: 6px; - } - } - - } - } - -} diff --git a/app/assets/templates/frontend/directives/account-data-menu.html.haml b/app/assets/templates/frontend/directives/account-data-menu.html.haml new file mode 100644 index 000000000..d4d7447a8 --- /dev/null +++ b/app/assets/templates/frontend/directives/account-data-menu.html.haml @@ -0,0 +1,18 @@ +.panel.panel-default.account-panel.panel-right.account-data-menu + .panel-body + .account-items + %section.account-item + %h3{"ng-click" => "showSN = !showSN"} Standard Notes Account + %account-vendor-account-section{"ng-if" => "showSN"} + + %section.account-item + %h3{"ng-click" => "showSync = !showSync"} Sync Providers + %account-sync-section{"ng-if" => "showSync"} + + %section.account-item + %h3{"ng-click" => "showKeys = !showKeys"} Encryption Keys + %account-keys-section{"ng-if" => "showKeys"} + + %section.account-item + %h3{"ng-click" => "showIO = !showIO"} Import/Export + %import-export-menu{"ng-if" => "showIO"} diff --git a/app/assets/templates/frontend/directives/account-keys-section.html.haml b/app/assets/templates/frontend/directives/account-keys-section.html.haml new file mode 100644 index 000000000..bdaca70da --- /dev/null +++ b/app/assets/templates/frontend/directives/account-keys-section.html.haml @@ -0,0 +1,11 @@ +.account-keys-section + .keys + .key{"ng-repeat" => "key in keys"} + .name {{key.name}} + .value {{key.key}} + + %a{"ng-click" => "newKeyData.showForm = !newKeyData.showForm"} Add New Key + %form{"ng-if" => "newKeyData.showForm"} + %input{"ng-model" => "newKeyData.name", "placeholder" => "Name your key"} + %input{"ng-model" => "newKeyData.key", "placeholder" => "Key"} + %button.light{"ng-click" => "submitNewKeyForm()"} Add Key diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml new file mode 100644 index 000000000..b99e52fc4 --- /dev/null +++ b/app/assets/templates/frontend/directives/account-sync-section.html.haml @@ -0,0 +1,22 @@ +.providers + .provider{"ng-repeat" => "provider in syncProviders"} + .type {{provider.primary == null ? 'Not enabled' : (provider.primary ? 'Primary' : 'Secondary')}} + .key{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} + .url {{provider.url}} + .options + %div{"ng-if" => "!provider.enabled"} + %strong Choose encryption key: + %select{"ng-model" => "provider.keyName"} + %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.keyName}}", "value" => "{{key.name}}"} + {{key.name}} + %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Enable as Primary sync provider + %button.light{"ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider + %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider + %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key + + %a{"ng-click" => "newSyncData.showAddSyncForm = !newSyncData.showAddSyncForm"} Add external sync with Secret URL + %form.sync-form{"ng-if" => "newSyncData.showAddSyncForm"} + .form-tag.has-feedback + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'newSyncData.url'} + %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} + Add External Sync diff --git a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml new file mode 100644 index 000000000..3b0ac24de --- /dev/null +++ b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml @@ -0,0 +1,42 @@ +.registration-login + %div{"ng-if" => "user"} + .email {{user.email}} + .server {{serverURL}} + .links{"ng-if" => "user"} + .link-item + %a{"ng-click" => "signOutPressed()"} Sign Out + .meta-container + .title Local Encryption + .desc Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes. + .action-container + %span.status-title Status: + {{encryptionStatusForNotes()}} + %div{"ng-if" => "!user"} + .meta-container + .title Sign in or Register (optional) + .desc Enter your Standard File account information. + .action-container + %form.account-form{'name' => "loginForm"} + .form-tag.has-feedback + %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'loginData.url'} + .form-tag.has-feedback + %input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'loginData.email'} + .form-tag.has-feedback + %input.form-control.login-input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.user_password'} + .checkbox{"ng-if" => "localNotesCount() > 0"} + %label + %input{"type" => "checkbox", "ng-model" => "loginData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} + Merge local notes ({{localNotesCount()}} notes) + %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span Sign In + %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span Register + %br + .login-forgot{"style" => "padding-top: 4px;"} + %a.btn.btn-link{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten. + .panel-status-text{"ng-if" => "loginData.status", "style" => "font-size: 14px;"} {{loginData.status}} + + %div{"ng-if" => "showResetForm"} + %p{"style" => "font-size: 13px; text-align: center;"} + Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password. + For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password. diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml new file mode 100644 index 000000000..efa0873ad --- /dev/null +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -0,0 +1,82 @@ +.panel.panel-default.account-panel.panel-right.extensions-panel + .panel-body + %div{"style" => "font-size: 18px;", "ng-if" => "!extensionManager.extensions.length"} No extensions installed + .registered-extensions{"ng-if" => "extensionManager.extensions.length"} + .extension{"ng-repeat" => "extension in extensionManager.extensions", "ng-init" => "extension.formData = {}"} + .extension-name {{extension.name}} + .encryption-format + .title Send data: + %label + %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension)"} + Encrypted + %label + %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension)"} + Decrypted + .ek-input-wrapper{"ng-if" => "extension.encrypted && (!extensionManager.ekForExtension(extension) || extension.formData.changingKey)"} + %input{"ng-model" => "extension.formData.ek", "placeholder" => "Set encryption key"} + %button.light{"ng-click" => "setEncryptionKeyForExtension(extension)"} Set + .extension-actions + .action{"ng-repeat" => "action in extension.actionsInGlobalContext()"} + %div{"ng-if" => "!action.sync_provider"} + .action-name {{action.label}} + .action-desc{"style" => "font-style: italic;"} {{action.desc}} + .execute-type{"ng-if" => "action.repeat_mode == 'watch'"} + Repeats when a change is made to your items. + .execute-type{"ng-if" => "action.repeat_mode == 'loop'"} + Repeats at most once every {{action.repeat_timeout}} seconds + .action-permissions + %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} + %div{"ng-if" => "action.showPermissions"} + {{action.permissionsString}} + .encryption-type + %span {{action.encryptionModeString}} + %div{"ng-if" => "action.sync_provider"} + .action-name This is a sync provider action. + .action-desc{"style" => "margin-top: -5px;"} + %p Enabling this sync provider as a primary provider will replace your current sync provider. + %p Enabling this sync provider as a secondary provider will save your data with this provider as a backup, but will not be used to pull changes. + %p You can have only one primary provider, and multiple secondary providers. + + %div{"ng-if" => "!syncProviderActionIsEnabled(action)"} + %button.light.execute{"ng-click" => "enableSyncProvider(action, extension, true)"} Enable as primary sync provider + %button.light.execute{"ng-click" => "enableSyncProvider(action, extension, false)"} Enable as backup sync provider + %div{"ng-if" => "syncProviderActionIsEnabled(action)"} + %button.light.execute{"ng-click" => "disableSyncProvider(action, extension)"} Remove as sync provider + + .execute{"ng-if" => "!action.sync_provider"} + %div{"ng-if" => "action.repeat_mode"} + %div{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension)"} Disable + %div{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable + %div{"ng-if" => "!action.repeat_mode", "ng-click" => "selectedAction(action, extension)"} + %div{"ng-if" => "!action.running"} + Perform Action + %div{"ng-if" => "action.running"} + .spinner.execution-spinner + .last-run{"ng-if" => "!action.error && action.lastExecuted && !action.running"} + Last run {{action.lastExecuted | appDateTime}} + .error{"ng-if" => "action.error"} + Error performing action. + + + %a.option-link{"ng-if" => "extension.encrypted && extensionManager.ekForExtension(extension) && !extension.formData.changingKey", "ng-click" => "extension.formData.showEk = !extension.formData.showEk"} + Show Encryption Key + .show-ek{"style" => "text-align: center", "ng-if" => "extension.formData.showEk"} + .ek {{extensionManager.ekForExtension(extension)}} + .disclaimer This key is saved locally and never sent to any servers. + %a.option-link{"ng-if" => "extension.encrypted && extensionManager.ekForExtension(extension) && !extension.formData.changingKey", "ng-click" => "changeEncryptionKeyPressed(extension)"} + Change Encryption Key + %a.option-link{"ng-click" => "deleteExtension(extension)"} Remove extension + + .extension-link + %a{"ng-click" => "toggleExtensionForm()"} Add new extension + + %form.extension-form{"ng-if" => "showNewExtensionForm"} + .form-tag.has-feedback + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'newExtensionData.url'} + %button.btn.dark-button.btn-block{"ng-click" => "submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span.ladda-label Add Extension + + .extension-link + %a{"ng-click" => "reloadExtensionsPressed()", "ng-if" => "extensionManager.extensions.length > 0"} Reload all extensions + .extension-link + %a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions diff --git a/app/assets/templates/frontend/directives/import-export-menu.html.haml b/app/assets/templates/frontend/directives/import-export-menu.html.haml new file mode 100644 index 000000000..0a6b5916a --- /dev/null +++ b/app/assets/templates/frontend/directives/import-export-menu.html.haml @@ -0,0 +1,26 @@ +.options{"style" => "font-size: 12px; margin-top: 4px;"} + %label.enc-option{"ng-if" => "user"} + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"} + Encrypted with Standard File key + %label.enc-option + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"} + {{user ? 'Encrypted with custom key' : 'Encrypted' }} + %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"} + %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"} + %label.enc-option + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"} + Decrypted +.action-container + %a{"ng-click" => "downloadDataArchive()"} Download Data Archive + +%div{"ng-if" => "!importData.loading"} + %label#import-archive + %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} + %a.disabled + %span + Import Data from Archive + .import-password{"ng-if" => "importData.requestPassword"} + Enter the account password associated with the import file. + %input.field{"type" => "text", "ng-model" => "importData.password"} + %button{"ng-click" => "submitImportPassword()"} Decrypt & Import +.spinner{"ng-if" => "importData.loading"} diff --git a/app/assets/templates/frontend/header.html.haml b/app/assets/templates/frontend/header.html.haml index ad14a0fee..67106684f 100644 --- a/app/assets/templates/frontend/header.html.haml +++ b/app/assets/templates/frontend/header.html.haml @@ -1,175 +1,16 @@ .header .header-content .menu.left + .items .item.account - %div{"ng-click" => "ctrl.accountMenuPressed()"} - %div{"ng-if" => "ctrl.user"} Account - %div{"ng-if" => "!ctrl.user"} Sign in or Register - .panel.panel-default.account-panel.panel-right{"ng-if" => "ctrl.showAccountMenu"} - .panel-body - .account-items - .account-item.registration-login{"ng-if" => "!ctrl.user"} - .account-item - .meta-container - .title Server - .desc Enter your Standard File server address, or use the default. - .action-container - %form.account-form{'ng-submit' => 'ctrl.changeServer()', 'name' => "serverChangeForm"} - .form-tag.has-feedback - %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'ctrl.serverData.url'} - %button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span.ladda-label Set Server - .meta-container - .title Sign in or Register - .desc - %form.account-form{'name' => "loginForm"} - .form-tag.has-feedback - %input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.loginData.email'} - .form-tag.has-feedback - %input.form-control.login-input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.loginData.user_password'} - .checkbox{"ng-if" => "ctrl.localNotesCount() > 0"} - %label - %input{"type" => "checkbox", "ng-model" => "ctrl.loginData.mergeLocal", "ng-bind" => "true", "ng-change" => "ctrl.mergeLocalChanged()"} - Merge local notes ({{ctrl.localNotesCount()}} notes) - %button.btn.dark-button.half-button{"ng-click" => "ctrl.loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span Sign In - %button.btn.dark-button.half-button{"ng-click" => "ctrl.submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span Register - %br - .login-forgot{"style" => "padding-top: 4px;"} - %a.btn.btn-link{"ng-click" => "ctrl.showResetForm = !ctrl.showResetForm"} Passwords cannot be forgotten. - .panel-status-text{"ng-if" => "ctrl.loginData.status", "style" => "font-size: 14px;"} {{ctrl.loginData.status}} - - %div{"ng-if" => "ctrl.showResetForm"} - %p{"style" => "font-size: 13px; text-align: center;"} - Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password. - For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password. - - .account-item{"ng-if" => "ctrl.user"} - .email {{ctrl.user.email}} - .server {{ctrl.serverData.url}} - .links{"ng-if" => "ctrl.user"} - -# .link-item - -# %a{"ng-click" => "ctrl.changePasswordPressed()"} Change Password - -# %form.account-form{"ng-if" => "ctrl.showNewPasswordForm", 'ng-submit' => 'ctrl.submitPasswordChange()', 'name' => "passwordChangeForm"} - -# .form-tag.has-feedback - -# %input.form-control.login-input{:autofocus => 'autofocus', :name => 'current', :placeholder => 'Current password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.current_password'} - -# .form-tag.has-feedback - -# %input.form-control.login-input{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password', "autocomplete" => "new-password"} - -# .form-tag.has-feedback - -# %input.form-control.login-input{:placeholder => 'Confirm password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password_confirmation', "autocomplete" => "new-password"} - -# %button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - -# %span.ladda-label Change Password - -# .panel-status-text{"ng-if" => "ctrl.passwordChangeData.status", "style" => "font-size: 14px;"} - -# {{ctrl.passwordChangeData.status}} - .link-item - %a{"ng-click" => "ctrl.signOutPressed()"} Sign Out - .meta-container - .title Local Encryption - .desc Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes. - .action-container - %span.status-title Status: - {{ctrl.encryptionStatusForNotes()}} - .account-item{"ng-if" => "ctrl.user"} - .meta-container - .title Data Archives - .options{"style" => "font-size: 12px; margin-top: 4px;"} - %label - %input{"type" => "radio", "ng-model" => "ctrl.archiveEncryptionFormat.encrypted", "ng-value" => "true", "ng-change" => "ctrl.archiveEncryptionFormat.encrypted = true"} - Encrypted - %label - %input{"type" => "radio", "ng-model" => "ctrl.archiveEncryptionFormat.encrypted", "ng-value" => "false", "ng-change" => "ctrl.archiveEncryptionFormat.encrypted = false"} - Decrypted - .action-container - %a{"ng-click" => "ctrl.downloadDataArchive()"} Download Data Archive - %br - %div{"ng-if" => "!ctrl.importData.loading"} - %label#import-archive - %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "ctrl.importFileSelected(files)"} - %a.disabled - %span - Import Data from Archive - .import-password{"ng-if" => "ctrl.importData.requestPassword"} - Enter the account password associated with the import file. - %input.field{"type" => "text", "ng-model" => "ctrl.importData.password"} - %button{"ng-click" => "ctrl.submitImportPassword()"} Decrypt & Import - .spinner{"ng-if" => "ctrl.importData.loading"} + %a{"ng-click" => "ctrl.accountMenuPressed()"} Data + %account-data-menu + -# {"ng-if" => "ctrl.showAccountMenu"} .item %a{"ng-click" => "ctrl.toggleExtensions()"} Extensions - .panel.panel-default.account-panel.panel-right.extensions-panel{"ng-if" => "ctrl.showExtensionsMenu"} - .panel-body - %div{"style" => "font-size: 18px;", "ng-if" => "!ctrl.extensionManager.extensions.length"} No extensions installed - .registered-extensions{"ng-if" => "ctrl.extensionManager.extensions.length"} - .extension{"ng-repeat" => "extension in ctrl.extensionManager.extensions"} - .extension-name {{extension.name}} - .encryption-format - .title Send data: - %label - %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(true, extension)"} - Encrypted - %label - %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "ctrl.extensionManager.changeExtensionEncryptionFormat(false, extension)"} - Decrypted - .encryption-key{"ng-if" => "extension.encrypted"} - %input{"ng-model" => "extension.ek", "placeholder" => "Set encryption key"} - .extension-actions - .action{"ng-repeat" => "action in extension.actionsInGlobalContext()"} - %div{"ng-if" => "!action.sync_provider"} - .action-name {{action.label}} - .action-desc{"style" => "font-style: italic;"} {{action.desc}} - .execute-type{"ng-if" => "action.repeat_mode == 'watch'"} - Repeats when a change is made to your items. - .execute-type{"ng-if" => "action.repeat_mode == 'loop'"} - Repeats at most once every {{action.repeat_timeout}} seconds - .action-permissions - %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} - %div{"ng-if" => "action.showPermissions"} - {{action.permissionsString}} - .encryption-type - %span {{action.encryptionModeString}} - %div{"ng-if" => "action.sync_provider"} - .action-name This is a sync provider action. - .action-desc{"style" => "margin-top: -5px;"} - %p Enabling this sync provider as a primary provider will replace your current sync provider. - %p Enabling this sync provider as a secondary provider will save your data with this provider as a backup, but will not be used to pull changes. - %p You can have only one primary provider, and multiple secondary providers. - - %div{"ng-if" => "!ctrl.syncProviderActionIsEnabled(action)"} - .execute{"ng-click" => "ctrl.enableSyncProvider(action, extension, true)"} Enable as primary sync provider - .execute{"ng-click" => "ctrl.enableSyncProvider(action, extension, false)"} Enable as backup sync provider - %div{"ng-if" => "ctrl.syncProviderActionIsEnabled(action)"} - .execute{"ng-click" => "ctrl.disableSyncProvider(action, extension)"} Remove as sync provider - - .execute{"ng-if" => "!action.sync_provider"} - %div{"ng-if" => "action.repeat_mode"} - %div{"ng-if" => "ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.disableRepeatAction(action, extension)"} Disable - %div{"ng-if" => "!ctrl.extensionManager.isRepeatActionEnabled(action)", "ng-click" => "ctrl.extensionManager.enableRepeatAction(action, extension)"} Enable - %div{"ng-if" => "!action.repeat_mode", "ng-click" => "ctrl.selectedAction(action, extension)"} - %div{"ng-if" => "!action.running"} - Perform Action - %div{"ng-if" => "action.running"} - .spinner.execution-spinner - .last-run{"ng-if" => "!action.error && action.lastExecuted && !action.running"} - Last run {{action.lastExecuted | appDateTime}} - .error{"ng-if" => "action.error"} - Error performing action. - %a{"ng-click" => "ctrl.deleteExtension(extension)", "style" => "margin-top: 22px; display: block; text-align: center;"} Remove extension - - .extension-link - %a{"ng-click" => "ctrl.toggleExtensionForm()"} Add new extension - - %form.extension-form{"ng-if" => "ctrl.showNewExtensionForm"} - .form-tag.has-feedback - %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'ctrl.newExtensionData.url'} - %button.btn.dark-button.btn-block{"ng-click" => "ctrl.submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span.ladda-label Add Extension - - .extension-link - %a{"ng-click" => "ctrl.reloadExtensionsPressed()", "ng-if" => "ctrl.extensionManager.extensions.length > 0"} Reload all extensions - .extension-link - %a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions + %global-extensions-menu{"ng-if" => "ctrl.showExtensionsMenu"} .item %a{"href" => "https://standardnotes.org", "target" => "_blank"} From 51012d7d540f8c391ff44aa620566136ca640aef Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 25 Jan 2017 17:14:11 -0600 Subject: [PATCH 03/25] api controller refactor --- .../app/frontend/controllers/_base.js | 6 +- .../app/frontend/controllers/header.js | 4 +- .../app/frontend/controllers/home.js | 20 +- .../app/frontend/models/local/itemParams.js | 52 +++ .../app/frontend/models/sync/syncProvider.js | 38 -- .../javascripts/app/services/apiController.js | 355 +----------------- .../services/directives/accountSyncSection.js | 32 +- .../directives/globalExtensionsMenu.js | 51 +-- .../app/services/extensionManager.js | 13 +- .../app/services/sync/encryptionHelper.js | 63 ++++ .../app/services/{ => sync}/syncManager.js | 80 +++- .../app/services/sync/syncPerformer.js | 196 ++++++++++ .../directives/account-sync-section.html.haml | 12 +- .../global-extensions-menu.html.haml | 50 +-- 14 files changed, 471 insertions(+), 501 deletions(-) create mode 100644 app/assets/javascripts/app/frontend/models/local/itemParams.js delete mode 100644 app/assets/javascripts/app/frontend/models/sync/syncProvider.js create mode 100644 app/assets/javascripts/app/services/sync/encryptionHelper.js rename app/assets/javascripts/app/services/{ => sync}/syncManager.js (65%) create mode 100644 app/assets/javascripts/app/services/sync/syncPerformer.js diff --git a/app/assets/javascripts/app/frontend/controllers/_base.js b/app/assets/javascripts/app/frontend/controllers/_base.js index e6e22df8d..a220696a4 100644 --- a/app/assets/javascripts/app/frontend/controllers/_base.js +++ b/app/assets/javascripts/app/frontend/controllers/_base.js @@ -1,9 +1,9 @@ class BaseCtrl { - constructor($rootScope, modelManager, apiController, dbManager) { + constructor(syncManager, dbManager) { dbManager.openDatabase(null, function(){ // new database, delete syncToken so that items can be refetched entirely from server - apiController.clearSyncToken(); - apiController.sync(); + syncManager.clearSyncToken(); + syncManager.sync(); }) } } diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js index f0f3975eb..b765b2a63 100644 --- a/app/assets/javascripts/app/frontend/controllers/header.js +++ b/app/assets/javascripts/app/frontend/controllers/header.js @@ -16,7 +16,7 @@ angular.module('app.frontend') } } }) - .controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager) { + .controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager, syncManager) { this.user = apiController.user; @@ -43,7 +43,7 @@ angular.module('app.frontend') this.refreshData = function() { this.isRefreshing = true; - apiController.sync(function(response){ + syncManager.sync(function(response){ $timeout(function(){ this.isRefreshing = false; }.bind(this), 200) diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index b03d22838..9d03da457 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -1,14 +1,14 @@ angular.module('app.frontend') -.controller('HomeCtrl', function ($scope, $rootScope, $timeout, apiController, modelManager) { +.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager) { $rootScope.bodyClass = "app-body-class"; - apiController.loadLocalItems(function(items){ + syncManager.loadLocalItems(function(items){ $scope.$apply(); - apiController.sync(null); + syncManager.sync(null); // refresh every 30s setInterval(function () { - apiController.sync(null); + syncManager.sync(null); }, 30000); }); @@ -31,7 +31,7 @@ angular.module('app.frontend') modelManager.createRelationshipBetweenItems(note, tag); } - apiController.sync(); + syncManager.sync(); } /* @@ -57,7 +57,7 @@ angular.module('app.frontend') $scope.tagsSave = function(tag, callback) { tag.setDirty(true); - apiController.sync(callback); + syncManager.sync(callback); } /* @@ -69,7 +69,7 @@ angular.module('app.frontend') if(validNotes == 0) { modelManager.setItemToBeDeleted(tag); // if no more notes, delete tag - apiController.sync(function(){ + syncManager.sync(function(){ // force scope tags to update on sub directives $scope.tags = []; $timeout(function(){ @@ -100,7 +100,7 @@ angular.module('app.frontend') $scope.saveNote = function(note, callback) { note.setDirty(true); - apiController.sync(function(response){ + syncManager.sync(function(response){ if(response && response.error) { if(!$scope.didShowErrorAlert) { $scope.didShowErrorAlert = true; @@ -137,8 +137,8 @@ angular.module('app.frontend') return; } - apiController.sync(function(){ - if(!apiController.user) { + syncManager.sync(function(){ + if(syncManager.offline) { // when deleting items while ofline, we need to explictly tell angular to refresh UI setTimeout(function () { $scope.safeApply(); diff --git a/app/assets/javascripts/app/frontend/models/local/itemParams.js b/app/assets/javascripts/app/frontend/models/local/itemParams.js new file mode 100644 index 000000000..614f670a6 --- /dev/null +++ b/app/assets/javascripts/app/frontend/models/local/itemParams.js @@ -0,0 +1,52 @@ +class ItemParams { + + constructor(item, ek, encryptionHelper) { + this.item = item; + this.ek = ek; + this.encryptionHelper = encryptionHelper; + } + + paramsForExportFile() { + this.additionalFields = ["created_at", "updated_at"]; + this.forExportFile = true; + return _.omit(this.__params(), ["deleted"]); + } + + paramsForExtension() { + return this.paramsForExportFile(); + } + + paramsForSync() { + return __params(null, false); + } + + __params() { + var itemCopy = _.cloneDeep(this.item); + + console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy) + + var params = {uuid: item.uuid, content_type: item.content_type, deleted: item.deleted}; + + if(this.ek) { + this.encryptionHelper.encryptItem(itemCopy, this.ek); + params.content = itemCopy.content; + params.enc_item_key = itemCopy.enc_item_key; + params.auth_hash = itemCopy.auth_hash; + } + else { + params.content = this.forExportFile ? itemCopy.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(itemCopy.createContentJSONFromProperties())); + if(!this.forExportFile) { + params.enc_item_key = null; + params.auth_hash = null; + } + } + + if(this.additionalFields) { + _.merge(params, _.pick(item, this.additionalFields)); + } + + return params; + } + + +} diff --git a/app/assets/javascripts/app/frontend/models/sync/syncProvider.js b/app/assets/javascripts/app/frontend/models/sync/syncProvider.js deleted file mode 100644 index a21bcce8d..000000000 --- a/app/assets/javascripts/app/frontend/models/sync/syncProvider.js +++ /dev/null @@ -1,38 +0,0 @@ -class SyncProvider { - constructor(obj) { - this.encrypted = true; - _.merge(this, obj); - } - - addPendingItems(items) { - if(!this.pendingItems) { - this.pendingItems = []; - } - - this.pendingItems = this.pendingItems.concat(items); - } - - removePendingItems(items) { - this.pendingItems = _.difference(this.pendingItems, items); - } - - get status() { - if(!this.enabled) { - return null; - } - - if(this.primary) return "primary"; - else return "secondary"; - } - - asJSON() { - return { - enabled: this.enabled, - url: this.url, - primary: this.primary, - keyName: this.keyName, - syncToken: this.syncToken - } - } - -} diff --git a/app/assets/javascripts/app/services/apiController.js b/app/assets/javascripts/app/services/apiController.js index a134a31f5..c119cf1ec 100644 --- a/app/assets/javascripts/app/services/apiController.js +++ b/app/assets/javascripts/app/services/apiController.js @@ -87,12 +87,7 @@ angular.module('app.frontend') var params = {password: keys.pw, email: email}; _.merge(request, params); request.post().then(function(response){ - localStorage.setItem("server", url); - localStorage.setItem("jwt", response.token); - localStorage.setItem("user", JSON.stringify(response.user)); - localStorage.setItem("auth_params", JSON.stringify(authParams)); - keyManager.addKey(SNKeyName, mk); - this.addStandardFileSyncProvider(url); + this.handleAuthResponse(response, url, authParams, mk); callback(response); }.bind(this)) .catch(function(response){ @@ -103,6 +98,15 @@ angular.module('app.frontend') }.bind(this)) } + this.handleAuthResponse = function(response, url, authParams, mk) { + localStorage.setItem("server", url); + localStorage.setItem("jwt", response.token); + localStorage.setItem("user", JSON.stringify(response.user)); + localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); + syncManager.addKey(syncManager.SNKeyName, mk); + syncManager.addStandardFileSyncProvider(url); + } + this.register = function(url, email, password, callback) { Neeto.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){ var mk = keys.mk; @@ -111,12 +115,7 @@ angular.module('app.frontend') var params = _.merge({password: keys.pw, email: email}, authParams); _.merge(request, params); request.post().then(function(response){ - localStorage.setItem("server", url); - localStorage.setItem("jwt", response.token); - localStorage.setItem("user", JSON.stringify(response.user)); - localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); - keyManager.addKey(SNKeyName, mk); - this.addStandardFileSyncProvider(url); + this.handleAuthResponse(response, url, authParams, mk); callback(response); }.bind(this)) .catch(function(response){ @@ -181,224 +180,11 @@ angular.module('app.frontend') Sync */ - this.syncWithOptions = function(callback, options = {}) { - - var allDirtyItems = modelManager.getDirtyItems(); - - // we want to write all dirty items to disk only if the user is not signed in, or if the sync op fails - // if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server - var writeAllDirtyItemsToDisk = function(completion) { - this.writeItemsToLocalStorage(allDirtyItems, function(responseItems){ - if(completion) { - completion(); - } - }) - }.bind(this); - - if(!this.isUserSignedIn()) { - writeAllDirtyItemsToDisk(function(){ - // delete anything needing to be deleted - allDirtyItems.forEach(function(item){ - if(item.deleted) { - modelManager.removeItemLocally(item); - } - }.bind(this)) - - modelManager.clearDirtyItems(allDirtyItems); - - }.bind(this)) - - this.syncOpInProgress = false; - - if(this.syncProviders.length == 0 && callback) { - callback(); - } - } - - for(let provider of this.syncProviders) { - if(!provider.enabled) { - continue; - } - provider.addPendingItems(allDirtyItems); - this.__performSyncWithProvider(provider, options, function(response){ - if(provider.primary) { - if(callback) { - callback(response) - } - } - }) - } - - modelManager.clearDirtyItems(allDirtyItems); - } - - this.__performSyncWithProvider = function(provider, options, callback) { - if(provider.syncOpInProgress) { - provider.repeatOnCompletion = true; - console.log("Sync op in progress for provider; returning.", provider); - return; - } - - - provider.syncOpInProgress = true; - - let submitLimit = 100; - var allItems = provider.pendingItems; - var subItems = allItems.slice(0, submitLimit); - if(subItems.length < allItems.length) { - // more items left to be synced, repeat - provider.repeatOnCompletion = true; - } else { - provider.repeatOnCompletion = false; - } - - console.log("Syncing with provider", provider, subItems); - - // Remove dirty items now. If this operation fails, we'll re-add them. - // This allows us to queue changes on the same item - provider.removePendingItems(subItems); - - var request = Restangular.oneUrl(provider.url, provider.url); - request.limit = 150; - request.items = _.map(subItems, function(item){ - return this.paramsForItem(item, provider.encrypted, provider.ek, options.additionalFields, false); - }.bind(this)); - - request.sync_token = provider.syncToken; - request.cursor_token = provider.cursorToken; - - request.post().then(function(response) { - - console.log("Sync completion", response); - - provider.syncToken = response.sync_token; - - if(provider.primary) { - $rootScope.$broadcast("sync:updated_token", provider.syncToken); - - // handle cursor token (more results waiting, perform another sync) - provider.cursorToken = response.cursor_token; - - var retrieved = this.handleItemsResponse(response.retrieved_items, null, provider); - // merge only metadata for saved items - var omitFields = ["content", "auth_hash"]; - var saved = this.handleItemsResponse(response.saved_items, omitFields, provider); - - this.handleUnsavedItemsResponse(response.unsaved, provider) - - this.writeItemsToLocalStorage(saved, null); - this.writeItemsToLocalStorage(retrieved, null); - } - - provider.syncOpInProgress = false; - this.didMakeChangesToSyncProviders(); - - if(provider.cursorToken || provider.repeatOnCompletion == true) { - this.__performSyncWithProvider(provider, options, callback); - } else { - if(callback) { - callback(response); - } - } - - }.bind(this)) - .catch(function(response){ - console.log("Sync error: ", response); - - // Re-add subItems since this operation failed. We'll have to try again. - provider.addPendingItems(subItems); - provider.syncOpInProgress = false; - - if(provider.primary) { - this.writeItemsToLocalStorage(allItems, null); - } - - if(callback) { - callback({error: "Sync error"}); - } - }.bind(this)) - } - - this.sync = function(callback) { - this.syncWithOptions(callback, undefined); - } - - this.handleUnsavedItemsResponse = function(unsaved, provider) { - if(unsaved.length == 0) { - return; - } - - console.log("Handle unsaved", unsaved); - for(var mapping of unsaved) { - var itemResponse = mapping.item; - var item = modelManager.findItem(itemResponse.uuid); - var error = mapping.error; - if(error.tag == "uuid_conflict") { - item.alternateUUID(); - item.setDirty(true); - item.markAllReferencesDirty(); - } - } - - this.__performSyncWithProvider(provider, {additionalFields: ["created_at", "updated_at"]}, null); - } - - this.handleItemsResponse = function(responseItems, omitFields, syncProvider) { - var ek = syncProvider ? keyManager.keyForName(syncProvider.keyName).key : null; - this.decryptItemsWithKey(responseItems, ek); - return modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); - } - - this.paramsForExportFile = function(item, ek, encrypted) { - return _.omit(this.paramsForItem(item, encrypted, ek, ["created_at", "updated_at"], true), ["deleted"]); - } - - this.paramsForExtension = function(item, encrypted) { - return _.omit(this.paramsForItem(item, encrypted, ["created_at", "updated_at"], true), ["deleted"]); - } - - this.paramsForItem = function(item, encrypted, ek, additionalFields, forExportFile) { - var itemCopy = _.cloneDeep(item); - - if(encrypted) { - console.assert(ek.length, "Attempting to encrypt without encryption key."); - } - console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy) - - var params = {uuid: item.uuid, content_type: item.content_type, deleted: item.deleted}; - - if(encrypted) { - this.encryptSingleItem(itemCopy, ek); - params.content = itemCopy.content; - params.enc_item_key = itemCopy.enc_item_key; - params.auth_hash = itemCopy.auth_hash; - } - else { - params.content = forExportFile ? itemCopy.createContentJSONFromProperties() : "000" + Neeto.crypto.base64(JSON.stringify(itemCopy.createContentJSONFromProperties())); - if(!forExportFile) { - params.enc_item_key = null; - params.auth_hash = null; - } - } - - if(additionalFields) { - _.merge(params, _.pick(item, additionalFields)); - } - - return params; - } /* Import */ - this.clearSyncToken = function() { - var primary = this.primarySyncProvider(); - if(primary) { - primary.syncToken = null; - } - } - this.importJSONData = function(data, password, callback) { console.log("Importing data", data); @@ -439,7 +225,7 @@ angular.module('app.frontend') Export */ - this.itemsDataFile = function(encrypted, custom_ek) { + this.itemsDataFile = function(ek) { var textFile = null; var makeTextFile = function (text) { var data = new Blob([text], {type: 'text/json'}); @@ -456,20 +242,16 @@ angular.module('app.frontend') return textFile; }.bind(this); - var ek = custom_ek; - if(encrypted && !custom_ek) { - ek = this.retrieveMk(); - } - var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){ - return this.paramsForExportFile(item, ek, encrypted); + var itemParams = new ItemParams(item, ek); + return itemParams.paramsForExportFile(); }.bind(this)); var data = { items: items } - if(encrypted && !custom_ek) { + if(ek.name == syncManager.SNKeyName) { // auth params are only needed when encrypted with a standard file key data["auth_params"] = this.getAuthParams(); } @@ -481,22 +263,6 @@ angular.module('app.frontend') return JSON.parse(JSON.stringify(object)); } - this.writeItemsToLocalStorage = function(items, callback) { - var params = items.map(function(item) { - return this.paramsForItem(item, false, ["created_at", "updated_at", "dirty"], true) - }.bind(this)); - - dbManager.saveItems(params, callback); - } - - this.loadLocalItems = function(callback) { - var params = dbManager.getAllItems(function(items){ - var items = this.handleItemsResponse(items, null, null); - Item.sortItemsByDate(items); - callback(items); - }.bind(this)) - - } /* Drafts @@ -519,101 +285,12 @@ angular.module('app.frontend') return modelManager.createItem(jsonObj); } - - /* - Encrpytion - */ - - this.retrieveMk = function() { - if(!this.mk) { - this.mk = localStorage.getItem("mk"); - } - return this.mk; - } - - this.setMk = function(mk) { - localStorage.setItem('mk', mk); - } - this.signoutOfStandardFile = function(callback) { - this.removeStandardFileSyncProvider(); + syncManager.removeStandardFileSyncProvider(); dbManager.clearAllItems(function(){ localStorage.clear(); callback(); }); } - - this.encryptSingleItem = function(item, masterKey) { - var item_key = null; - if(item.enc_item_key) { - item_key = Neeto.crypto.decryptText(item.enc_item_key, masterKey); - } else { - item_key = Neeto.crypto.generateRandomEncryptionKey(); - item.enc_item_key = Neeto.crypto.encryptText(item_key, masterKey); - } - - var ek = Neeto.crypto.firstHalfOfKey(item_key); - var ak = Neeto.crypto.secondHalfOfKey(item_key); - var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.createContentJSONFromProperties()), ek); - var authHash = Neeto.crypto.hmac256(encryptedContent, ak); - - item.content = encryptedContent; - item.auth_hash = authHash; - item.local_encryption_scheme = "1.0"; - } - - this.decryptSingleItem = function(item, masterKey) { - var item_key = Neeto.crypto.decryptText(item.enc_item_key, masterKey); - - var ek = Neeto.crypto.firstHalfOfKey(item_key); - var ak = Neeto.crypto.secondHalfOfKey(item_key); - var authHash = Neeto.crypto.hmac256(item.content, ak); - if(authHash !== item.auth_hash || !item.auth_hash) { - console.log("Authentication hash does not match.") - return; - } - - var content = Neeto.crypto.decryptText(item.content.substring(3, item.content.length), ek); - item.content = content; - } - - this.decryptItemsWithKey = function(items, key) { - for (var item of items) { - if(item.deleted == true) { - continue; - } - var isString = typeof item.content === 'string' || item.content instanceof String; - if(isString) { - try { - if(item.content.substring(0, 3) == "001" && item.enc_item_key) { - // is encrypted - this.decryptSingleItem(item, key); - } else { - // is base64 encoded - item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)) - } - } catch (e) { - console.log("Error decrypting item", item, e); - continue; - } - } - } - } - - this.reencryptAllItemsAndSave = function(user, newMasterKey, oldMasterKey, callback) { - var items = modelManager.allItems(); - items.forEach(function(item){ - if(item.content.substring(0, 3) == "001" && item.enc_item_key) { - // first decrypt item_key with old key - var item_key = Neeto.crypto.decryptText(item.enc_item_key, oldMasterKey); - // now encrypt item_key with new key - item.enc_item_key = Neeto.crypto.encryptText(item_key, newMasterKey); - } - }); - - this.saveBatchItems(user, items, function(success) { - callback(success); - }.bind(this)); - } } }); diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js index e2c5fb08d..e690e8940 100644 --- a/app/assets/javascripts/app/services/directives/accountSyncSection.js +++ b/app/assets/javascripts/app/services/directives/accountSyncSection.js @@ -7,15 +7,15 @@ class AccountSyncSection { }; } - controller($scope, apiController, modelManager, keyManager) { + controller($scope, modelManager, keyManager, syncManager) { 'ngInject'; - $scope.syncProviders = apiController.syncProviders; + $scope.syncProviders = syncManager.syncProviders; $scope.newSyncData = {showAddSyncForm: false} $scope.keys = keyManager.keys; $scope.submitExternalSyncURL = function() { - apiController.addSyncProviderFromURL($scope.newSyncData.url); + syncManager.addSyncProviderFromURL($scope.newSyncData.url); $scope.newSyncData.showAddSyncForm = false; } @@ -24,11 +24,33 @@ class AccountSyncSection { alert("You must choose an encryption key for this provider before enabling it."); return; } - apiController.enableSyncProvider(provider, primary); + + syncManager.enableSyncProvider(provider, primary); + syncManager.addAllDataAsNeedingSyncForProvider(provider); + syncManager.sync(); } $scope.removeSyncProvider = function(provider) { - apiController.removeSyncProvider(provider); + syncManager.removeSyncProvider(provider); + } + + $scope.changeEncryptionKey = function(provider) { + if(!confirm("Changing your encryption key will re-encrypt all your notes with the new key and sync them back to the server. This can take several minutes. We strongly recommend downloading a backup of your notes before continuing.")) { + return; + } + + provider.formData = {keyName: provider.keyName}; + provider.showKeyForm = true; + } + + $scope.saveKey = function(provider) { + provider.showKeyForm = false; + provider.keyName = provider.formData.keyName; + + if(provider.enabled) { + syncManager.addAllDataAsNeedingSyncForProvider(provider); + syncManager.sync(); + } } } } diff --git a/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js index 579471118..5834e4bc7 100644 --- a/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js +++ b/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js @@ -7,7 +7,7 @@ class GlobalExtensionsMenu { }; } - controller($scope, apiController, extensionManager) { + controller($scope, extensionManager, syncManager) { 'ngInject'; $scope.extensionManager = extensionManager; @@ -39,69 +39,26 @@ class GlobalExtensionsMenu { alert("There was an error performing this action. Please try again."); } else { action.error = false; - apiController.sync(null); + syncManager.sync(null); } }) } $scope.changeExtensionEncryptionFormat = function(encrypted, extension) { - var provider = $scope.syncProviderForExtension(extension); - if(provider) { - if(confirm("Changing encryption status will update all your items and re-sync them back to the server. This can take several minutes. Are you sure you want to continue?")) { - extensionManager.changeExtensionEncryptionFormat(encrypted, extension); - apiController.resyncAllDataForProvider(provider); - } else { - // revert setting - console.log("reverting"); - extension.encrypted = extensionManager.extensionUsesEncryptedData(extension); - } - } else { - extensionManager.changeExtensionEncryptionFormat(encrypted, extension); - } + extensionManager.changeExtensionEncryptionFormat(encrypted, extension); } $scope.deleteExtension = function(extension) { if(confirm("Are you sure you want to delete this extension?")) { extensionManager.deleteExtension(extension); - var syncProviderAction = extension.syncProviderAction; - if(syncProviderAction) { - apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url)); - } } } $scope.reloadExtensionsPressed = function() { - if(confirm("For your security, reloading extensions will disable any currently enabled sync providers and repeat actions.")) { + if(confirm("For your security, reloading extensions will disable any currently enabled repeat actions.")) { extensionManager.refreshExtensionsFromServer(); - var syncProviderAction = extension.syncProviderAction; - if(syncProviderAction) { - apiController.removeSyncProvider(apiController.syncProviderForURL(syncProviderAction.url)); - } } } - - $scope.setEncryptionKeyForExtension = function(extension) { - extension.formData.changingKey = false; - var ek = extension.formData.ek; - extensionManager.setEkForExtension(extension, ek); - if(extension.formData.changingKey) { - var syncAction = extension.syncProviderAction; - if(syncAction) { - var provider = apiController.syncProviderForURL(syncAction.url); - provider.ek = ek; - apiController.didMakeChangesToSyncProviders(); - apiController.resyncAllDataForProvider(provider); - } - } - } - - $scope.changeEncryptionKeyPressed = function(extension) { - if(!confirm("Changing your encryption key will re-encrypt all your notes with the new key and sync them back to the server. This can take several minutes. We strongly recommend downloading a backup of your notes before continuing.")) { - return; - } - - extension.formData.changingKey = true; - } } } diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index 28f9710b4..fd691b79a 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -1,12 +1,13 @@ class ExtensionManager { - constructor(Restangular, modelManager, apiController) { + constructor(Restangular, modelManager, apiController, syncManager) { this.Restangular = Restangular; this.modelManager = modelManager; this.apiController = apiController; this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || []; this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || []; this.extensionEks = JSON.parse(localStorage.getItem("extensionEks")) || {}; + this.syncManager = syncManager; modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){ for (var ext of items) { @@ -79,7 +80,7 @@ class ExtensionManager { } this.modelManager.setItemToBeDeleted(extension); - this.apiController.sync(null); + this.syncManager.sync(null); } /* @@ -122,7 +123,7 @@ class ExtensionManager { extension.url = url; extension.setDirty(true); this.modelManager.addItem(extension); - this.apiController.sync(null); + this.syncManager.sync(null); } return extension; @@ -145,7 +146,8 @@ class ExtensionManager { executeAction(action, extension, item, callback) { - if(this.extensionUsesEncryptedData(extension) && !this.apiController.isUserSignedIn()) { + //todo + if(this.extensionUsesEncryptedData(extension)) { alert("To send data encrypted, you must have an encryption key, and must therefore be signed in."); callback(null); return; @@ -279,7 +281,8 @@ class ExtensionManager { } outgoingParamsForItem(item, extension) { - return this.apiController.paramsForExtension(item, this.extensionUsesEncryptedData(extension)); + var itemParams = new itemParams(item, extension.ek); + return itemParams.paramsForExtension(); } performPost(action, extension, params, callback) { diff --git a/app/assets/javascripts/app/services/sync/encryptionHelper.js b/app/assets/javascripts/app/services/sync/encryptionHelper.js new file mode 100644 index 000000000..a5b547eab --- /dev/null +++ b/app/assets/javascripts/app/services/sync/encryptionHelper.js @@ -0,0 +1,63 @@ +class EncryptionHelper { + + encryptItem(item, key) { + var item_key = null; + if(item.enc_item_key) { + 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); + var encryptedContent = "001" + Neeto.crypto.encryptText(JSON.stringify(item.createContentJSONFromProperties()), ek); + var authHash = Neeto.crypto.hmac256(encryptedContent, ak); + + item.content = encryptedContent; + item.auth_hash = authHash; + item.local_encryption_scheme = "1.0"; + } + + decryptItem(item, key) { + var item_key = Neeto.crypto.decryptText(item.enc_item_key, key); + + var ek = Neeto.crypto.firstHalfOfKey(item_key); + var ak = Neeto.crypto.secondHalfOfKey(item_key); + var authHash = Neeto.crypto.hmac256(item.content, ak); + if(authHash !== item.auth_hash || !item.auth_hash) { + console.log("Authentication hash does not match.") + return; + } + + var content = Neeto.crypto.decryptText(item.content.substring(3, item.content.length), ek); + item.content = content; + } + + decryptMultipleItems(items, key) { + for (var item of items) { + if(item.deleted == true) { + continue; + } + + var isString = typeof item.content === 'string' || item.content instanceof String; + if(isString) { + try { + if(item.content.substring(0, 3) == "001" && item.enc_item_key) { + // is encrypted + this.decryptItem(item, key); + } else { + // is base64 encoded + item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)) + } + } catch (e) { + console.log("Error decrypting item", item, e); + continue; + } + } + } + } + +} + +angular.module('app.frontend').service('encryptionHelper', EncryptionHelper); diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/sync/syncManager.js similarity index 65% rename from app/assets/javascripts/app/services/syncManager.js rename to app/assets/javascripts/app/services/sync/syncManager.js index bfa5de223..aec4a5ffd 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/sync/syncManager.js @@ -1,9 +1,26 @@ class SyncManager { - let SNKeyName = "Standard Notes Key"; - constructor(modelManager) { this.modelManager = modelManager; + this.syncPerformer = new SyncPerformer() + this.SNKeyName = "Standard Notes Key"; + this.loadSyncProviders(); + } + + get offline() { + return this.syncProviders.length == 0; + } + + sync(callback) { + this.syncPerformer.sync(this.syncProviders, callback); + } + + syncWithProvider(provider, callback) { + this.syncPerformer.performSyncWithProvider(provider, callback); + } + + loadLocalItems(callback) { + this.syncPerformer.loadLocalItems(callback); } syncProviderForURL(url) { @@ -36,7 +53,7 @@ class SyncManager { addStandardFileSyncProvider(url) { var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.syncProviders.length == 0}); - defaultProvider.keyName = SNKeyName; + defaultProvider.keyName = this.SNKeyName; defaultProvider.enabled = this.syncProviders.length == 0; this.syncProviders.push(defaultProvider); return defaultProvider; @@ -64,7 +81,7 @@ class SyncManager { // migrate old key structure to new var mk = localStorage.getItem("mk"); if(mk) { - keyManager.addKey(SNKeyName, mk); + keyManager.addKey(this.SNKeyName, mk); localStorage.removeItem("mk"); } this.didMakeChangesToSyncProviders(); @@ -72,8 +89,6 @@ class SyncManager { } } - this.loadSyncProviders(); - addSyncProviderFromURL(url) { var provider = new SyncProvider({url: url}); this.syncProviders.push(provider); @@ -91,13 +106,13 @@ class SyncManager { syncProvider.primary = primary; // since we're enabling a new provider, we need to send it EVERYTHING we have now. - syncProvider.addPendingItems(this.modelManager.allItems); + this.addAllDataAsNeedingSyncForProvider(syncProvider); this.didMakeChangesToSyncProviders(); + this.syncWithProvider(syncProvider); } - resyncAllDataForProvider(syncProvider) { + addAllDataAsNeedingSyncForProvider(syncProvider) { syncProvider.addPendingItems(this.modelManager.allItems); - this.sync(); } removeSyncProvider(provider) { @@ -105,7 +120,52 @@ class SyncManager { this.didMakeChangesToSyncProviders(); } - + clearSyncToken() { + var primary = this.primarySyncProvider(); + if(primary) { + primary.syncToken = null; + } + } } angular.module('app.frontend').service('syncManager', SyncManager); + +class SyncProvider { + + constructor(obj) { + this.encrypted = true; + _.merge(this, obj); + } + + addPendingItems(items) { + if(!this.pendingItems) { + this.pendingItems = []; + } + + this.pendingItems = this.pendingItems.concat(items); + } + + removePendingItems(items) { + this.pendingItems = _.difference(this.pendingItems, items); + } + + get status() { + if(!this.enabled) { + return null; + } + + if(this.primary) return "primary"; + else return "secondary"; + } + + asJSON() { + return { + enabled: this.enabled, + url: this.url, + primary: this.primary, + keyName: this.keyName, + syncToken: this.syncToken + } + } + +} diff --git a/app/assets/javascripts/app/services/sync/syncPerformer.js b/app/assets/javascripts/app/services/sync/syncPerformer.js new file mode 100644 index 000000000..e475dc701 --- /dev/null +++ b/app/assets/javascripts/app/services/sync/syncPerformer.js @@ -0,0 +1,196 @@ +class SyncPerformer { + + constructor(modelManager, dbManager, encryptionHelper, keyManager) { + this.modelManager = modelManager; + this.dbManager = dbManager; + this.encryptionHelper = encryptionHelper; + this.keyManager = keyManager; + } + + setOnChangeProviderCallback(callback) { + this.onChangeProviderCallback = callback; + } + + didMakeChangesToSyncProvider(provider) { + this.onChangeProviderCallback(provider); + } + + writeItemsToLocalStorage(items, callback) { + var params = items.map(function(item) { + return this.paramsForItem(item, false, ["created_at", "updated_at", "dirty"], true) + }.bind(this)); + + this.dbManager.saveItems(params, callback); + } + + loadLocalItems(callback) { + var params = this.dbManager.getAllItems(function(items){ + var items = this.handleItemsResponse(items, null, null); + Item.sortItemsByDate(items); + callback(items); + }.bind(this)) + } + + syncOffline(items, callback) { + this.writeItemsToLocalStorage(items, function(responseItems){ + // delete anything needing to be deleted + for(var item of items) { + if(item.deleted) { + this.modelManager.removeItemLocally(item); + } + } + }.bind(this)) + + if(callback) { + callback(); + } + } + + sync(providers, callback, options = {}) { + + var allDirtyItems = this.modelManager.getDirtyItems(); + + // we want to write all dirty items to disk only if the user has no sync providers, or if the sync op fails + // if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server + if(providers.length == 0) { + this.syncOffline(allDirtyItems, callback); + } + + for(let provider of providers) { + if(!provider.enabled) { + continue; + } + + provider.addPendingItems(allDirtyItems); + this.didMakeChangesToSyncProvider(provider); + + this.__performSyncWithProvider(provider, options, function(response){ + if(provider.primary) { + if(callback) { + callback(response) + } + } + }) + } + + this.modelManager.clearDirtyItems(allDirtyItems); + } + + performSyncWithProvider(provider, callback) { + this.__performSyncWithProvider(provider, {}, callback); + } + + __performSyncWithProvider(provider, options, callback) { + if(provider.syncOpInProgress) { + provider.repeatOnCompletion = true; + console.log("Sync op in progress for provider; returning.", provider); + return; + } + + + provider.syncOpInProgress = true; + + let submitLimit = 100; + var allItems = provider.pendingItems; + var subItems = allItems.slice(0, submitLimit); + if(subItems.length < allItems.length) { + // more items left to be synced, repeat + provider.repeatOnCompletion = true; + } else { + provider.repeatOnCompletion = false; + } + + console.log("Syncing with provider", provider, subItems); + + // Remove dirty items now. If this operation fails, we'll re-add them. + // This allows us to queue changes on the same item + provider.removePendingItems(subItems); + + var request = Restangular.oneUrl(provider.url, provider.url); + request.limit = 150; + request.items = _.map(subItems, function(item){ + var itemParams = new ItemParams(item, provider.ek); + itemParams.additionalFields = options.additionalFields; + return itemParams.paramsForSync(); + }.bind(this)); + + request.sync_token = provider.syncToken; + request.cursor_token = provider.cursorToken; + + request.post().then(function(response) { + + console.log("Sync completion", response); + + provider.syncToken = response.sync_token; + + if(provider.primary) { + $rootScope.$broadcast("sync:updated_token", provider.syncToken); + + // handle cursor token (more results waiting, perform another sync) + provider.cursorToken = response.cursor_token; + + var retrieved = this.handleItemsResponse(response.retrieved_items, null, provider); + // merge only metadata for saved items + var omitFields = ["content", "auth_hash"]; + var saved = this.handleItemsResponse(response.saved_items, omitFields, provider); + + this.handleUnsavedItemsResponse(response.unsaved, provider) + + this.writeItemsToLocalStorage(saved, null); + this.writeItemsToLocalStorage(retrieved, null); + } + + provider.syncOpInProgress = false; + + if(provider.cursorToken || provider.repeatOnCompletion == true) { + this.__performSyncWithProvider(provider, options, callback); + } else { + if(callback) { + callback(response); + } + } + + }.bind(this)) + .catch(function(response){ + console.log("Sync error: ", response); + + // Re-add subItems since this operation failed. We'll have to try again. + provider.addPendingItems(subItems); + provider.syncOpInProgress = false; + + if(provider.primary) { + this.writeItemsToLocalStorage(allItems, null); + } + + if(callback) { + callback({error: "Sync error"}); + } + }.bind(this)) + } + + handleUnsavedItemsResponse(unsaved, provider) { + if(unsaved.length == 0) { + return; + } + + console.log("Handle unsaved", unsaved); + for(var mapping of unsaved) { + var itemResponse = mapping.item; + var item = this.modelManager.findItem(itemResponse.uuid); + var error = mapping.error; + if(error.tag == "uuid_conflict") { + item.alternateUUID(); + item.setDirty(true); + item.markAllReferencesDirty(); + } + } + + this.__performSyncWithProvider(provider, {additionalFields: ["created_at", "updated_at"]}, null); + } + + handleItemsResponse(responseItems, omitFields, syncProvider) { + var ek = syncProvider ? this.keyManager.keyForName(syncProvider.keyName).key : null; + this.encryptionHelper.decryptMultipleItems(responseItems, ek); + return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); + } +} diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml index b99e52fc4..afaed9829 100644 --- a/app/assets/templates/frontend/directives/account-sync-section.html.haml +++ b/app/assets/templates/frontend/directives/account-sync-section.html.haml @@ -5,14 +5,16 @@ .url {{provider.url}} .options %div{"ng-if" => "!provider.enabled"} - %strong Choose encryption key: - %select{"ng-model" => "provider.keyName"} - %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.keyName}}", "value" => "{{key.name}}"} - {{key.name}} + %div{"ng-if" => "!provider.keyName || provider.showKeyForm"} + %strong Choose encryption key: + %select{"ng-model" => "provider.formData.keyName"} + %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"} + {{key.name}} + %button{"ng-click" => "saveKey(provider)"} Set %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Enable as Primary sync provider %button.light{"ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider - %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key + %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider %a{"ng-click" => "newSyncData.showAddSyncForm = !newSyncData.showAddSyncForm"} Add external sync with Secret URL %form.sync-form{"ng-if" => "newSyncData.showAddSyncForm"} diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index efa0873ad..e1119a642 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -12,38 +12,22 @@ %label %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension)"} Decrypted - .ek-input-wrapper{"ng-if" => "extension.encrypted && (!extensionManager.ekForExtension(extension) || extension.formData.changingKey)"} - %input{"ng-model" => "extension.formData.ek", "placeholder" => "Set encryption key"} - %button.light{"ng-click" => "setEncryptionKeyForExtension(extension)"} Set .extension-actions .action{"ng-repeat" => "action in extension.actionsInGlobalContext()"} - %div{"ng-if" => "!action.sync_provider"} - .action-name {{action.label}} - .action-desc{"style" => "font-style: italic;"} {{action.desc}} - .execute-type{"ng-if" => "action.repeat_mode == 'watch'"} - Repeats when a change is made to your items. - .execute-type{"ng-if" => "action.repeat_mode == 'loop'"} - Repeats at most once every {{action.repeat_timeout}} seconds - .action-permissions - %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} - %div{"ng-if" => "action.showPermissions"} - {{action.permissionsString}} - .encryption-type - %span {{action.encryptionModeString}} - %div{"ng-if" => "action.sync_provider"} - .action-name This is a sync provider action. - .action-desc{"style" => "margin-top: -5px;"} - %p Enabling this sync provider as a primary provider will replace your current sync provider. - %p Enabling this sync provider as a secondary provider will save your data with this provider as a backup, but will not be used to pull changes. - %p You can have only one primary provider, and multiple secondary providers. + .action-name {{action.label}} + .action-desc{"style" => "font-style: italic;"} {{action.desc}} + .execute-type{"ng-if" => "action.repeat_mode == 'watch'"} + Repeats when a change is made to your items. + .execute-type{"ng-if" => "action.repeat_mode == 'loop'"} + Repeats at most once every {{action.repeat_timeout}} seconds + .action-permissions + %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} + %div{"ng-if" => "action.showPermissions"} + {{action.permissionsString}} + .encryption-type + %span {{action.encryptionModeString}} - %div{"ng-if" => "!syncProviderActionIsEnabled(action)"} - %button.light.execute{"ng-click" => "enableSyncProvider(action, extension, true)"} Enable as primary sync provider - %button.light.execute{"ng-click" => "enableSyncProvider(action, extension, false)"} Enable as backup sync provider - %div{"ng-if" => "syncProviderActionIsEnabled(action)"} - %button.light.execute{"ng-click" => "disableSyncProvider(action, extension)"} Remove as sync provider - - .execute{"ng-if" => "!action.sync_provider"} + .execute %div{"ng-if" => "action.repeat_mode"} %div{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension)"} Disable %div{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable @@ -57,14 +41,6 @@ .error{"ng-if" => "action.error"} Error performing action. - - %a.option-link{"ng-if" => "extension.encrypted && extensionManager.ekForExtension(extension) && !extension.formData.changingKey", "ng-click" => "extension.formData.showEk = !extension.formData.showEk"} - Show Encryption Key - .show-ek{"style" => "text-align: center", "ng-if" => "extension.formData.showEk"} - .ek {{extensionManager.ekForExtension(extension)}} - .disclaimer This key is saved locally and never sent to any servers. - %a.option-link{"ng-if" => "extension.encrypted && extensionManager.ekForExtension(extension) && !extension.formData.changingKey", "ng-click" => "changeEncryptionKeyPressed(extension)"} - Change Encryption Key %a.option-link{"ng-click" => "deleteExtension(extension)"} Remove extension .extension-link From b2200c5707de17b839eee26b3dfad65464b87c73 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 25 Jan 2017 19:44:48 -0600 Subject: [PATCH 04/25] sync provider wip --- .../app/frontend/controllers/editor.js | 9 +- .../app/frontend/controllers/header.js | 2 +- .../app/frontend/controllers/home.js | 6 +- .../app/frontend/controllers/notes.js | 10 +-- .../app/frontend/models/local/itemParams.js | 16 ++-- .../javascripts/app/services/apiController.js | 54 +++++------- .../services/directives/accountDataMenu.js | 9 ++ .../services/directives/accountSyncSection.js | 15 +++- .../directives/accountVendorAccountSection.js | 6 +- .../services/directives/importExportMenu.js | 2 +- .../app/services/sync/syncManager.js | 82 +++++++------------ .../app/services/sync/syncProvider.js | 43 ++++++++++ .../sync/{syncPerformer.js => syncRunner.js} | 38 +++++---- .../directives/account-data-menu.html.haml | 2 + .../directives/account-sync-section.html.haml | 18 ++-- .../account-vendor-account-section.html.haml | 3 +- .../templates/frontend/header.html.haml | 3 +- 17 files changed, 173 insertions(+), 145 deletions(-) create mode 100644 app/assets/javascripts/app/services/sync/syncProvider.js rename app/assets/javascripts/app/services/sync/{syncPerformer.js => syncRunner.js} (82%) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index a12a75394..a4eeafd21 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -20,7 +20,7 @@ angular.module('app.frontend') * Insert 4 spaces when a tab key is pressed, * only used when inside of the text editor. * If the shift key is pressed first, this event is - * not fired. + * not fired. */ var handleTab = function (event) { if (!event.shiftKey && event.which == 9) { @@ -146,8 +146,6 @@ angular.module('app.frontend') note.dummy = false; this.save()(note, function(success){ if(success) { - apiController.clearDraft(); - if(statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function(){ this.noteStatus = "All changes saved" @@ -171,10 +169,6 @@ angular.module('app.frontend') this.changesMade = function() { this.note.hasChanges = true; this.note.dummy = false; - if(apiController.isUserSignedIn()) { - // signed out users have local autosave, dont need draft saving - apiController.saveDraftToDisk(this.note); - } if(saveTimeout) $timeout.cancel(saveTimeout); if(statusTimeout) $timeout.cancel(statusTimeout); @@ -236,7 +230,6 @@ angular.module('app.frontend') } this.deleteNote = function() { - apiController.clearDraft(); this.remove()(this.note); this.showMenu = false; } diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js index b765b2a63..99b901b80 100644 --- a/app/assets/javascripts/app/frontend/controllers/header.js +++ b/app/assets/javascripts/app/frontend/controllers/header.js @@ -21,7 +21,7 @@ angular.module('app.frontend') this.user = apiController.user; this.accountMenuPressed = function() { - this.serverData = {url: apiController.getServer()}; + this.serverData = {url: syncManager.serverURL()}; this.showAccountMenu = !this.showAccountMenu; this.showFaq = false; this.showNewPasswordForm = false; diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index 9d03da457..7f46bd22c 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -7,9 +7,9 @@ angular.module('app.frontend') syncManager.sync(null); // refresh every 30s - setInterval(function () { - syncManager.sync(null); - }, 30000); + // setInterval(function () { + // syncManager.sync(null); + // }, 30000); }); $scope.allTag = new Tag({all: true}); diff --git a/app/assets/javascripts/app/frontend/controllers/notes.js b/app/assets/javascripts/app/frontend/controllers/notes.js index 111eb95d8..0a9944c2e 100644 --- a/app/assets/javascripts/app/frontend/controllers/notes.js +++ b/app/assets/javascripts/app/frontend/controllers/notes.js @@ -48,14 +48,8 @@ angular.module('app.frontend') if(isFirstLoad) { $timeout(function(){ - var draft = apiController.getDraft(); - if(draft) { - var note = draft; - this.selectNote(note); - } else { - this.createNewNote(); - isFirstLoad = false; - } + this.createNewNote(); + isFirstLoad = false; }.bind(this)) } else if(tag.notes.length == 0) { this.createNewNote(); diff --git a/app/assets/javascripts/app/frontend/models/local/itemParams.js b/app/assets/javascripts/app/frontend/models/local/itemParams.js index 614f670a6..033360956 100644 --- a/app/assets/javascripts/app/frontend/models/local/itemParams.js +++ b/app/assets/javascripts/app/frontend/models/local/itemParams.js @@ -7,7 +7,7 @@ class ItemParams { } paramsForExportFile() { - this.additionalFields = ["created_at", "updated_at"]; + this.additionalFields = ["updated_at"]; this.forExportFile = true; return _.omit(this.__params(), ["deleted"]); } @@ -16,16 +16,22 @@ class ItemParams { return this.paramsForExportFile(); } + paramsForLocalStorage() { + this.additionalFields = ["updated_at", "dirty"]; + this.forExportFile = true; + return this.__params(); + } + paramsForSync() { - return __params(null, false); + return this.__params(null, false); } __params() { var itemCopy = _.cloneDeep(this.item); - console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy) + console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy) - var params = {uuid: item.uuid, content_type: item.content_type, deleted: item.deleted}; + var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at}; if(this.ek) { this.encryptionHelper.encryptItem(itemCopy, this.ek); @@ -42,7 +48,7 @@ class ItemParams { } if(this.additionalFields) { - _.merge(params, _.pick(item, this.additionalFields)); + _.merge(params, _.pick(this.item, this.additionalFields)); } return params; diff --git a/app/assets/javascripts/app/services/apiController.js b/app/assets/javascripts/app/services/apiController.js index c119cf1ec..bfd1b4007 100644 --- a/app/assets/javascripts/app/services/apiController.js +++ b/app/assets/javascripts/app/services/apiController.js @@ -28,18 +28,10 @@ angular.module('app.frontend') Auth */ - this.defaultServerURL = function() { - return localStorage.getItem("server") || "https://n3.standardnotes.org"; - } - this.getAuthParams = function() { return JSON.parse(localStorage.getItem("auth_params")); } - this.isUserSignedIn = function() { - return localStorage.getItem("jwt"); - } - this.getAuthParamsForEmail = function(url, email, callback) { var requestUrl = url + "/auth/params"; var request = Restangular.oneUrl(requestUrl, requestUrl); @@ -103,7 +95,7 @@ angular.module('app.frontend') localStorage.setItem("jwt", response.token); localStorage.setItem("user", JSON.stringify(response.user)); localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); - syncManager.addKey(syncManager.SNKeyName, mk); + keyManager.addKey(SNKeyName, mk); syncManager.addStandardFileSyncProvider(url); } @@ -119,6 +111,7 @@ angular.module('app.frontend') callback(response); }.bind(this)) .catch(function(response){ + console.log("Registration error", response); callback(response.data); }) }.bind(this)); @@ -251,7 +244,7 @@ angular.module('app.frontend') items: items } - if(ek.name == syncManager.SNKeyName) { + if(ek.name == SNKeyName) { // auth params are only needed when encrypted with a standard file key data["auth_params"] = this.getAuthParams(); } @@ -263,34 +256,27 @@ angular.module('app.frontend') return JSON.parse(JSON.stringify(object)); } - - /* - Drafts - */ - - this.saveDraftToDisk = function(draft) { - localStorage.setItem("draft", JSON.stringify(draft)); - } - - this.clearDraft = function() { - localStorage.removeItem("draft"); - } - - this.getDraft = function() { - var draftString = localStorage.getItem("draft"); - if(!draftString || draftString == 'undefined') { - return null; - } - var jsonObj = _.merge({content_type: "Note"}, JSON.parse(draftString)); - return modelManager.createItem(jsonObj); - } - - this.signoutOfStandardFile = function(callback) { + this.signoutOfStandardFile = function(destroyAll, callback) { syncManager.removeStandardFileSyncProvider(); + if(destroyAll) { + this.destroyLocalData(callback); + } else { + localStorage.removeItem("user"); + localStorage.removeItem("jwt"); + localStorage.removeItem("server"); + localStorage.removeItem("auth_params"); + callback(); + } + } + + this.destroyLocalData = function(callback) { dbManager.clearAllItems(function(){ localStorage.clear(); - callback(); + if(callback) { + callback(); + } }); } + } }); diff --git a/app/assets/javascripts/app/services/directives/accountDataMenu.js b/app/assets/javascripts/app/services/directives/accountDataMenu.js index 91a032c2a..a1c4a9d11 100644 --- a/app/assets/javascripts/app/services/directives/accountDataMenu.js +++ b/app/assets/javascripts/app/services/directives/accountDataMenu.js @@ -10,6 +10,15 @@ class AccountDataMenu { controller($scope, apiController, modelManager) { 'ngInject'; + $scope.destroyLocalData = function() { + if(!confirm("Are you sure you want to end your session? This will delete all local items, sync providers, keys, and extensions.")) { + return; + } + + apiController.destroyLocalData(function(){ + window.location.reload(); + }) + } } } diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js index e690e8940..ea839c34a 100644 --- a/app/assets/javascripts/app/services/directives/accountSyncSection.js +++ b/app/assets/javascripts/app/services/directives/accountSyncSection.js @@ -31,10 +31,22 @@ class AccountSyncSection { } $scope.removeSyncProvider = function(provider) { - syncManager.removeSyncProvider(provider); + if(provider.isStandardNotesAccount) { + alert("To remove your Standard Notes sync, sign out of your Standard Notes account.") + return; + } + + if(confirm("Are you sure you want to remove this sync provider?")) { + syncManager.removeSyncProvider(provider); + } } $scope.changeEncryptionKey = function(provider) { + if(provider.isStandardNotesAccount) { + alert("To change your encryption key for your Standard Notes account, you need to change your password. However, this functionality is not currently supported."); + return; + } + if(!confirm("Changing your encryption key will re-encrypt all your notes with the new key and sync them back to the server. This can take several minutes. We strongly recommend downloading a backup of your notes before continuing.")) { return; } @@ -46,6 +58,7 @@ class AccountSyncSection { $scope.saveKey = function(provider) { provider.showKeyForm = false; provider.keyName = provider.formData.keyName; + syncManager.didMakeChangesToSyncProviders(); if(provider.enabled) { syncManager.addAllDataAsNeedingSyncForProvider(provider); diff --git a/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js b/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js index 0b73f6c5e..789078619 100644 --- a/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js +++ b/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js @@ -7,10 +7,10 @@ class AccountVendorAccountSection { }; } - controller($scope, apiController, modelManager, $timeout, dbManager) { + controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) { 'ngInject'; - $scope.loginData = {mergeLocal: true, url: apiController.defaultServerURL()}; + $scope.loginData = {mergeLocal: true, url: syncManager.serverURL()}; $scope.user = apiController.user; $scope.changePasswordPressed = function() { @@ -19,7 +19,7 @@ class AccountVendorAccountSection { $scope.signOutPressed = function() { $scope.showAccountMenu = false; - apiController.signoutOfStandardFile(function(){ + apiController.signoutOfStandardFile(false, function(){ window.location.reload(); }) } diff --git a/app/assets/javascripts/app/services/directives/importExportMenu.js b/app/assets/javascripts/app/services/directives/importExportMenu.js index 59654399a..fe96806e1 100644 --- a/app/assets/javascripts/app/services/directives/importExportMenu.js +++ b/app/assets/javascripts/app/services/directives/importExportMenu.js @@ -14,7 +14,7 @@ class ImportExportMenu { $scope.user = apiController.user; $scope.downloadDataArchive = function() { - if(!apiController.isUserSignedIn() && $scope.archiveFormData.encryption_type == 'ek') { + if($scope.archiveFormData.encryption_type == 'ek') { if(!$scope.archiveFormData.ek) { alert("You must set an encryption key to export the data encrypted.") return; diff --git a/app/assets/javascripts/app/services/sync/syncManager.js b/app/assets/javascripts/app/services/sync/syncManager.js index aec4a5ffd..d9603fedf 100644 --- a/app/assets/javascripts/app/services/sync/syncManager.js +++ b/app/assets/javascripts/app/services/sync/syncManager.js @@ -1,26 +1,38 @@ +export const SNKeyName = "Standard Notes Key"; + class SyncManager { - constructor(modelManager) { + constructor(modelManager, syncRunner) { this.modelManager = modelManager; - this.syncPerformer = new SyncPerformer() - this.SNKeyName = "Standard Notes Key"; + this.syncRunner = syncRunner; + this.syncRunner.setOnChangeProviderCallback(function(){ + this.didMakeChangesToSyncProviders(); + }.bind(this)) this.loadSyncProviders(); } get offline() { - return this.syncProviders.length == 0; + return this.enabledProviders.length == 0; + } + + serverURL() { + return localStorage.getItem("server") || "https://n3.standardnotes.org"; + } + + get enabledProviders() { + return this.syncProviders.filter(function(provider){return provider.enabled == true}); } sync(callback) { - this.syncPerformer.sync(this.syncProviders, callback); + this.syncRunner.sync(this.enabledProviders, callback); } syncWithProvider(provider, callback) { - this.syncPerformer.performSyncWithProvider(provider, callback); + this.syncRunner.performSyncWithProvider(provider, callback); } loadLocalItems(callback) { - this.syncPerformer.loadLocalItems(callback); + this.syncRunner.loadLocalItems(callback); } syncProviderForURL(url) { @@ -46,16 +58,17 @@ class SyncManager { } removeStandardFileSyncProvider() { - var sfProvider = _.find(this.syncProviders, {url: this.defaultServerURL() + "/items/sync"}) + var sfProvider = _.find(this.syncProviders, {url: this.serverURL() + "/items/sync"}) _.pull(this.syncProviders, sfProvider); this.didMakeChangesToSyncProviders(); } addStandardFileSyncProvider(url) { - var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.syncProviders.length == 0}); - defaultProvider.keyName = this.SNKeyName; + var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.enabledProviders.length == 0}); + defaultProvider.keyName = SNKeyName; defaultProvider.enabled = this.syncProviders.length == 0; this.syncProviders.push(defaultProvider); + this.didMakeChangesToSyncProviders(); return defaultProvider; } @@ -74,14 +87,15 @@ class SyncManager { this.syncProviders.push(new SyncProvider(p)); } } else { - // no providers saved, use default - if(this.isUserSignedIn()) { - var defaultProvider = this.addStandardFileSyncProvider(this.defaultServerURL()); + // no providers saved, this means migrating from old system to new + // check if user is signed in + if(this.offline && localStorage.getItem("user")) { + var defaultProvider = this.addStandardFileSyncProvider(this.serverURL()); defaultProvider.syncToken = localStorage.getItem("syncToken"); // migrate old key structure to new var mk = localStorage.getItem("mk"); if(mk) { - keyManager.addKey(this.SNKeyName, mk); + keyManager.addKey(SNKeyName, mk); localStorage.removeItem("mk"); } this.didMakeChangesToSyncProviders(); @@ -129,43 +143,3 @@ class SyncManager { } angular.module('app.frontend').service('syncManager', SyncManager); - -class SyncProvider { - - constructor(obj) { - this.encrypted = true; - _.merge(this, obj); - } - - addPendingItems(items) { - if(!this.pendingItems) { - this.pendingItems = []; - } - - this.pendingItems = this.pendingItems.concat(items); - } - - removePendingItems(items) { - this.pendingItems = _.difference(this.pendingItems, items); - } - - get status() { - if(!this.enabled) { - return null; - } - - if(this.primary) return "primary"; - else return "secondary"; - } - - asJSON() { - return { - enabled: this.enabled, - url: this.url, - primary: this.primary, - keyName: this.keyName, - syncToken: this.syncToken - } - } - -} diff --git a/app/assets/javascripts/app/services/sync/syncProvider.js b/app/assets/javascripts/app/services/sync/syncProvider.js new file mode 100644 index 000000000..79055803c --- /dev/null +++ b/app/assets/javascripts/app/services/sync/syncProvider.js @@ -0,0 +1,43 @@ +class SyncProvider { + + constructor(obj) { + this.encrypted = true; + _.merge(this, obj); + } + + addPendingItems(items) { + if(!this.pendingItems) { + this.pendingItems = []; + } + + this.pendingItems = this.pendingItems.concat(items); + } + + removePendingItems(items) { + this.pendingItems = _.difference(this.pendingItems, items); + } + + get isStandardNotesAccount() { + return this.keyName == SNKeyName; + } + + get status() { + if(!this.enabled) { + return null; + } + + if(this.primary) return "primary"; + else return "secondary"; + } + + asJSON() { + return { + enabled: this.enabled, + url: this.url, + primary: this.primary, + keyName: this.keyName, + syncToken: this.syncToken + } + } + +} diff --git a/app/assets/javascripts/app/services/sync/syncPerformer.js b/app/assets/javascripts/app/services/sync/syncRunner.js similarity index 82% rename from app/assets/javascripts/app/services/sync/syncPerformer.js rename to app/assets/javascripts/app/services/sync/syncRunner.js index e475dc701..0d1b809e9 100644 --- a/app/assets/javascripts/app/services/sync/syncPerformer.js +++ b/app/assets/javascripts/app/services/sync/syncRunner.js @@ -1,10 +1,12 @@ -class SyncPerformer { +class SyncRunner { - constructor(modelManager, dbManager, encryptionHelper, keyManager) { + constructor($rootScope, modelManager, dbManager, encryptionHelper, keyManager, Restangular) { + this.rootScope = $rootScope; this.modelManager = modelManager; this.dbManager = dbManager; this.encryptionHelper = encryptionHelper; this.keyManager = keyManager; + this.Restangular = Restangular; } setOnChangeProviderCallback(callback) { @@ -15,9 +17,14 @@ class SyncPerformer { this.onChangeProviderCallback(provider); } - writeItemsToLocalStorage(items, callback) { + writeItemsToLocalStorage(items, offlineOnly, callback) { var params = items.map(function(item) { - return this.paramsForItem(item, false, ["created_at", "updated_at", "dirty"], true) + var itemParams = new ItemParams(item, null); + itemParams = itemParams.paramsForLocalStorage(); + if(offlineOnly) { + delete itemParams.dirty; + } + return itemParams; }.bind(this)); this.dbManager.saveItems(params, callback); @@ -32,7 +39,8 @@ class SyncPerformer { } syncOffline(items, callback) { - this.writeItemsToLocalStorage(items, function(responseItems){ + console.log("Writing items offline", items); + this.writeItemsToLocalStorage(items, true, function(responseItems){ // delete anything needing to be deleted for(var item of items) { if(item.deleted) { @@ -57,10 +65,6 @@ class SyncPerformer { } for(let provider of providers) { - if(!provider.enabled) { - continue; - } - provider.addPendingItems(allDirtyItems); this.didMakeChangesToSyncProvider(provider); @@ -100,13 +104,13 @@ class SyncPerformer { provider.repeatOnCompletion = false; } - console.log("Syncing with provider", provider, subItems); + console.log("Syncing with provider:", provider.url, "items:", subItems.length); // Remove dirty items now. If this operation fails, we'll re-add them. // This allows us to queue changes on the same item provider.removePendingItems(subItems); - var request = Restangular.oneUrl(provider.url, provider.url); + var request = this.Restangular.oneUrl(provider.url, provider.url); request.limit = 150; request.items = _.map(subItems, function(item){ var itemParams = new ItemParams(item, provider.ek); @@ -119,12 +123,12 @@ class SyncPerformer { request.post().then(function(response) { - console.log("Sync completion", response); + console.log("Completed sync for provider:", provider.url, "Response:", response); provider.syncToken = response.sync_token; if(provider.primary) { - $rootScope.$broadcast("sync:updated_token", provider.syncToken); + this.rootScope.$broadcast("sync:updated_token", provider.syncToken); // handle cursor token (more results waiting, perform another sync) provider.cursorToken = response.cursor_token; @@ -136,8 +140,8 @@ class SyncPerformer { this.handleUnsavedItemsResponse(response.unsaved, provider) - this.writeItemsToLocalStorage(saved, null); - this.writeItemsToLocalStorage(retrieved, null); + this.writeItemsToLocalStorage(saved, false, null); + this.writeItemsToLocalStorage(retrieved, false, null); } provider.syncOpInProgress = false; @@ -159,7 +163,7 @@ class SyncPerformer { provider.syncOpInProgress = false; if(provider.primary) { - this.writeItemsToLocalStorage(allItems, null); + this.writeItemsToLocalStorage(allItems, false, null); } if(callback) { @@ -194,3 +198,5 @@ class SyncPerformer { return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); } } + +angular.module('app.frontend').service('syncRunner', SyncRunner); diff --git a/app/assets/templates/frontend/directives/account-data-menu.html.haml b/app/assets/templates/frontend/directives/account-data-menu.html.haml index d4d7447a8..123ba1c9c 100644 --- a/app/assets/templates/frontend/directives/account-data-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-data-menu.html.haml @@ -16,3 +16,5 @@ %section.account-item %h3{"ng-click" => "showIO = !showIO"} Import/Export %import-export-menu{"ng-if" => "showIO"} + + %a{"ng-click" => "destroyLocalData()"} Destroy all local data diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml index afaed9829..b16189b48 100644 --- a/app/assets/templates/frontend/directives/account-sync-section.html.haml +++ b/app/assets/templates/frontend/directives/account-sync-section.html.haml @@ -1,18 +1,20 @@ .providers .provider{"ng-repeat" => "provider in syncProviders"} - .type {{provider.primary == null ? 'Not enabled' : (provider.primary ? 'Primary' : 'Secondary')}} + .type {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Primary' : 'Secondary')}} .key{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} .url {{provider.url}} .options - %div{"ng-if" => "!provider.enabled"} - %div{"ng-if" => "!provider.keyName || provider.showKeyForm"} + %div{"ng-if" => "!provider.keyName || provider.showKeyForm"} + %p %strong Choose encryption key: - %select{"ng-model" => "provider.formData.keyName"} - %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"} - {{key.name}} - %button{"ng-click" => "saveKey(provider)"} Set + %select{"ng-model" => "provider.formData.keyName"} + %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"} + {{key.name}} + %button{"ng-click" => "saveKey(provider)"} Set + + %div{"ng-if" => "!provider.enabled"} %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Enable as Primary sync provider - %button.light{"ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider + %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider diff --git a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml index 3b0ac24de..1800ab39f 100644 --- a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml +++ b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml @@ -4,7 +4,8 @@ .server {{serverURL}} .links{"ng-if" => "user"} .link-item - %a{"ng-click" => "signOutPressed()"} Sign Out + %a{"ng-click" => "signOutPressed()"} Sign out + %p Note: Signing out does not delete your local items, extensions, and keys. .meta-container .title Local Encryption .desc Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes. diff --git a/app/assets/templates/frontend/header.html.haml b/app/assets/templates/frontend/header.html.haml index 67106684f..7004a8fad 100644 --- a/app/assets/templates/frontend/header.html.haml +++ b/app/assets/templates/frontend/header.html.haml @@ -5,8 +5,7 @@ .items .item.account %a{"ng-click" => "ctrl.accountMenuPressed()"} Data - %account-data-menu - -# {"ng-if" => "ctrl.showAccountMenu"} + %account-data-menu{"ng-if" => "ctrl.showAccountMenu"} .item %a{"ng-click" => "ctrl.toggleExtensions()"} Extensions From 5141b963bc1803a2357331ce3d2131f175467513 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 25 Jan 2017 21:58:32 -0600 Subject: [PATCH 05/25] sync status --- .../services/directives/accountDataMenu.js | 6 ++- .../services/directives/accountSyncSection.js | 2 - .../app/services/directives/delay-hide.js | 42 +++++++++++++++++++ .../app/services/sync/syncManager.js | 2 + .../app/services/sync/syncProvider.js | 12 +++++- .../app/services/sync/syncRunner.js | 15 ++++++- app/assets/stylesheets/app/_directives.scss | 5 +++ .../directives/account-data-menu.html.haml | 2 +- .../directives/account-sync-section.html.haml | 3 ++ 9 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/app/services/directives/delay-hide.js diff --git a/app/assets/javascripts/app/services/directives/accountDataMenu.js b/app/assets/javascripts/app/services/directives/accountDataMenu.js index a1c4a9d11..2d5c5fedf 100644 --- a/app/assets/javascripts/app/services/directives/accountDataMenu.js +++ b/app/assets/javascripts/app/services/directives/accountDataMenu.js @@ -7,14 +7,16 @@ class AccountDataMenu { }; } - controller($scope, apiController, modelManager) { + controller($scope, apiController, modelManager, keyManager) { 'ngInject'; + $scope.keys = keyManager.keys; + $scope.destroyLocalData = function() { if(!confirm("Are you sure you want to end your session? This will delete all local items, sync providers, keys, and extensions.")) { return; } - + apiController.destroyLocalData(function(){ window.location.reload(); }) diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js index ea839c34a..bccec8351 100644 --- a/app/assets/javascripts/app/services/directives/accountSyncSection.js +++ b/app/assets/javascripts/app/services/directives/accountSyncSection.js @@ -26,8 +26,6 @@ class AccountSyncSection { } syncManager.enableSyncProvider(provider, primary); - syncManager.addAllDataAsNeedingSyncForProvider(provider); - syncManager.sync(); } $scope.removeSyncProvider = function(provider) { diff --git a/app/assets/javascripts/app/services/directives/delay-hide.js b/app/assets/javascripts/app/services/directives/delay-hide.js new file mode 100644 index 000000000..6bd3b6c0b --- /dev/null +++ b/app/assets/javascripts/app/services/directives/delay-hide.js @@ -0,0 +1,42 @@ +angular + .module('app.frontend') + .directive('delayHide', function($timeout) { + return { + restrict: 'A', + scope: { + show: '=', + delay: '@' + }, + link: function(scope, elem, attrs) { + var showTimer; + + //This is where all the magic happens! + // Whenever the scope variable updates we simply + // show if it evaluates to 'true' and hide if 'false' + scope.$watch('show', function(newVal){ + console.log("show value changed", newVal); + newVal ? showSpinner() : hideSpinner(); + }); + + function showSpinner() { + showElement(true); + } + + function hideSpinner() { + $timeout(showElement.bind(this, false), getDelay()); + } + + function showElement(show) { + console.log("show:", show); + show ? elem.css({display:''}) : elem.css({display:'none'}); + } + + function getDelay() { + var delay = parseInt(scope.delay); + + return angular.isNumber(delay) ? delay : 200; + } + } + + }; +}); diff --git a/app/assets/javascripts/app/services/sync/syncManager.js b/app/assets/javascripts/app/services/sync/syncManager.js index d9603fedf..c0d32ab6c 100644 --- a/app/assets/javascripts/app/services/sync/syncManager.js +++ b/app/assets/javascripts/app/services/sync/syncManager.js @@ -123,6 +123,8 @@ class SyncManager { this.addAllDataAsNeedingSyncForProvider(syncProvider); this.didMakeChangesToSyncProviders(); this.syncWithProvider(syncProvider); + this.syncWithProvider(syncProvider); + this.syncWithProvider(syncProvider); } addAllDataAsNeedingSyncForProvider(syncProvider) { diff --git a/app/assets/javascripts/app/services/sync/syncProvider.js b/app/assets/javascripts/app/services/sync/syncProvider.js index 79055803c..0e197e8d9 100644 --- a/app/assets/javascripts/app/services/sync/syncProvider.js +++ b/app/assets/javascripts/app/services/sync/syncProvider.js @@ -2,6 +2,7 @@ class SyncProvider { constructor(obj) { this.encrypted = true; + this.syncStatus = new SyncStatus(); _.merge(this, obj); } @@ -39,5 +40,14 @@ class SyncProvider { syncToken: this.syncToken } } - +} + +class SyncStatus { + constructor() { + + } + + get statusString() { + return `${this.current}/${this.total}` + } } diff --git a/app/assets/javascripts/app/services/sync/syncRunner.js b/app/assets/javascripts/app/services/sync/syncRunner.js index 0d1b809e9..4172e3a8e 100644 --- a/app/assets/javascripts/app/services/sync/syncRunner.js +++ b/app/assets/javascripts/app/services/sync/syncRunner.js @@ -91,6 +91,8 @@ class SyncRunner { return; } + // whether this is a repeat sync (a continuation from another sync; we use this to update status accurately) + var isRepeatRun = provider.repeatOnCompletion; provider.syncOpInProgress = true; @@ -104,6 +106,11 @@ class SyncRunner { provider.repeatOnCompletion = false; } + if(!isRepeatRun) { + provider.syncStatus.total = allItems.length; + provider.syncStatus.current = 0; + } + console.log("Syncing with provider:", provider.url, "items:", subItems.length); // Remove dirty items now. If this operation fails, we'll re-add them. @@ -123,7 +130,9 @@ class SyncRunner { request.post().then(function(response) { - console.log("Completed sync for provider:", provider.url, "Response:", response); + if(!provider.primary) { + console.log("Completed sync for provider:", provider.url, "Response:", response); + } provider.syncToken = response.sync_token; @@ -145,6 +154,10 @@ class SyncRunner { } provider.syncOpInProgress = false; + if(!provider.primary) { + console.log("Adding", subItems.length, "to", provider.syncStatus.current); + } + provider.syncStatus.current += subItems.length; if(provider.cursorToken || provider.repeatOnCompletion == true) { this.__performSyncWithProvider(provider, options, callback); diff --git a/app/assets/stylesheets/app/_directives.scss b/app/assets/stylesheets/app/_directives.scss index 6037d8051..1f06542c4 100644 --- a/app/assets/stylesheets/app/_directives.scss +++ b/app/assets/stylesheets/app/_directives.scss @@ -242,6 +242,11 @@ Extensions margin-top: 4px; } + .sync-status { + font-weight: bold; + height: 30px; + } + > .options { margin-top: 10px; } diff --git a/app/assets/templates/frontend/directives/account-data-menu.html.haml b/app/assets/templates/frontend/directives/account-data-menu.html.haml index 123ba1c9c..195d82343 100644 --- a/app/assets/templates/frontend/directives/account-data-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-data-menu.html.haml @@ -10,7 +10,7 @@ %account-sync-section{"ng-if" => "showSync"} %section.account-item - %h3{"ng-click" => "showKeys = !showKeys"} Encryption Keys + %h3{"ng-click" => "showKeys = !showKeys"} Encryption Keys ({{keys.length}}) %account-keys-section{"ng-if" => "showKeys"} %section.account-item diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml index b16189b48..856fde971 100644 --- a/app/assets/templates/frontend/directives/account-sync-section.html.haml +++ b/app/assets/templates/frontend/directives/account-sync-section.html.haml @@ -17,6 +17,9 @@ %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider + .sync-status{"delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} + .text{"style" => "float: left;"} Syncing: {{provider.syncStatus.statusString}} + .spinner{"style" => "float: right"} %a{"ng-click" => "newSyncData.showAddSyncForm = !newSyncData.showAddSyncForm"} Add external sync with Secret URL %form.sync-form{"ng-if" => "newSyncData.showAddSyncForm"} From ac5569429d0afb06ca3dbebb4d491699571861c3 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 26 Jan 2017 01:00:47 -0600 Subject: [PATCH 06/25] css --- .../services/directives/accountDataMenu.js | 3 +- .../app/services/directives/delay-hide.js | 2 + app/assets/stylesheets/app/_directives.scss | 15 ++---- app/assets/stylesheets/app/_header.scss | 19 ++++++- .../directives/account-data-menu.html.haml | 12 ++--- .../directives/account-keys-section.html.haml | 4 +- .../directives/account-sync-section.html.haml | 4 +- .../account-vendor-account-section.html.haml | 4 +- .../directives/import-export-menu.html.haml | 51 ++++++++++--------- .../templates/frontend/header.html.haml | 2 +- 10 files changed, 66 insertions(+), 50 deletions(-) diff --git a/app/assets/javascripts/app/services/directives/accountDataMenu.js b/app/assets/javascripts/app/services/directives/accountDataMenu.js index 2d5c5fedf..3a34027de 100644 --- a/app/assets/javascripts/app/services/directives/accountDataMenu.js +++ b/app/assets/javascripts/app/services/directives/accountDataMenu.js @@ -7,10 +7,11 @@ class AccountDataMenu { }; } - controller($scope, apiController, modelManager, keyManager) { + controller($scope, apiController, modelManager, keyManager, syncManager) { 'ngInject'; $scope.keys = keyManager.keys; + $scope.syncProviders = syncManager.syncProviders; $scope.destroyLocalData = function() { if(!confirm("Are you sure you want to end your session? This will delete all local items, sync providers, keys, and extensions.")) { diff --git a/app/assets/javascripts/app/services/directives/delay-hide.js b/app/assets/javascripts/app/services/directives/delay-hide.js index 6bd3b6c0b..732e9b4f0 100644 --- a/app/assets/javascripts/app/services/directives/delay-hide.js +++ b/app/assets/javascripts/app/services/directives/delay-hide.js @@ -10,6 +10,8 @@ angular link: function(scope, elem, attrs) { var showTimer; + showElement(false); + //This is where all the magic happens! // Whenever the scope variable updates we simply // show if it evaluates to 'true' and hide if 'false' diff --git a/app/assets/stylesheets/app/_directives.scss b/app/assets/stylesheets/app/_directives.scss index 1f06542c4..c79567e9f 100644 --- a/app/assets/stylesheets/app/_directives.scss +++ b/app/assets/stylesheets/app/_directives.scss @@ -96,8 +96,6 @@ Extensions .extension { margin-bottom: 18px; - background-color: #f6f6f6; - border: 1px solid #f2f2f2; padding: 14px 6px; padding-bottom: 8px; color: black; @@ -224,8 +222,6 @@ Extensions font-size: 12px; .provider { - background-color: #f6f6f6; - border: 1px solid #f2f2f2; padding: 10px 10px; padding-bottom: 8px; margin-bottom: 10px; @@ -235,10 +231,10 @@ Extensions } > .url { - // white-space: nowrap; - // text-overflow: ellipsis; - // overflow: hidden; - word-wrap: break-word; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + // word-wrap: break-word; margin-top: 4px; } @@ -257,10 +253,9 @@ Extensions .account-keys-section { .keys { .key { - background-color: #f6f6f6; - border: 1px solid #f2f2f2; padding: 10px 10px; padding-bottom: 8px; + margin-bottom: 8px; > .name { font-size: 13px; diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index 6d71631c9..3c7153ae2 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -91,6 +91,7 @@ margin-top: 0px; margin-bottom: 4px; font-size: 16px; + width: 100%; } a { @@ -154,6 +155,16 @@ float: left; } +.gray-bg { + background-color: #f6f6f6; + border: 1px solid #f2f2f2; +} + +.white-bg { + background-color: white; + border: 1px solid rgba(gray, 0.2); +} + .item.last-refreshed { font-weight: normal !important; cursor: default !important; @@ -193,9 +204,15 @@ margin-top: 0px; } + .account-section-content { + margin-top: 15px; + } + .account-item { width: 100%; - margin-bottom: 34px; + margin-bottom: 15px; + min-height: 50px; + padding: 20px 14px; a { color: $blue-color; diff --git a/app/assets/templates/frontend/directives/account-data-menu.html.haml b/app/assets/templates/frontend/directives/account-data-menu.html.haml index 195d82343..b05d2cb07 100644 --- a/app/assets/templates/frontend/directives/account-data-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-data-menu.html.haml @@ -1,19 +1,19 @@ .panel.panel-default.account-panel.panel-right.account-data-menu .panel-body .account-items - %section.account-item + %section.account-item.gray-bg{"ng-init" => "showSN = true"} %h3{"ng-click" => "showSN = !showSN"} Standard Notes Account %account-vendor-account-section{"ng-if" => "showSN"} - %section.account-item - %h3{"ng-click" => "showSync = !showSync"} Sync Providers + %section.account-item.gray-bg + %h3{"ng-click" => "showSync = !showSync"} Sync Locations ({{syncProviders.length}}) %account-sync-section{"ng-if" => "showSync"} - %section.account-item - %h3{"ng-click" => "showKeys = !showKeys"} Encryption Keys ({{keys.length}}) + %section.account-item.gray-bg + %h3{"ng-click" => "showKeys = !showKeys"} Encryption Keys ({{keys.length}}) %account-keys-section{"ng-if" => "showKeys"} - %section.account-item + %section.account-item.gray-bg %h3{"ng-click" => "showIO = !showIO"} Import/Export %import-export-menu{"ng-if" => "showIO"} diff --git a/app/assets/templates/frontend/directives/account-keys-section.html.haml b/app/assets/templates/frontend/directives/account-keys-section.html.haml index bdaca70da..3885ba341 100644 --- a/app/assets/templates/frontend/directives/account-keys-section.html.haml +++ b/app/assets/templates/frontend/directives/account-keys-section.html.haml @@ -1,6 +1,6 @@ -.account-keys-section +.account-keys-section.account-section-content .keys - .key{"ng-repeat" => "key in keys"} + .key.white-bg{"ng-repeat" => "key in keys"} .name {{key.name}} .value {{key.key}} diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml index 856fde971..da0047435 100644 --- a/app/assets/templates/frontend/directives/account-sync-section.html.haml +++ b/app/assets/templates/frontend/directives/account-sync-section.html.haml @@ -1,5 +1,5 @@ -.providers - .provider{"ng-repeat" => "provider in syncProviders"} +.providers.account-section-content + .provider.white-bg{"ng-repeat" => "provider in syncProviders"} .type {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Primary' : 'Secondary')}} .key{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} .url {{provider.url}} diff --git a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml index 1800ab39f..9c4528909 100644 --- a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml +++ b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml @@ -1,4 +1,4 @@ -.registration-login +.registration-login.account-section-content %div{"ng-if" => "user"} .email {{user.email}} .server {{serverURL}} @@ -15,7 +15,7 @@ %div{"ng-if" => "!user"} .meta-container .title Sign in or Register (optional) - .desc Enter your Standard File account information. + .desc Enter your Standard Notes account information. .action-container %form.account-form{'name' => "loginForm"} .form-tag.has-feedback diff --git a/app/assets/templates/frontend/directives/import-export-menu.html.haml b/app/assets/templates/frontend/directives/import-export-menu.html.haml index 0a6b5916a..87b52c429 100644 --- a/app/assets/templates/frontend/directives/import-export-menu.html.haml +++ b/app/assets/templates/frontend/directives/import-export-menu.html.haml @@ -1,26 +1,27 @@ -.options{"style" => "font-size: 12px; margin-top: 4px;"} - %label.enc-option{"ng-if" => "user"} - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"} - Encrypted with Standard File key - %label.enc-option - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"} - {{user ? 'Encrypted with custom key' : 'Encrypted' }} - %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"} - %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"} - %label.enc-option - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"} - Decrypted -.action-container - %a{"ng-click" => "downloadDataArchive()"} Download Data Archive +.account-section-content + .options{"style" => "font-size: 12px; margin-top: 4px;"} + %label.enc-option{"ng-if" => "user"} + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"} + Encrypted with Standard File key + %label.enc-option + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"} + {{user ? 'Encrypted with custom key' : 'Encrypted' }} + %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"} + %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"} + %label.enc-option + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"} + Decrypted + .action-container + %a{"ng-click" => "downloadDataArchive()"} Download Data Archive -%div{"ng-if" => "!importData.loading"} - %label#import-archive - %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} - %a.disabled - %span - Import Data from Archive - .import-password{"ng-if" => "importData.requestPassword"} - Enter the account password associated with the import file. - %input.field{"type" => "text", "ng-model" => "importData.password"} - %button{"ng-click" => "submitImportPassword()"} Decrypt & Import -.spinner{"ng-if" => "importData.loading"} + %div{"ng-if" => "!importData.loading"} + %label#import-archive + %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} + %a.disabled + %span + Import Data from Archive + .import-password{"ng-if" => "importData.requestPassword"} + Enter the account password associated with the import file. + %input.field{"type" => "text", "ng-model" => "importData.password"} + %button{"ng-click" => "submitImportPassword()"} Decrypt & Import + .spinner{"ng-if" => "importData.loading"} diff --git a/app/assets/templates/frontend/header.html.haml b/app/assets/templates/frontend/header.html.haml index 7004a8fad..0ab81a3b2 100644 --- a/app/assets/templates/frontend/header.html.haml +++ b/app/assets/templates/frontend/header.html.haml @@ -4,7 +4,7 @@ .items .item.account - %a{"ng-click" => "ctrl.accountMenuPressed()"} Data + %a{"ng-click" => "ctrl.accountMenuPressed()"} Account %account-data-menu{"ng-if" => "ctrl.showAccountMenu"} .item From 191f665dde30c54af84cef8eb40dfa051fa08855 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 26 Jan 2017 01:19:49 -0600 Subject: [PATCH 07/25] key updates --- .../app/services/directives/accountKeysSection.js | 9 +++++++-- app/assets/javascripts/app/services/keyManager.js | 13 +++++++++++-- .../javascripts/app/services/sync/syncManager.js | 2 +- app/assets/stylesheets/app/_directives.scss | 7 ++++++- .../directives/account-keys-section.html.haml | 2 +- .../directives/account-sync-section.html.haml | 6 +++--- 6 files changed, 29 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/app/services/directives/accountKeysSection.js b/app/assets/javascripts/app/services/directives/accountKeysSection.js index f1d30d1fe..0f2566729 100644 --- a/app/assets/javascripts/app/services/directives/accountKeysSection.js +++ b/app/assets/javascripts/app/services/directives/accountKeysSection.js @@ -12,9 +12,14 @@ class AccountKeysSection { $scope.newKeyData = {}; $scope.keys = keyManager.keys; - + $scope.submitNewKeyForm = function() { - keyManager.addKey($scope.newKeyData.name, $scope.newKeyData.key); + var key = keyManager.addKey($scope.newKeyData.name, $scope.newKeyData.key); + if(!key) { + alert("This key name is already in use. Please use a different name."); + return; + } + $scope.newKeyData.showForm = false; } } diff --git a/app/assets/javascripts/app/services/keyManager.js b/app/assets/javascripts/app/services/keyManager.js index 8a874ecf4..292bc6b7a 100644 --- a/app/assets/javascripts/app/services/keyManager.js +++ b/app/assets/javascripts/app/services/keyManager.js @@ -5,12 +5,21 @@ class KeyManager { } addKey(name, key) { - this.keys.push({name: name, key: key}); + var existing = this.keyForName(name); + if(existing) { + return null; + } + + var newKey = {name: name, key: key}; + this.keys.push(newKey); this.persist(); + return newKey; } keyForName(name) { - return _.find(this.keys, {name: name}); + return _.find(this.keys, function(key){ + return key.name.toLowerCase() == name.toLowerCase(); + }); } deleteKey(name) { diff --git a/app/assets/javascripts/app/services/sync/syncManager.js b/app/assets/javascripts/app/services/sync/syncManager.js index c0d32ab6c..9b189fc9e 100644 --- a/app/assets/javascripts/app/services/sync/syncManager.js +++ b/app/assets/javascripts/app/services/sync/syncManager.js @@ -64,7 +64,7 @@ class SyncManager { } addStandardFileSyncProvider(url) { - var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.enabledProviders.length == 0}); + var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: !this.primarySyncProvider()}); defaultProvider.keyName = SNKeyName; defaultProvider.enabled = this.syncProviders.length == 0; this.syncProviders.push(defaultProvider); diff --git a/app/assets/stylesheets/app/_directives.scss b/app/assets/stylesheets/app/_directives.scss index c79567e9f..7a69224da 100644 --- a/app/assets/stylesheets/app/_directives.scss +++ b/app/assets/stylesheets/app/_directives.scss @@ -228,6 +228,11 @@ Extensions > .type { font-weight: bold; + margin-bottom: 2px; + } + + > .key { + font-style: italic; } > .url { @@ -235,7 +240,7 @@ Extensions text-overflow: ellipsis; overflow: hidden; // word-wrap: break-word; - margin-top: 4px; + margin-top: 2px; } .sync-status { diff --git a/app/assets/templates/frontend/directives/account-keys-section.html.haml b/app/assets/templates/frontend/directives/account-keys-section.html.haml index 3885ba341..a85a7a8c5 100644 --- a/app/assets/templates/frontend/directives/account-keys-section.html.haml +++ b/app/assets/templates/frontend/directives/account-keys-section.html.haml @@ -1,6 +1,6 @@ .account-keys-section.account-section-content .keys - .key.white-bg{"ng-repeat" => "key in keys"} + .key.white-bg{"ng-repeat" => "key in keys track by key.name"} .name {{key.name}} .value {{key.key}} diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml index da0047435..270cc0beb 100644 --- a/app/assets/templates/frontend/directives/account-sync-section.html.haml +++ b/app/assets/templates/frontend/directives/account-sync-section.html.haml @@ -1,6 +1,6 @@ .providers.account-section-content .provider.white-bg{"ng-repeat" => "provider in syncProviders"} - .type {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Primary' : 'Secondary')}} + .type {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} .key{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} .url {{provider.url}} .options @@ -13,8 +13,8 @@ %button{"ng-click" => "saveKey(provider)"} Set %div{"ng-if" => "!provider.enabled"} - %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Enable as Primary sync provider - %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider + %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Set as Main + %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider .sync-status{"delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} From ee6ec5bfbc157952320000b976ed8b8c4b754b11 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 26 Jan 2017 13:25:46 -0600 Subject: [PATCH 08/25] css cleanup --- app/assets/stylesheets/app/_directives.scss | 61 --- app/assets/stylesheets/app/_header.scss | 421 +++++++----------- .../directives/account-data-menu.html.haml | 31 +- .../directives/account-keys-section.html.haml | 18 +- .../directives/account-sync-section.html.haml | 52 ++- .../account-vendor-account-section.html.haml | 73 ++- .../directives/import-export-menu.html.haml | 52 +-- .../templates/frontend/header.html.haml | 42 +- 8 files changed, 296 insertions(+), 454 deletions(-) diff --git a/app/assets/stylesheets/app/_directives.scss b/app/assets/stylesheets/app/_directives.scss index 7a69224da..e9a9625bd 100644 --- a/app/assets/stylesheets/app/_directives.scss +++ b/app/assets/stylesheets/app/_directives.scss @@ -210,67 +210,6 @@ Extensions margin-top: 6px; } } - - } - } -} - -.account-data-menu { - padding: 5px !important; - - .providers { - font-size: 12px; - - .provider { - padding: 10px 10px; - padding-bottom: 8px; - margin-bottom: 10px; - - > .type { - font-weight: bold; - margin-bottom: 2px; - } - - > .key { - font-style: italic; - } - - > .url { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - // word-wrap: break-word; - margin-top: 2px; - } - - .sync-status { - font-weight: bold; - height: 30px; - } - - > .options { - margin-top: 10px; - } - } - } -} - -.account-keys-section { - .keys { - .key { - padding: 10px 10px; - padding-bottom: 8px; - margin-bottom: 8px; - - > .name { - font-size: 13px; - font-weight: bold; - margin-bottom: 2px; - } - - > .value { - word-wrap: break-word; - } } } } diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index 3c7153ae2..db519ee40 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -1,6 +1,15 @@ -.header { +.pull-left { + float: left; +} + +.pull-right { + float: right; +} + +.footer-bar { position: relative; width: 100%; + padding: 5px; background-color: #d8d7d9; height: $header-height; max-height: $header-height; @@ -10,140 +19,155 @@ border-bottom: 1px solid rgba(#979799, 0.4); a { - color: $dark-gray; - } - -} - -.header-content { - margin-bottom: 0px; - padding-top: 0px; - border-radius: 0px; - - left: 0px; - right: 0px; -} - -.panel-status-text { - margin-top: 20px; - font-style: italic; - font-size: 14px; -} - -.menu { - margin-left: 15px; - padding-top: 5px; - margin-top: 0px; - color: #515263; - z-index: 1000; - margin-bottom: 0px; - font-size: 11px; - - &.left { - float: left; - } - - &.right { - float: right; - margin-right: 10px; - } - - .login-panel .login-input { - border-radius: 0px; - } -} - -.items { - - .item { - - display: inline-block; - margin-right: 7px; - position: relative; - cursor: pointer; font-weight: bold; + cursor: pointer; + color: $blue-color; - a { - color: #515263; + &.gray { + color: $dark-gray !important; } - .panel { - position: absolute; - right: 0px; - bottom: 20px; - min-width: 300px; - z-index: 1000; - margin-top: 15px; - box-shadow: 0px 0px 15px rgba(black, 0.2); - border: none; - cursor: default; - max-height: 85vh; - overflow: auto; - background-color: white; - font-weight: normal; - - section { - margin-bottom: 10px; - } - - h3 { - font-weight: bold; - margin-top: 0px; - margin-bottom: 4px; - font-size: 16px; - width: 100%; - } - - a { - font-size: 12px; - color: $blue-color; - font-weight: bold; - } - - button.light { - font-weight: bold; - margin-bottom: 0px; - font-size: 12px; - height: 30px; - padding-top: 3px; - text-align: center; - margin-bottom: 6px; - background-color: white; - display: block; - width: 100%; - border: 1px solid rgba(gray, 0.15); - cursor: pointer; - color: $blue-color; - - &:hover { - background-color: rgba(gray, 0.10); - } - - .execution-spinner { - margin-left: auto; - margin-right: auto; - text-align: center; - margin-top: 3px; - } - } - - .storage-text { - font-size: 14px; - } - - .checkbox { - font-size: 14px; - font-weight: normal; - margin-left: auto; - margin-right: auto; - } + &.block { + display: block !important; } } + + p { + margin: 2px 0px; + font-size: 12px; + } + + label { + font-weight: bold; + margin-bottom: 4px; + } + + strong { + display: block; + } + + h3 { + font-size: 14px !important; + margin-top: 4px !important; + margin-bottom: 0px !important; + } + + h4 { + margin-bottom: 0px !important; + } + + section { + padding: 5px; + margin-top: 5px; + + &.inline-h { + padding: 5px 0px; + } + } + + input { + margin-bottom: 10px; + border-radius: 0px; + } + + .block { + display: block; + } + + .wrap { + word-wrap: break-word; + } + + .one-line-overflow { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + .small-v-space { + height: 6px; + display: block; + } + + .medium-padding { + padding: 10px; + } + + .large-padding { + padding: 15px; + } } -.item.io { - .enc-option { - display: block; +.footer-bar-link { + font-size: 11px; + font-weight: bold; + margin-left: 8px; + color: #515263; + + z-index: 1000; + display: inline-block; + position: relative; + cursor: pointer; + + > a { + color: #515263; + } +} + +.footer-bar-link .panel { + font-weight: normal; + font-size: 12px; + + max-height: 85vh; + position: absolute; + right: 0px; + bottom: 20px; + min-width: 300px; + z-index: 1000; + margin-top: 15px; + + box-shadow: 0px 0px 15px rgba(black, 0.2); + border: none; + cursor: default; + overflow: auto; + background-color: white; + + button.light { + font-weight: bold; + margin-bottom: 0px; + font-size: 12px; + height: 30px; + padding-top: 3px; + text-align: center; + margin-bottom: 6px; + background-color: white; + display: block; + width: 100%; + border: 1px solid rgba(gray, 0.15); + cursor: pointer; + color: $blue-color; + + &:hover { + background-color: rgba(gray, 0.10); + } + + .execution-spinner { + margin-left: auto; + margin-right: auto; + text-align: center; + margin-top: 3px; + } + } + + .storage-text { + font-size: 14px; + } + + .checkbox { + font-size: 14px; + font-weight: normal; + margin-left: auto; + margin-right: auto; } } @@ -170,120 +194,26 @@ cursor: default !important; } -.item.account { +.account-panel { + width: 350px; +} - .email { - font-size: 18px; - font-weight: bold; - margin-bottom: 2px; +.import-password { + margin-top: 14px; + + > .field { + display: block; + margin: 5px 0px; } +} - .server { - margin-bottom: 10px; - } - - .links { - margin-bottom: 25px; - } - - .link-item { - margin-bottom: 8px; - - } - - input { - border-radius: 0px; - } - - .account-panel { - - padding: 12px; - padding-bottom: 6px; - - .account-items { - margin-top: 0px; +.encryption-confirmation { + position: relative; + .buttons { + .cancel { + font-weight: normal; + margin-right: 3px; } - - .account-section-content { - margin-top: 15px; - } - - .account-item { - width: 100%; - margin-bottom: 15px; - min-height: 50px; - padding: 20px 14px; - - a { - color: $blue-color; - font-weight: bold; - cursor: pointer; - } - - .meta-container { - display: block; - font-size: 10px; - } - - .action-container { - font-size: 12px; - margin-top: 6px; - - .status-title { - font-weight: bold; - } - - .subtext { - font-size: 10px; - margin-top: 2px; - } - - a { - display: block; - margin-bottom: -10px; - } - } - - .import-password { - margin-top: 14px; - - > .field { - display: block; - margin: 5px 0px; - } - } - - .encryption-confirmation { - position: relative; - .buttons { - .cancel { - font-weight: normal; - margin-right: 3px; - } - } - } - - &:last-child { - margin-bottom: 8px !important; - } - - .meta-container { - > .title { - font-size: 13px; - font-weight: bold; - } - - > .desc { - font-size: 12px; - margin-top: 3px; - } - } - } - - .membership-settings { - font-size: 14px; - } - } } @@ -291,23 +221,6 @@ a.disabled { pointer-events: none; } -.account-form { - margin-top: 10px; -} - -.registration-login { - - .login-forgot { - margin-top: 20px; - clear: both; - a { - display: block; - font-size: 13px !important; - text-align: center; - } - } -} - .spinner { height: 10px; width: 10px; diff --git a/app/assets/templates/frontend/directives/account-data-menu.html.haml b/app/assets/templates/frontend/directives/account-data-menu.html.haml index b05d2cb07..a778bdb31 100644 --- a/app/assets/templates/frontend/directives/account-data-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-data-menu.html.haml @@ -1,20 +1,25 @@ .panel.panel-default.account-panel.panel-right.account-data-menu .panel-body - .account-items - %section.account-item.gray-bg{"ng-init" => "showSN = true"} - %h3{"ng-click" => "showSN = !showSN"} Standard Notes Account - %account-vendor-account-section{"ng-if" => "showSN"} - %section.account-item.gray-bg - %h3{"ng-click" => "showSync = !showSync"} Sync Locations ({{syncProviders.length}}) - %account-sync-section{"ng-if" => "showSync"} + %section.gray-bg.medium-padding{"ng-init" => "showSN = true"} + %h3{"ng-click" => "showSN = !showSN"} Standard Notes Account + .small-v-space + %account-vendor-account-section{"ng-if" => "showSN"} - %section.account-item.gray-bg - %h3{"ng-click" => "showKeys = !showKeys"} Encryption Keys ({{keys.length}}) - %account-keys-section{"ng-if" => "showKeys"} + %h4 + %a{"ng-click" => "showIO = !showIO"} Import/Export + %section.gray-bg{"ng-if" => "showIO"} + %import-export-menu{"ng-if" => "showIO"} - %section.account-item.gray-bg - %h3{"ng-click" => "showIO = !showIO"} Import/Export - %import-export-menu{"ng-if" => "showIO"} + %h4 + %a{"ng-click" => "showAdvanced = !showAdvanced"} Advanced + %div{"ng-if" => "showAdvanced"} + %section.gray-bg.medium-padding + %h3 Sync Locations ({{syncProviders.length}}) + %account-sync-section + %section.gray-bg.medium-padding + %h3 Encryption Keys ({{keys.length}}) + %account-keys-section + %h4 %a{"ng-click" => "destroyLocalData()"} Destroy all local data diff --git a/app/assets/templates/frontend/directives/account-keys-section.html.haml b/app/assets/templates/frontend/directives/account-keys-section.html.haml index a85a7a8c5..a82336b9c 100644 --- a/app/assets/templates/frontend/directives/account-keys-section.html.haml +++ b/app/assets/templates/frontend/directives/account-keys-section.html.haml @@ -1,11 +1,9 @@ -.account-keys-section.account-section-content - .keys - .key.white-bg{"ng-repeat" => "key in keys track by key.name"} - .name {{key.name}} - .value {{key.key}} +%section.white-bg{"ng-repeat" => "key in keys track by key.name"} + %label {{key.name}} + %p.wrap {{key.key}} - %a{"ng-click" => "newKeyData.showForm = !newKeyData.showForm"} Add New Key - %form{"ng-if" => "newKeyData.showForm"} - %input{"ng-model" => "newKeyData.name", "placeholder" => "Name your key"} - %input{"ng-model" => "newKeyData.key", "placeholder" => "Key"} - %button.light{"ng-click" => "submitNewKeyForm()"} Add Key +%a{"ng-click" => "newKeyData.showForm = !newKeyData.showForm"} Add New Key +%form{"ng-if" => "newKeyData.showForm"} + %input{"ng-model" => "newKeyData.name", "placeholder" => "Name your key"} + %input{"ng-model" => "newKeyData.key", "placeholder" => "Key"} + %button.light{"ng-click" => "submitNewKeyForm()"} Add Key diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml index 270cc0beb..1e7e5f7cb 100644 --- a/app/assets/templates/frontend/directives/account-sync-section.html.haml +++ b/app/assets/templates/frontend/directives/account-sync-section.html.haml @@ -1,29 +1,27 @@ -.providers.account-section-content - .provider.white-bg{"ng-repeat" => "provider in syncProviders"} - .type {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} - .key{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} - .url {{provider.url}} - .options - %div{"ng-if" => "!provider.keyName || provider.showKeyForm"} - %p - %strong Choose encryption key: - %select{"ng-model" => "provider.formData.keyName"} - %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"} - {{key.name}} - %button{"ng-click" => "saveKey(provider)"} Set +%section.white-bg{"ng-repeat" => "provider in syncProviders"} + %label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} + %em{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} + %p {{provider.url}} + %section.inline-h + %div{"ng-if" => "!provider.keyName || provider.showKeyForm"} + %p + %strong Choose encryption key: + %select{"ng-model" => "provider.formData.keyName"} + %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"} + {{key.name}} + %button{"ng-click" => "saveKey(provider)"} Set - %div{"ng-if" => "!provider.enabled"} - %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Set as Main - %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary - %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key - %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider - .sync-status{"delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} - .text{"style" => "float: left;"} Syncing: {{provider.syncStatus.statusString}} - .spinner{"style" => "float: right"} + %div{"ng-if" => "!provider.enabled"} + %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Set as Main + %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary + %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key + %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider + %div{"style" => "height: 30px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} + %strong{"style" => "float: left;"} Syncing: {{provider.syncStatus.statusString}} + .spinner{"style" => "float: right"} - %a{"ng-click" => "newSyncData.showAddSyncForm = !newSyncData.showAddSyncForm"} Add external sync with Secret URL - %form.sync-form{"ng-if" => "newSyncData.showAddSyncForm"} - .form-tag.has-feedback - %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'newSyncData.url'} - %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} - Add External Sync +%a{"ng-click" => "newSyncData.showAddSyncForm = !newSyncData.showAddSyncForm"} Add external sync with Secret URL +%form{"ng-if" => "newSyncData.showAddSyncForm"} + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'newSyncData.url'} + %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} + Add External Sync diff --git a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml index 9c4528909..f3dcf5993 100644 --- a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml +++ b/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml @@ -1,43 +1,34 @@ -.registration-login.account-section-content +%div{"ng-if" => "user"} + %h3 {{user.email}} + %p {{serverURL}} %div{"ng-if" => "user"} - .email {{user.email}} - .server {{serverURL}} - .links{"ng-if" => "user"} - .link-item - %a{"ng-click" => "signOutPressed()"} Sign out - %p Note: Signing out does not delete your local items, extensions, and keys. - .meta-container - .title Local Encryption - .desc Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes. - .action-container - %span.status-title Status: - {{encryptionStatusForNotes()}} - %div{"ng-if" => "!user"} - .meta-container - .title Sign in or Register (optional) - .desc Enter your Standard Notes account information. - .action-container - %form.account-form{'name' => "loginForm"} - .form-tag.has-feedback - %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'loginData.url'} - .form-tag.has-feedback - %input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'loginData.email'} - .form-tag.has-feedback - %input.form-control.login-input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.user_password'} - .checkbox{"ng-if" => "localNotesCount() > 0"} - %label - %input{"type" => "checkbox", "ng-model" => "loginData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} - Merge local notes ({{localNotesCount()}} notes) - %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span Sign In - %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span Register - %br - .login-forgot{"style" => "padding-top: 4px;"} - %a.btn.btn-link{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten. - .panel-status-text{"ng-if" => "loginData.status", "style" => "font-size: 14px;"} {{loginData.status}} + %a{"ng-click" => "signOutPressed()"} Sign out + %p Note: Signing out does not delete your local items, extensions, and keys. + %label Local Encryption + %p Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes. + %label Status: + {{encryptionStatusForNotes()}} +%div{"ng-if" => "!user"} + %label Sign in or Register (optional) + %p Enter your Standard Notes account information. + %form.account-form{'name' => "loginForm"} + %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'loginData.url'} + %input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'loginData.email'} + %input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.user_password'} + .checkbox{"ng-if" => "localNotesCount() > 0"} + %label + %input{"type" => "checkbox", "ng-model" => "loginData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} + Merge local notes ({{localNotesCount()}} notes) + %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span Sign In + %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span Register + %br + .block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"} + %a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten. + %em{"ng-if" => "loginData.status", "style" => "font-size: 14px;"} {{loginData.status}} - %div{"ng-if" => "showResetForm"} - %p{"style" => "font-size: 13px; text-align: center;"} - Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password. - For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password. + %div{"ng-if" => "showResetForm"} + %p{"style" => "font-size: 13px; text-align: center;"} + Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password. + For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password. diff --git a/app/assets/templates/frontend/directives/import-export-menu.html.haml b/app/assets/templates/frontend/directives/import-export-menu.html.haml index 87b52c429..f20e60863 100644 --- a/app/assets/templates/frontend/directives/import-export-menu.html.haml +++ b/app/assets/templates/frontend/directives/import-export-menu.html.haml @@ -1,27 +1,27 @@ -.account-section-content - .options{"style" => "font-size: 12px; margin-top: 4px;"} - %label.enc-option{"ng-if" => "user"} - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"} - Encrypted with Standard File key - %label.enc-option - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"} - {{user ? 'Encrypted with custom key' : 'Encrypted' }} - %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"} - %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"} - %label.enc-option - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"} - Decrypted - .action-container - %a{"ng-click" => "downloadDataArchive()"} Download Data Archive +.options{"style" => "font-size: 12px; margin-top: 4px;"} + %label{"ng-if" => "user"} + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"} + Encrypted with Standard File key + %label + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"} + {{user ? 'Encrypted with custom key' : 'Encrypted' }} + %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"} + %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"} + %label + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"} + Decrypted - %div{"ng-if" => "!importData.loading"} - %label#import-archive - %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} - %a.disabled - %span - Import Data from Archive - .import-password{"ng-if" => "importData.requestPassword"} - Enter the account password associated with the import file. - %input.field{"type" => "text", "ng-model" => "importData.password"} - %button{"ng-click" => "submitImportPassword()"} Decrypt & Import - .spinner{"ng-if" => "importData.loading"} +%a{"ng-click" => "downloadDataArchive()"} Download Data Archive + +%div{"ng-if" => "!importData.loading"} + %label#import-archive + %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} + %a.disabled + %span + Import Data from Archive + %div{"ng-if" => "importData.requestPassword"} + Enter the account password associated with the import file. + %input{"type" => "text", "ng-model" => "importData.password"} + %button{"ng-click" => "submitImportPassword()"} Decrypt & Import + +.spinner{"ng-if" => "importData.loading"} diff --git a/app/assets/templates/frontend/header.html.haml b/app/assets/templates/frontend/header.html.haml index 0ab81a3b2..a9d7371cb 100644 --- a/app/assets/templates/frontend/header.html.haml +++ b/app/assets/templates/frontend/header.html.haml @@ -1,26 +1,24 @@ -.header - .header-content - .menu.left +.footer-bar + .pull-left + .footer-bar-link + %a{"ng-click" => "ctrl.accountMenuPressed()"} Account + %account-data-menu{"ng-if" => "ctrl.showAccountMenu"} - .items - .item.account - %a{"ng-click" => "ctrl.accountMenuPressed()"} Account - %account-data-menu{"ng-if" => "ctrl.showAccountMenu"} + .footer-bar-link + %a{"ng-click" => "ctrl.toggleExtensions()"} Extensions + %global-extensions-menu{"ng-if" => "ctrl.showExtensionsMenu"} - .item - %a{"ng-click" => "ctrl.toggleExtensions()"} Extensions - %global-extensions-menu{"ng-if" => "ctrl.showExtensionsMenu"} + .footer-bar-link + %a{"href" => "https://standardnotes.org", "target" => "_blank"} + Help - .item - %a{"href" => "https://standardnotes.org", "target" => "_blank"} - Help + .pull-right - .menu.right - .items - .item.last-refreshed{"ng-if" => "ctrl.lastSyncDate"} - %span{"ng-if" => "!ctrl.isRefreshing"} - Last refreshed {{ctrl.lastSyncDate | appDateTime}} - %span{"ng-if" => "ctrl.isRefreshing"} - .spinner - .item{"ng-click" => "ctrl.refreshData()"} - Refresh + .footer-bar-link + %div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"} + %span{"ng-if" => "!ctrl.isRefreshing"} + Last refreshed {{ctrl.lastSyncDate | appDateTime}} + %span{"ng-if" => "ctrl.isRefreshing"} + .spinner{"style" => "margin-top: 2px;"} + + %a{"ng-click" => "ctrl.refreshData()"} Refresh From a255b8487e109a7631b41076a489c10594bdec6d Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 26 Jan 2017 15:11:41 -0600 Subject: [PATCH 09/25] account menu sections --- .../services/directives/accountDataMenu.js | 3 +- ...tExportMenu.js => accountExportSection.js} | 6 ++-- .../services/directives/accountKeysSection.js | 4 +-- ...Section.js => accountNewAccountSection.js} | 35 ++++++++++-------- .../services/directives/accountSyncSection.js | 9 ++--- app/assets/stylesheets/app/_header.scss | 27 +++++++++++++- .../directives/account-data-menu.html.haml | 24 +++++-------- .../directives/account-keys-section.html.haml | 9 ----- .../account-export-section.html.haml | 31 ++++++++++++++++ .../account-keys-section.html.haml | 18 ++++++++++ .../account-new-account-section.html.haml} | 36 +++++++++---------- .../account-sync-section.html.haml | 26 ++++++++++++++ .../directives/account-sync-section.html.haml | 27 -------------- .../directives/import-export-menu.html.haml | 27 -------------- 14 files changed, 156 insertions(+), 126 deletions(-) rename app/assets/javascripts/app/services/directives/{importExportMenu.js => accountExportSection.js} (90%) rename app/assets/javascripts/app/services/directives/{accountVendorAccountSection.js => accountNewAccountSection.js} (69%) delete mode 100644 app/assets/templates/frontend/directives/account-keys-section.html.haml create mode 100644 app/assets/templates/frontend/directives/account-menu/account-export-section.html.haml create mode 100644 app/assets/templates/frontend/directives/account-menu/account-keys-section.html.haml rename app/assets/templates/frontend/directives/{account-vendor-account-section.html.haml => account-menu/account-new-account-section.html.haml} (57%) create mode 100644 app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml delete mode 100644 app/assets/templates/frontend/directives/account-sync-section.html.haml delete mode 100644 app/assets/templates/frontend/directives/import-export-menu.html.haml diff --git a/app/assets/javascripts/app/services/directives/accountDataMenu.js b/app/assets/javascripts/app/services/directives/accountDataMenu.js index 3a34027de..2d5c5fedf 100644 --- a/app/assets/javascripts/app/services/directives/accountDataMenu.js +++ b/app/assets/javascripts/app/services/directives/accountDataMenu.js @@ -7,11 +7,10 @@ class AccountDataMenu { }; } - controller($scope, apiController, modelManager, keyManager, syncManager) { + controller($scope, apiController, modelManager, keyManager) { 'ngInject'; $scope.keys = keyManager.keys; - $scope.syncProviders = syncManager.syncProviders; $scope.destroyLocalData = function() { if(!confirm("Are you sure you want to end your session? This will delete all local items, sync providers, keys, and extensions.")) { diff --git a/app/assets/javascripts/app/services/directives/importExportMenu.js b/app/assets/javascripts/app/services/directives/accountExportSection.js similarity index 90% rename from app/assets/javascripts/app/services/directives/importExportMenu.js rename to app/assets/javascripts/app/services/directives/accountExportSection.js index fe96806e1..335ae347b 100644 --- a/app/assets/javascripts/app/services/directives/importExportMenu.js +++ b/app/assets/javascripts/app/services/directives/accountExportSection.js @@ -1,8 +1,8 @@ -class ImportExportMenu { +class AccountExportSection { constructor() { this.restrict = "E"; - this.templateUrl = "frontend/directives/import-export-menu.html"; + this.templateUrl = "frontend/directives/account-menu/account-export-section.html"; this.scope = { }; } @@ -76,4 +76,4 @@ class ImportExportMenu { } -angular.module('app.frontend').directive('importExportMenu', () => new ImportExportMenu); +angular.module('app.frontend').directive('accountExportSection', () => new AccountExportSection); diff --git a/app/assets/javascripts/app/services/directives/accountKeysSection.js b/app/assets/javascripts/app/services/directives/accountKeysSection.js index 0f2566729..db9526488 100644 --- a/app/assets/javascripts/app/services/directives/accountKeysSection.js +++ b/app/assets/javascripts/app/services/directives/accountKeysSection.js @@ -2,7 +2,7 @@ class AccountKeysSection { constructor() { this.restrict = "E"; - this.templateUrl = "frontend/directives/account-keys-section.html"; + this.templateUrl = "frontend/directives/account-menu/account-keys-section.html"; this.scope = { }; } @@ -19,7 +19,7 @@ class AccountKeysSection { alert("This key name is already in use. Please use a different name."); return; } - + $scope.newKeyData.showForm = false; } } diff --git a/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js similarity index 69% rename from app/assets/javascripts/app/services/directives/accountVendorAccountSection.js rename to app/assets/javascripts/app/services/directives/accountNewAccountSection.js index 789078619..fceb4e092 100644 --- a/app/assets/javascripts/app/services/directives/accountVendorAccountSection.js +++ b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js @@ -1,8 +1,8 @@ -class AccountVendorAccountSection { +class AccountNewAccountSection { constructor() { this.restrict = "E"; - this.templateUrl = "frontend/directives/account-vendor-account-section.html"; + this.templateUrl = "frontend/directives/account-menu/account-new-account-section.html"; this.scope = { }; } @@ -10,13 +10,20 @@ class AccountVendorAccountSection { controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) { 'ngInject'; - $scope.loginData = {mergeLocal: true, url: syncManager.serverURL()}; + $scope.formData = {mergeLocal: true, url: syncManager.serverURL()}; $scope.user = apiController.user; + $scope.showForm = syncManager.syncProviders.length == 0; + $scope.changePasswordPressed = function() { $scope.showNewPasswordForm = !$scope.showNewPasswordForm; } + $scope.submitExternalSyncURL = function() { + syncManager.addSyncProviderFromURL($scope.newSyncData.url); + $scope.newSyncData.showAddSyncForm = false; + } + $scope.signOutPressed = function() { $scope.showAccountMenu = false; apiController.signoutOfStandardFile(false, function(){ @@ -45,21 +52,21 @@ class AccountVendorAccountSection { } $scope.mergeLocalChanged = function() { - if(!$scope.loginData.mergeLocal) { + if(!$scope.formData.mergeLocal) { if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) { - $scope.loginData.mergeLocal = true; + $scope.formData.mergeLocal = true; } } } $scope.loginSubmitPressed = function() { - $scope.loginData.status = "Generating Login Keys..."; - console.log("logging in with url", $scope.loginData.url); + $scope.formData.status = "Generating Login Keys..."; + console.log("logging in with url", $scope.formData.url); $timeout(function(){ - apiController.login($scope.loginData.url, $scope.loginData.email, $scope.loginData.user_password, function(response){ + apiController.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ if(!response || response.error) { var error = response ? response.error : {message: "An unknown error occured."} - $scope.loginData.status = null; + $scope.formData.status = null; if(!response || (response && !response.didDisplayAlert)) { alert(error.message); } @@ -71,13 +78,13 @@ class AccountVendorAccountSection { } $scope.submitRegistrationForm = function() { - $scope.loginData.status = "Generating Account Keys..."; + $scope.formData.status = "Generating Account Keys..."; $timeout(function(){ - apiController.register($scope.loginData.url, $scope.loginData.email, $scope.loginData.user_password, function(response){ + apiController.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ if(!response || response.error) { var error = response ? response.error : {message: "An unknown error occured."} - $scope.loginData.status = null; + $scope.formData.status = null; alert(error.message); } else { $scope.onAuthSuccess(response.user); @@ -98,7 +105,7 @@ class AccountVendorAccountSection { $scope.showRegistration = false; }; - if(!$scope.loginData.mergeLocal) { + if(!$scope.formData.mergeLocal) { dbManager.clearAllItems(function(){ block(); }); @@ -110,4 +117,4 @@ class AccountVendorAccountSection { } } -angular.module('app.frontend').directive('accountVendorAccountSection', () => new AccountVendorAccountSection); +angular.module('app.frontend').directive('accountNewAccountSection', () => new AccountNewAccountSection); diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js index bccec8351..c9009d772 100644 --- a/app/assets/javascripts/app/services/directives/accountSyncSection.js +++ b/app/assets/javascripts/app/services/directives/accountSyncSection.js @@ -2,7 +2,7 @@ class AccountSyncSection { constructor() { this.restrict = "E"; - this.templateUrl = "frontend/directives/account-sync-section.html"; + this.templateUrl = "frontend/directives/account-menu/account-sync-section.html"; this.scope = { }; } @@ -11,13 +11,8 @@ class AccountSyncSection { 'ngInject'; $scope.syncProviders = syncManager.syncProviders; - $scope.newSyncData = {showAddSyncForm: false} $scope.keys = keyManager.keys; - - $scope.submitExternalSyncURL = function() { - syncManager.addSyncProviderFromURL($scope.newSyncData.url); - $scope.newSyncData.showAddSyncForm = false; - } + $scope.showSection = $scope.syncProviders.length > 0; $scope.enableSyncProvider = function(provider, primary) { if(!provider.keyName) { diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index db519ee40..a69286884 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -6,6 +6,18 @@ float: right; } +.mt-5 { + margin-top: 5px; +} + +.mt-10 { + margin-top: 10px; +} + +.faded { + opacity: 0.5; +} + .footer-bar { position: relative; width: 100%; @@ -18,6 +30,10 @@ color: $dark-gray; border-bottom: 1px solid rgba(#979799, 0.4); + .medium-text { + font-size: 14px; + } + a { font-weight: bold; cursor: pointer; @@ -49,7 +65,7 @@ h3 { font-size: 14px !important; margin-top: 4px !important; - margin-bottom: 0px !important; + margin-bottom: 3px !important; } h4 { @@ -70,6 +86,15 @@ border-radius: 0px; } + .center-align { + text-align: center; + } + + .center { + margin-left: auto; + margin-right: auto; + } + .block { display: block; } diff --git a/app/assets/templates/frontend/directives/account-data-menu.html.haml b/app/assets/templates/frontend/directives/account-data-menu.html.haml index a778bdb31..2b5128125 100644 --- a/app/assets/templates/frontend/directives/account-data-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-data-menu.html.haml @@ -2,24 +2,16 @@ .panel-body %section.gray-bg.medium-padding{"ng-init" => "showSN = true"} - %h3{"ng-click" => "showSN = !showSN"} Standard Notes Account - .small-v-space - %account-vendor-account-section{"ng-if" => "showSN"} + %account-new-account-section - %h4 - %a{"ng-click" => "showIO = !showIO"} Import/Export - %section.gray-bg{"ng-if" => "showIO"} - %import-export-menu{"ng-if" => "showIO"} + %section.gray-bg.medium-padding + %account-sync-section - %h4 - %a{"ng-click" => "showAdvanced = !showAdvanced"} Advanced - %div{"ng-if" => "showAdvanced"} - %section.gray-bg.medium-padding - %h3 Sync Locations ({{syncProviders.length}}) - %account-sync-section + %section.gray-bg.medium-padding + %account-export-section - %section.gray-bg.medium-padding - %h3 Encryption Keys ({{keys.length}}) - %account-keys-section + %section.gray-bg.medium-padding + %account-keys-section + %h4 %a{"ng-click" => "destroyLocalData()"} Destroy all local data diff --git a/app/assets/templates/frontend/directives/account-keys-section.html.haml b/app/assets/templates/frontend/directives/account-keys-section.html.haml deleted file mode 100644 index a82336b9c..000000000 --- a/app/assets/templates/frontend/directives/account-keys-section.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -%section.white-bg{"ng-repeat" => "key in keys track by key.name"} - %label {{key.name}} - %p.wrap {{key.key}} - -%a{"ng-click" => "newKeyData.showForm = !newKeyData.showForm"} Add New Key -%form{"ng-if" => "newKeyData.showForm"} - %input{"ng-model" => "newKeyData.name", "placeholder" => "Name your key"} - %input{"ng-model" => "newKeyData.key", "placeholder" => "Key"} - %button.light{"ng-click" => "submitNewKeyForm()"} Add Key diff --git a/app/assets/templates/frontend/directives/account-menu/account-export-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-export-section.html.haml new file mode 100644 index 000000000..b22b6d178 --- /dev/null +++ b/app/assets/templates/frontend/directives/account-menu/account-export-section.html.haml @@ -0,0 +1,31 @@ +%h3{"ng-click" => "showSection = !showSection"} + %a Import or export data + +%div{"ng-if" => "showSection"} + .options{"style" => "font-size: 12px; margin-top: 4px;"} + %label{"ng-if" => "user"} + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"} + Encrypted with Standard File key + %label + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"} + {{user ? 'Encrypted with custom key' : 'Encrypted' }} + %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"} + %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"} + %label + %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"} + Decrypted + + %a{"ng-click" => "downloadDataArchive()"} Download Data Archive + + %div{"ng-if" => "!importData.loading"} + %label#import-archive + %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} + %a.disabled + %span + Import Data from Archive + %div{"ng-if" => "importData.requestPassword"} + Enter the account password associated with the import file. + %input{"type" => "text", "ng-model" => "importData.password"} + %button{"ng-click" => "submitImportPassword()"} Decrypt & Import + + .spinner{"ng-if" => "importData.loading"} diff --git a/app/assets/templates/frontend/directives/account-menu/account-keys-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-keys-section.html.haml new file mode 100644 index 000000000..7295cb611 --- /dev/null +++ b/app/assets/templates/frontend/directives/account-menu/account-keys-section.html.haml @@ -0,0 +1,18 @@ +%h3{"ng-click" => "showSection = !showSection"} + %a Manage keys + +%div{"ng-if" => "showSection"} + %h4 Encryption Keys + + %div{"ng-if" => "showSection"} + %p Keys are used to encrypt and decrypt your data. + .mt-10 + %section.white-bg{"ng-repeat" => "key in keys track by key.name"} + %label {{key.name}} + %p.wrap {{key.key}} + + %a.block.mt-10{"ng-click" => "newKeyData.showForm = !newKeyData.showForm"} Add New Key + %form{"ng-if" => "newKeyData.showForm"} + %input{"ng-model" => "newKeyData.name", "placeholder" => "Name your key"} + %input{"ng-model" => "newKeyData.key", "placeholder" => "Key"} + %button.light{"ng-click" => "submitNewKeyForm()"} Add Key diff --git a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml similarity index 57% rename from app/assets/templates/frontend/directives/account-vendor-account-section.html.haml rename to app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml index f3dcf5993..758c4b570 100644 --- a/app/assets/templates/frontend/directives/account-vendor-account-section.html.haml +++ b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml @@ -1,23 +1,15 @@ -%div{"ng-if" => "user"} - %h3 {{user.email}} - %p {{serverURL}} - %div{"ng-if" => "user"} - %a{"ng-click" => "signOutPressed()"} Sign out - %p Note: Signing out does not delete your local items, extensions, and keys. - %label Local Encryption - %p Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes. - %label Status: - {{encryptionStatusForNotes()}} -%div{"ng-if" => "!user"} - %label Sign in or Register (optional) - %p Enter your Standard Notes account information. +%h3{"ng-click" => "showForm = !showForm"} + %a Add a sync account +%div{"ng-if" => "showForm"} + %p Enter your Standard File account information. + .small-v-space %form.account-form{'name' => "loginForm"} - %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'loginData.url'} - %input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'loginData.email'} - %input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.user_password'} + %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'} + %input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'formData.email'} + %input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'} .checkbox{"ng-if" => "localNotesCount() > 0"} %label - %input{"type" => "checkbox", "ng-model" => "loginData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} + %input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} Merge local notes ({{localNotesCount()}} notes) %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} %span Sign In @@ -26,7 +18,15 @@ %br .block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"} %a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten. - %em{"ng-if" => "loginData.status", "style" => "font-size: 14px;"} {{loginData.status}} + %em{"ng-if" => "formData.status", "style" => "font-size: 14px;"} {{formData.status}} + + %label.center-align.block.faded — OR — + %a.block.center-align.medium-text{"ng-if" => "!formData.showAddLinkForm", "ng-click" => "formData.showAddLinkForm = true"} Add sync using secret link + %form{"ng-if" => "formData.showAddLinkForm"} + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.url'} + %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} + Add Sync Account + %a.block.center-align.mt-5{"ng-click" => "formData.showAddLinkForm = false"} Cancel %div{"ng-if" => "showResetForm"} %p{"style" => "font-size: 13px; text-align: center;"} diff --git a/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml new file mode 100644 index 000000000..6d430bd91 --- /dev/null +++ b/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml @@ -0,0 +1,26 @@ +%h3{"ng-click" => "showSection = !showSection"} + %a Your sync accounts ({{syncProviders.length}}) + +%div{"ng-if" => "showSection"} + .small-v-space + %section.white-bg{"ng-repeat" => "provider in syncProviders"} + %label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} + %em{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} + %p {{provider.url}} + %section.inline-h + %div{"ng-if" => "!provider.keyName || provider.showKeyForm"} + %p + %strong Choose encryption key: + %select{"ng-model" => "provider.formData.keyName"} + %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"} + {{key.name}} + %button{"ng-click" => "saveKey(provider)"} Set + + %div{"ng-if" => "!provider.enabled"} + %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Set as Main + %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary + %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key + %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider + %div{"style" => "height: 30px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} + %strong{"style" => "float: left;"} Syncing: {{provider.syncStatus.statusString}} + .spinner{"style" => "float: right"} diff --git a/app/assets/templates/frontend/directives/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-sync-section.html.haml deleted file mode 100644 index 1e7e5f7cb..000000000 --- a/app/assets/templates/frontend/directives/account-sync-section.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -%section.white-bg{"ng-repeat" => "provider in syncProviders"} - %label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} - %em{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} - %p {{provider.url}} - %section.inline-h - %div{"ng-if" => "!provider.keyName || provider.showKeyForm"} - %p - %strong Choose encryption key: - %select{"ng-model" => "provider.formData.keyName"} - %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"} - {{key.name}} - %button{"ng-click" => "saveKey(provider)"} Set - - %div{"ng-if" => "!provider.enabled"} - %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Set as Main - %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary - %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key - %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider - %div{"style" => "height: 30px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} - %strong{"style" => "float: left;"} Syncing: {{provider.syncStatus.statusString}} - .spinner{"style" => "float: right"} - -%a{"ng-click" => "newSyncData.showAddSyncForm = !newSyncData.showAddSyncForm"} Add external sync with Secret URL -%form{"ng-if" => "newSyncData.showAddSyncForm"} - %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'newSyncData.url'} - %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} - Add External Sync diff --git a/app/assets/templates/frontend/directives/import-export-menu.html.haml b/app/assets/templates/frontend/directives/import-export-menu.html.haml deleted file mode 100644 index f20e60863..000000000 --- a/app/assets/templates/frontend/directives/import-export-menu.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -.options{"style" => "font-size: 12px; margin-top: 4px;"} - %label{"ng-if" => "user"} - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"} - Encrypted with Standard File key - %label - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"} - {{user ? 'Encrypted with custom key' : 'Encrypted' }} - %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"} - %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"} - %label - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"} - Decrypted - -%a{"ng-click" => "downloadDataArchive()"} Download Data Archive - -%div{"ng-if" => "!importData.loading"} - %label#import-archive - %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} - %a.disabled - %span - Import Data from Archive - %div{"ng-if" => "importData.requestPassword"} - Enter the account password associated with the import file. - %input{"type" => "text", "ng-model" => "importData.password"} - %button{"ng-click" => "submitImportPassword()"} Decrypt & Import - -.spinner{"ng-if" => "importData.loading"} From b7492176d18735e83107d3596d90aad0c87da29c Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 26 Jan 2017 19:50:47 -0600 Subject: [PATCH 10/25] sync accounts --- app/assets/javascripts/app/app.frontend.js | 15 +--- .../app/frontend/controllers/editor.js | 26 +++--- .../app/frontend/controllers/header.js | 13 ++- .../javascripts/app/services/apiController.js | 40 ++++----- .../services/directives/accountDataMenu.js | 5 +- .../directives/accountNewAccountSection.js | 15 ++-- .../services/directives/accountSyncSection.js | 10 +-- .../app/services/directives/delay-hide.js | 2 - .../app/services/sync/syncManager.js | 81 ++++++++++++------- .../app/services/sync/syncProvider.js | 22 ++++- .../app/services/sync/syncRunner.js | 9 ++- app/assets/stylesheets/app/_header.scss | 14 +++- .../account-new-account-section.html.haml | 2 +- .../account-sync-section.html.haml | 20 +++-- .../templates/frontend/header.html.haml | 7 +- 15 files changed, 167 insertions(+), 114 deletions(-) diff --git a/app/assets/javascripts/app/app.frontend.js b/app/assets/javascripts/app/app.frontend.js index 9563cc6fe..60ec42208 100644 --- a/app/assets/javascripts/app/app.frontend.js +++ b/app/assets/javascripts/app/app.frontend.js @@ -1,6 +1,7 @@ 'use strict'; var Neeto = Neeto || {}; +var SN = SN || {}; // detect IE8 and above, and edge. // IE and Edge do not support pbkdf2 in WebCrypto, therefore we need to use CryptoJS @@ -19,18 +20,4 @@ angular.module('app.frontend', [ .config(function (RestangularProvider, apiControllerProvider) { RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"}); - - RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) { - var token = localStorage.getItem("jwt"); - if(token) { - headers = _.extend(headers, {Authorization: "Bearer " + localStorage.getItem("jwt")}); - } - - return { - element: element, - params: params, - headers: headers, - httpConfig: httpConfig - }; - }); }) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index a4eeafd21..d0e1ae539 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -19,9 +19,9 @@ angular.module('app.frontend') /** * Insert 4 spaces when a tab key is pressed, * only used when inside of the text editor. - * If the shift key is pressed first, this event is - * not fired. - */ + * If the shift key is pressed first, this event is + * not fired. + */ var handleTab = function (event) { if (!event.shiftKey && event.which == 9) { event.preventDefault(); @@ -29,13 +29,13 @@ angular.module('app.frontend') var end = this.selectionEnd; var spaces = " "; - // Insert 4 spaces + // Insert 4 spaces this.value = this.value.substring(0, start) + spaces + this.value.substring(end); - // Place cursor 4 spaces away from where - // the tab key was pressed - this.selectionStart = this.selectionEnd = start + 4; + // Place cursor 4 spaces away from where + // the tab key was pressed + this.selectionStart = this.selectionEnd = start + 4; } } @@ -88,7 +88,7 @@ angular.module('app.frontend') } } }) - .controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager) { + .controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager, syncManager) { this.setNote = function(note, oldNote) { this.editorMode = 'edit'; @@ -148,12 +148,18 @@ angular.module('app.frontend') if(success) { if(statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function(){ - this.noteStatus = "All changes saved" + this.saveError = false; + var status = "All changes saved" + if(syncManager.offline) { + status += " (offline)"; + } + this.noteStatus = status; }.bind(this), 200) } else { if(statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function(){ - this.noteStatus = "(Offline) — All changes saved" + this.saveError = true; + this.noteStatus = "Error saving" }.bind(this), 200) } }.bind(this)); diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js index 99b901b80..822e788da 100644 --- a/app/assets/javascripts/app/frontend/controllers/header.js +++ b/app/assets/javascripts/app/frontend/controllers/header.js @@ -12,6 +12,10 @@ angular.module('app.frontend') link:function(scope, elem, attrs, ctrl) { scope.$on("sync:updated_token", function(){ ctrl.syncUpdated(); + ctrl.findErrors(); + }) + scope.$on("sync:error", function(){ + ctrl.findErrors(); }) } } @@ -19,9 +23,16 @@ angular.module('app.frontend') .controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager, syncManager) { this.user = apiController.user; + this.offline = syncManager.offline; + + this.findErrors = function() { + this.error = syncManager.syncProviders.filter(function(provider){return provider.error}).length > 0 ? true : false; + } + this.findErrors(); + this.accountMenuPressed = function() { - this.serverData = {url: syncManager.serverURL()}; + this.serverData = {}; this.showAccountMenu = !this.showAccountMenu; this.showFaq = false; this.showNewPasswordForm = false; diff --git a/app/assets/javascripts/app/services/apiController.js b/app/assets/javascripts/app/services/apiController.js index bfd1b4007..fc847f9fc 100644 --- a/app/assets/javascripts/app/services/apiController.js +++ b/app/assets/javascripts/app/services/apiController.js @@ -7,11 +7,11 @@ angular.module('app.frontend') return domain; } - this.$get = function($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) { - return new ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager); + this.$get = function($rootScope, Restangular, modelManager, dbManager, syncManager) { + return new ApiController($rootScope, Restangular, modelManager, dbManager, syncManager); } - function ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) { + function ApiController($rootScope, Restangular, modelManager, dbManager, syncManager) { var userData = localStorage.getItem("user"); if(userData) { @@ -79,7 +79,7 @@ angular.module('app.frontend') var params = {password: keys.pw, email: email}; _.merge(request, params); request.post().then(function(response){ - this.handleAuthResponse(response, url, authParams, mk); + this.handleAuthResponse(response, email, url, authParams, mk); callback(response); }.bind(this)) .catch(function(response){ @@ -90,13 +90,16 @@ angular.module('app.frontend') }.bind(this)) } - this.handleAuthResponse = function(response, url, authParams, mk) { - localStorage.setItem("server", url); - localStorage.setItem("jwt", response.token); - localStorage.setItem("user", JSON.stringify(response.user)); - localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); - keyManager.addKey(SNKeyName, mk); - syncManager.addStandardFileSyncProvider(url); + this.handleAuthResponse = function(response, email, url, authParams, mk) { + var params = { + url: url, + email: email, + uuid: response.user.uuid, + ek: mk, + jwt: response.token, + auth_params: _.omit(authParams, ["pw_nonce"]) + } + syncManager.addAccountBasedSyncProvider(params); } this.register = function(url, email, password, callback) { @@ -107,7 +110,7 @@ angular.module('app.frontend') var params = _.merge({password: keys.pw, email: email}, authParams); _.merge(request, params); request.post().then(function(response){ - this.handleAuthResponse(response, url, authParams, mk); + this.handleAuthResponse(response, email, url, authParams, mk); callback(response); }.bind(this)) .catch(function(response){ @@ -256,19 +259,6 @@ angular.module('app.frontend') return JSON.parse(JSON.stringify(object)); } - this.signoutOfStandardFile = function(destroyAll, callback) { - syncManager.removeStandardFileSyncProvider(); - if(destroyAll) { - this.destroyLocalData(callback); - } else { - localStorage.removeItem("user"); - localStorage.removeItem("jwt"); - localStorage.removeItem("server"); - localStorage.removeItem("auth_params"); - callback(); - } - } - this.destroyLocalData = function(callback) { dbManager.clearAllItems(function(){ localStorage.clear(); diff --git a/app/assets/javascripts/app/services/directives/accountDataMenu.js b/app/assets/javascripts/app/services/directives/accountDataMenu.js index 2d5c5fedf..244101b28 100644 --- a/app/assets/javascripts/app/services/directives/accountDataMenu.js +++ b/app/assets/javascripts/app/services/directives/accountDataMenu.js @@ -3,8 +3,7 @@ class AccountDataMenu { constructor() { this.restrict = "E"; this.templateUrl = "frontend/directives/account-data-menu.html"; - this.scope = { - }; + this.scope = {}; } controller($scope, apiController, modelManager, keyManager) { @@ -13,7 +12,7 @@ class AccountDataMenu { $scope.keys = keyManager.keys; $scope.destroyLocalData = function() { - if(!confirm("Are you sure you want to end your session? This will delete all local items, sync providers, keys, and extensions.")) { + if(!confirm("Are you sure you want to end your session? This will delete all local items, sync accounts, keys, and extensions.")) { return; } diff --git a/app/assets/javascripts/app/services/directives/accountNewAccountSection.js b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js index fceb4e092..b11bb571e 100644 --- a/app/assets/javascripts/app/services/directives/accountNewAccountSection.js +++ b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js @@ -10,7 +10,7 @@ class AccountNewAccountSection { controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) { 'ngInject'; - $scope.formData = {mergeLocal: true, url: syncManager.serverURL()}; + $scope.formData = {mergeLocal: true, url: syncManager.defaultServerURL()}; $scope.user = apiController.user; $scope.showForm = syncManager.syncProviders.length == 0; @@ -20,15 +20,10 @@ class AccountNewAccountSection { } $scope.submitExternalSyncURL = function() { - syncManager.addSyncProviderFromURL($scope.newSyncData.url); - $scope.newSyncData.showAddSyncForm = false; - } - - $scope.signOutPressed = function() { - $scope.showAccountMenu = false; - apiController.signoutOfStandardFile(false, function(){ - window.location.reload(); - }) + syncManager.addSyncProviderFromURL($scope.formData.secretUrl); + $scope.formData.showAddLinkForm = false; + $scope.formData.secretUrl = null; + $scope.showForm = false; } $scope.submitPasswordChange = function() { diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js index c9009d772..630c416a2 100644 --- a/app/assets/javascripts/app/services/directives/accountSyncSection.js +++ b/app/assets/javascripts/app/services/directives/accountSyncSection.js @@ -16,7 +16,7 @@ class AccountSyncSection { $scope.enableSyncProvider = function(provider, primary) { if(!provider.keyName) { - alert("You must choose an encryption key for this provider before enabling it."); + alert("You must choose an encryption key for this account before enabling it."); return; } @@ -24,19 +24,19 @@ class AccountSyncSection { } $scope.removeSyncProvider = function(provider) { - if(provider.isStandardNotesAccount) { - alert("To remove your Standard Notes sync, sign out of your Standard Notes account.") + if(provider.primary) { + alert("You cannot remove your main sync account. Instead, end your session by destroying all local data. Or, choose another account to be your primary sync account.") return; } - if(confirm("Are you sure you want to remove this sync provider?")) { + if(confirm("Are you sure you want to remove this sync account?")) { syncManager.removeSyncProvider(provider); } } $scope.changeEncryptionKey = function(provider) { if(provider.isStandardNotesAccount) { - alert("To change your encryption key for your Standard Notes account, you need to change your password. However, this functionality is not currently supported."); + alert("To change your encryption key for your Standard File account, you need to change your password. However, this functionality is not currently available."); return; } diff --git a/app/assets/javascripts/app/services/directives/delay-hide.js b/app/assets/javascripts/app/services/directives/delay-hide.js index 732e9b4f0..7467208f4 100644 --- a/app/assets/javascripts/app/services/directives/delay-hide.js +++ b/app/assets/javascripts/app/services/directives/delay-hide.js @@ -16,7 +16,6 @@ angular // Whenever the scope variable updates we simply // show if it evaluates to 'true' and hide if 'false' scope.$watch('show', function(newVal){ - console.log("show value changed", newVal); newVal ? showSpinner() : hideSpinner(); }); @@ -29,7 +28,6 @@ angular } function showElement(show) { - console.log("show:", show); show ? elem.css({display:''}) : elem.css({display:'none'}); } diff --git a/app/assets/javascripts/app/services/sync/syncManager.js b/app/assets/javascripts/app/services/sync/syncManager.js index 9b189fc9e..54cb16501 100644 --- a/app/assets/javascripts/app/services/sync/syncManager.js +++ b/app/assets/javascripts/app/services/sync/syncManager.js @@ -2,8 +2,9 @@ export const SNKeyName = "Standard Notes Key"; class SyncManager { - constructor(modelManager, syncRunner) { + constructor(modelManager, syncRunner, keyManager) { this.modelManager = modelManager; + this.keyManager = keyManager; this.syncRunner = syncRunner; this.syncRunner.setOnChangeProviderCallback(function(){ this.didMakeChangesToSyncProviders(); @@ -15,14 +16,19 @@ class SyncManager { return this.enabledProviders.length == 0; } - serverURL() { - return localStorage.getItem("server") || "https://n3.standardnotes.org"; + defaultServerURL() { + return "https://n3.standardnotes.org"; } get enabledProviders() { return this.syncProviders.filter(function(provider){return provider.enabled == true}); } + /* Used when adding a new account with */ + markAllOfflineItemsDirtyAndSave() { + + } + sync(callback) { this.syncRunner.sync(this.enabledProviders, callback); } @@ -57,21 +63,6 @@ class SyncManager { return _.find(this.syncProviders, {primary: true}); } - removeStandardFileSyncProvider() { - var sfProvider = _.find(this.syncProviders, {url: this.serverURL() + "/items/sync"}) - _.pull(this.syncProviders, sfProvider); - this.didMakeChangesToSyncProviders(); - } - - addStandardFileSyncProvider(url) { - var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: !this.primarySyncProvider()}); - defaultProvider.keyName = SNKeyName; - defaultProvider.enabled = this.syncProviders.length == 0; - this.syncProviders.push(defaultProvider); - this.didMakeChangesToSyncProviders(); - return defaultProvider; - } - didMakeChangesToSyncProviders() { localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) { return provider.asJSON() @@ -89,22 +80,58 @@ class SyncManager { } else { // no providers saved, this means migrating from old system to new // check if user is signed in - if(this.offline && localStorage.getItem("user")) { - var defaultProvider = this.addStandardFileSyncProvider(this.serverURL()); - defaultProvider.syncToken = localStorage.getItem("syncToken"); - // migrate old key structure to new - var mk = localStorage.getItem("mk"); - if(mk) { - keyManager.addKey(SNKeyName, mk); - localStorage.removeItem("mk"); + var userJSON = localStorage.getItem("user"); + if(this.offline && userJSON) { + var user = JSON.parse(userJSON); + var params = { + url: localStorage.getItem("server"), + email: user.email, + uuid: user.uuid, + ek: localStorage.getItem("mk"), + jwt: response.token, + auth_params: JSON.parse(localStorage.getItem("auth_params")), } + var defaultProvider = this.addAccountBasedSyncProvider(params); + defaultProvider.syncToken = localStorage.getItem("syncToken"); + localStorage.removeItem("mk"); + localStorage.removeItem("syncToken"); + localStorage.removeItem("auth_params"); + localStorage.removeItem("user"); + localStorage.removeItem("server"); this.didMakeChangesToSyncProviders(); } } } + addAccountBasedSyncProvider({url, email, uuid, ek, jwt, auth_params} = {}) { + var provider = new SyncProvider({ + url: url + "/items/sync", + primary: !this.primarySyncProvider(), + email: email, + uuid: uuid, + jwt: jwt, + auth_params: auth_params, + type: SN.SyncProviderType.account + }); + + provider.keyName = provider.name; + + this.syncProviders.push(provider); + + this.didMakeChangesToSyncProviders(); + + this.keyManager.addKey(provider.keyName, ek); + + if(this.syncProviders.length == 0) { + this.enableSyncProvider(provider, true); + } + + return provider; + } + addSyncProviderFromURL(url) { var provider = new SyncProvider({url: url}); + provider.type = SN.SyncProviderType.URL; this.syncProviders.push(provider); this.didMakeChangesToSyncProviders(); } @@ -123,8 +150,6 @@ class SyncManager { this.addAllDataAsNeedingSyncForProvider(syncProvider); this.didMakeChangesToSyncProviders(); this.syncWithProvider(syncProvider); - this.syncWithProvider(syncProvider); - this.syncWithProvider(syncProvider); } addAllDataAsNeedingSyncForProvider(syncProvider) { diff --git a/app/assets/javascripts/app/services/sync/syncProvider.js b/app/assets/javascripts/app/services/sync/syncProvider.js index 0e197e8d9..c9dceb4e4 100644 --- a/app/assets/javascripts/app/services/sync/syncProvider.js +++ b/app/assets/javascripts/app/services/sync/syncProvider.js @@ -1,3 +1,8 @@ +SN.SyncProviderType = { + Account: 1, + URL: 2 +} + class SyncProvider { constructor(obj) { @@ -31,13 +36,28 @@ class SyncProvider { else return "secondary"; } + get name() { + if(this.type == SN.SyncProviderType.account) { + return this.email + "@" + this.url; + } else { + return this.url; + } + } + asJSON() { return { enabled: this.enabled, url: this.url, + type: this.type, primary: this.primary, keyName: this.keyName, - syncToken: this.syncToken + syncToken: this.syncToken, + + // account based + email: this.email, + uuid: this.uuid, + jwt: this.jwt, + auth_params: this.auth_params } } } diff --git a/app/assets/javascripts/app/services/sync/syncRunner.js b/app/assets/javascripts/app/services/sync/syncRunner.js index 4172e3a8e..f79b7c694 100644 --- a/app/assets/javascripts/app/services/sync/syncRunner.js +++ b/app/assets/javascripts/app/services/sync/syncRunner.js @@ -39,7 +39,6 @@ class SyncRunner { } syncOffline(items, callback) { - console.log("Writing items offline", items); this.writeItemsToLocalStorage(items, true, function(responseItems){ // delete anything needing to be deleted for(var item of items) { @@ -128,7 +127,9 @@ class SyncRunner { request.sync_token = provider.syncToken; request.cursor_token = provider.cursorToken; - request.post().then(function(response) { + var headers = provider.jwt ? {Authorization: "Bearer " + provider.jwt} : {}; + request.post("", undefined, undefined, headers).then(function(response) { + provider.error = null; if(!provider.primary) { console.log("Completed sync for provider:", provider.url, "Response:", response); @@ -170,15 +171,19 @@ class SyncRunner { }.bind(this)) .catch(function(response){ console.log("Sync error: ", response); + var error = response.data.error || {message: "Could not connect to server."}; // Re-add subItems since this operation failed. We'll have to try again. provider.addPendingItems(subItems); provider.syncOpInProgress = false; + provider.error = error; if(provider.primary) { this.writeItemsToLocalStorage(allItems, false, null); } + this.rootScope.$broadcast("sync:error", error); + if(callback) { callback({error: "Sync error"}); } diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index a69286884..00729ed13 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -14,6 +14,10 @@ margin-top: 10px; } +.mt-15 { + margin-top: 15px; +} + .faded { opacity: 0.5; } @@ -74,11 +78,15 @@ section { padding: 5px; + padding-bottom: 2px; margin-top: 5px; &.inline-h { - padding: 5px 0px; + padding-top: 5px; + padding-left: 0; + padding-right: 0; } + } input { @@ -121,6 +129,10 @@ .large-padding { padding: 15px; } + + .red { + color: red; + } } .footer-bar-link { diff --git a/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml index 758c4b570..94ed496b3 100644 --- a/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml +++ b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml @@ -23,7 +23,7 @@ %label.center-align.block.faded — OR — %a.block.center-align.medium-text{"ng-if" => "!formData.showAddLinkForm", "ng-click" => "formData.showAddLinkForm = true"} Add sync using secret link %form{"ng-if" => "formData.showAddLinkForm"} - %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.url'} + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.secretUrl'} %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} Add Sync Account %a.block.center-align.mt-5{"ng-click" => "formData.showAddLinkForm = false"} Cancel diff --git a/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml index 6d430bd91..d1ca0da2e 100644 --- a/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml +++ b/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml @@ -3,7 +3,7 @@ %div{"ng-if" => "showSection"} .small-v-space - %section.white-bg{"ng-repeat" => "provider in syncProviders"} + %section.white-bg.medium-padding{"ng-repeat" => "provider in syncProviders"} %label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} %em{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} %p {{provider.url}} @@ -16,11 +16,15 @@ {{key.name}} %button{"ng-click" => "saveKey(provider)"} Set - %div{"ng-if" => "!provider.enabled"} - %button.light{"ng-click" => "enableSyncProvider(provider, true)"} Set as Main - %button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary + %button.light{"ng-if" => "!provider.enabled || !provider.primary", "ng-click" => "enableSyncProvider(provider, true)"} Set as Main + %button.light{"ng-if" => "syncProviders.length > 1 && (provider.primary || !provider.enabled)", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary + %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key - %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider - %div{"style" => "height: 30px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} - %strong{"style" => "float: left;"} Syncing: {{provider.syncStatus.statusString}} - .spinner{"style" => "float: right"} + %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Account + + .mt-15{"ng-if" => "provider.error"} + %strong.red Error syncing: {{provider.error.message}} + .mt-15{"style" => "height: 15px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} + .spinner{"style" => "float: left; margin-top: 3px; margin-left: 2px;"} + %strong{"style" => "float: left; margin-left: 7px;"} Syncing: +   {{provider.syncStatus.statusString}} diff --git a/app/assets/templates/frontend/header.html.haml b/app/assets/templates/frontend/header.html.haml index a9d7371cb..f1a72f35f 100644 --- a/app/assets/templates/frontend/header.html.haml +++ b/app/assets/templates/frontend/header.html.haml @@ -1,7 +1,7 @@ .footer-bar .pull-left .footer-bar-link - %a{"ng-click" => "ctrl.accountMenuPressed()"} Account + %a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account %account-data-menu{"ng-if" => "ctrl.showAccountMenu"} .footer-bar-link @@ -14,11 +14,12 @@ .pull-right - .footer-bar-link + .footer-bar-link{"style" => "margin-right: 5px;"} %div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"} %span{"ng-if" => "!ctrl.isRefreshing"} Last refreshed {{ctrl.lastSyncDate | appDateTime}} %span{"ng-if" => "ctrl.isRefreshing"} .spinner{"style" => "margin-top: 2px;"} - %a{"ng-click" => "ctrl.refreshData()"} Refresh + %strong{"ng-if" => "ctrl.offline"} Offline + %a{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} Refresh From 3029a97c0e96fbcb099dee0c1d9d7b32d42bb820 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 26 Jan 2017 19:59:55 -0600 Subject: [PATCH 11/25] editor error message --- .../app/frontend/controllers/editor.js | 2 +- .../app/services/sync/syncRunner.js | 2 +- app/assets/stylesheets/app/_header.scss | 94 ++++++++++--------- .../templates/frontend/editor.html.haml | 2 +- 4 files changed, 52 insertions(+), 48 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index d0e1ae539..3ed103bca 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -148,11 +148,11 @@ angular.module('app.frontend') if(success) { if(statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function(){ - this.saveError = false; var status = "All changes saved" if(syncManager.offline) { status += " (offline)"; } + this.saveError = false; this.noteStatus = status; }.bind(this), 200) } else { diff --git a/app/assets/javascripts/app/services/sync/syncRunner.js b/app/assets/javascripts/app/services/sync/syncRunner.js index f79b7c694..9c1734409 100644 --- a/app/assets/javascripts/app/services/sync/syncRunner.js +++ b/app/assets/javascripts/app/services/sync/syncRunner.js @@ -171,7 +171,7 @@ class SyncRunner { }.bind(this)) .catch(function(response){ console.log("Sync error: ", response); - var error = response.data.error || {message: "Could not connect to server."}; + var error = response.data ? response.data.error : {message: "Could not connect to server."}; // Re-add subItems since this operation failed. We'll have to try again. provider.addPendingItems(subItems); diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index 00729ed13..fdd951bc2 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -1,27 +1,71 @@ .pull-left { - float: left; + float: left !important; } .pull-right { - float: right; + float: right !important; } .mt-5 { - margin-top: 5px; + margin-top: 5px !important; } .mt-10 { - margin-top: 10px; + margin-top: 10px !important; } .mt-15 { - margin-top: 15px; + margin-top: 15px !important; } .faded { opacity: 0.5; } +.center-align { + text-align: center !important; +} + +.center { + margin-left: auto !important; + margin-right: auto !important; +} + +.block { + display: block !important; +} + +.wrap { + word-wrap: break-word; +} + +.one-line-overflow { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + +.small-v-space { + height: 6px; + display: block; +} + +.medium-padding { + padding: 10px !important; +} + +.large-padding { + padding: 15px !important; +} + +.red { + color: red !important; +} + +.bold { + font-weight: bold !important; +} + .footer-bar { position: relative; width: 100%; @@ -93,46 +137,6 @@ margin-bottom: 10px; border-radius: 0px; } - - .center-align { - text-align: center; - } - - .center { - margin-left: auto; - margin-right: auto; - } - - .block { - display: block; - } - - .wrap { - word-wrap: break-word; - } - - .one-line-overflow { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - } - - .small-v-space { - height: 6px; - display: block; - } - - .medium-padding { - padding: 10px; - } - - .large-padding { - padding: 15px; - } - - .red { - color: red; - } } .footer-bar-link { diff --git a/app/assets/templates/frontend/editor.html.haml b/app/assets/templates/frontend/editor.html.haml index 322b42d7f..50461f012 100644 --- a/app/assets/templates/frontend/editor.html.haml +++ b/app/assets/templates/frontend/editor.html.haml @@ -5,7 +5,7 @@ %input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)", "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()", "select-on-click" => "true"} - .save-status {{ctrl.noteStatus}} + .save-status{"ng-class" => "{'red bold': ctrl.saveError}"} {{ctrl.noteStatus}} .tags %input.tags-input{"type" => "text", "ng-keyup" => "$event.keyCode == 13 && ctrl.updateTagsFromTagsString($event, ctrl.tagsString)", "ng-model" => "ctrl.tagsString", "placeholder" => "#tags", "ng-blur" => "ctrl.updateTagsFromTagsString($event, ctrl.tagsString)"} From 7c88531cff056971edf82fb04a142efe2740db6c Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Fri, 27 Jan 2017 13:54:23 -0600 Subject: [PATCH 12/25] updates --- .../app/frontend/controllers/header.js | 8 +++- .../directives/accountNewAccountSection.js | 24 ++--------- .../services/directives/accountSyncSection.js | 3 +- .../app/services/sync/syncManager.js | 7 ++- .../app/services/sync/syncProvider.js | 4 ++ .../app/services/sync/syncRunner.js | 11 ++--- app/assets/stylesheets/app/_header.scss | 4 ++ .../account-new-account-section.html.haml | 43 +++++++++++-------- .../account-sync-section.html.haml | 4 +- 9 files changed, 53 insertions(+), 55 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js index 822e788da..c6ac21d32 100644 --- a/app/assets/javascripts/app/frontend/controllers/header.js +++ b/app/assets/javascripts/app/frontend/controllers/header.js @@ -13,9 +13,11 @@ angular.module('app.frontend') scope.$on("sync:updated_token", function(){ ctrl.syncUpdated(); ctrl.findErrors(); + ctrl.updateOfflineStatus(); }) scope.$on("sync:error", function(){ ctrl.findErrors(); + ctrl.updateOfflineStatus(); }) } } @@ -23,7 +25,11 @@ angular.module('app.frontend') .controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager, syncManager) { this.user = apiController.user; - this.offline = syncManager.offline; + + this.updateOfflineStatus = function() { + this.offline = syncManager.offline; + } + this.updateOfflineStatus(); this.findErrors = function() { this.error = syncManager.syncProviders.filter(function(provider){return provider.error}).length > 0 ? true : false; diff --git a/app/assets/javascripts/app/services/directives/accountNewAccountSection.js b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js index b11bb571e..06c19c181 100644 --- a/app/assets/javascripts/app/services/directives/accountNewAccountSection.js +++ b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js @@ -59,14 +59,14 @@ class AccountNewAccountSection { console.log("logging in with url", $scope.formData.url); $timeout(function(){ apiController.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ + $scope.formData.status = null; if(!response || response.error) { var error = response ? response.error : {message: "An unknown error occured."} - $scope.formData.status = null; if(!response || (response && !response.didDisplayAlert)) { alert(error.message); } } else { - $scope.onAuthSuccess(response.user); + $scope.showForm = false; } }); }) @@ -77,12 +77,12 @@ class AccountNewAccountSection { $timeout(function(){ apiController.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ + $scope.formData.status = null; if(!response || response.error) { var error = response ? response.error : {message: "An unknown error occured."} - $scope.formData.status = null; alert(error.message); } else { - $scope.onAuthSuccess(response.user); + $scope.showForm = false; } }); }) @@ -93,22 +93,6 @@ class AccountNewAccountSection { return allNotes.length + "/" + allNotes.length + " notes encrypted"; } - $scope.onAuthSuccess = function(user) { - var block = function(){ - window.location.reload(); - $scope.showLogin = false; - $scope.showRegistration = false; - }; - - if(!$scope.formData.mergeLocal) { - dbManager.clearAllItems(function(){ - block(); - }); - } else { - block(); - } - } - } } diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js index 630c416a2..7bb03f4fd 100644 --- a/app/assets/javascripts/app/services/directives/accountSyncSection.js +++ b/app/assets/javascripts/app/services/directives/accountSyncSection.js @@ -10,9 +10,10 @@ class AccountSyncSection { controller($scope, modelManager, keyManager, syncManager) { 'ngInject'; + $scope.syncManager = syncManager; $scope.syncProviders = syncManager.syncProviders; $scope.keys = keyManager.keys; - $scope.showSection = $scope.syncProviders.length > 0; + // $scope.showSection = syncManager.syncProviders.length > 0; $scope.enableSyncProvider = function(provider, primary) { if(!provider.keyName) { diff --git a/app/assets/javascripts/app/services/sync/syncManager.js b/app/assets/javascripts/app/services/sync/syncManager.js index 54cb16501..61c07d88c 100644 --- a/app/assets/javascripts/app/services/sync/syncManager.js +++ b/app/assets/javascripts/app/services/sync/syncManager.js @@ -17,7 +17,8 @@ class SyncManager { } defaultServerURL() { - return "https://n3.standardnotes.org"; + // return "https://n3.standardnotes.org"; + return "http://localhost:3000"; } get enabledProviders() { @@ -122,9 +123,7 @@ class SyncManager { this.keyManager.addKey(provider.keyName, ek); - if(this.syncProviders.length == 0) { - this.enableSyncProvider(provider, true); - } + this.enableSyncProvider(provider, this.enabledProviders == 0); return provider; } diff --git a/app/assets/javascripts/app/services/sync/syncProvider.js b/app/assets/javascripts/app/services/sync/syncProvider.js index c9dceb4e4..6c6767317 100644 --- a/app/assets/javascripts/app/services/sync/syncProvider.js +++ b/app/assets/javascripts/app/services/sync/syncProvider.js @@ -27,6 +27,10 @@ class SyncProvider { return this.keyName == SNKeyName; } + get secondary() { + return this.status == "secondary"; + } + get status() { if(!this.enabled) { return null; diff --git a/app/assets/javascripts/app/services/sync/syncRunner.js b/app/assets/javascripts/app/services/sync/syncRunner.js index 9c1734409..fc4398583 100644 --- a/app/assets/javascripts/app/services/sync/syncRunner.js +++ b/app/assets/javascripts/app/services/sync/syncRunner.js @@ -110,7 +110,6 @@ class SyncRunner { provider.syncStatus.current = 0; } - console.log("Syncing with provider:", provider.url, "items:", subItems.length); // Remove dirty items now. If this operation fails, we'll re-add them. // This allows us to queue changes on the same item @@ -124,16 +123,15 @@ class SyncRunner { return itemParams.paramsForSync(); }.bind(this)); - request.sync_token = provider.syncToken; + // request.sync_token = provider.syncToken; request.cursor_token = provider.cursorToken; + console.log("Syncing with provider:", provider, "items:", subItems.length, "token", request.sync_token); var headers = provider.jwt ? {Authorization: "Bearer " + provider.jwt} : {}; request.post("", undefined, undefined, headers).then(function(response) { provider.error = null; - if(!provider.primary) { - console.log("Completed sync for provider:", provider.url, "Response:", response); - } + console.log("Completed sync for provider:", provider.url, "Response:", response.plain()); provider.syncToken = response.sync_token; @@ -155,9 +153,6 @@ class SyncRunner { } provider.syncOpInProgress = false; - if(!provider.primary) { - console.log("Adding", subItems.length, "to", provider.syncStatus.current); - } provider.syncStatus.current += subItems.length; if(provider.cursorToken || provider.repeatOnCompletion == true) { diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index fdd951bc2..54663a566 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -18,6 +18,10 @@ margin-top: 15px !important; } +.mb-10 { + margin-bottom: 10px !important; +} + .faded { opacity: 0.5; } diff --git a/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml index 94ed496b3..b015a1443 100644 --- a/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml +++ b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml @@ -3,30 +3,35 @@ %div{"ng-if" => "showForm"} %p Enter your Standard File account information. .small-v-space + %form.account-form{'name' => "loginForm"} %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'} %input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'formData.email'} %input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'} - .checkbox{"ng-if" => "localNotesCount() > 0"} - %label - %input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} - Merge local notes ({{localNotesCount()}} notes) - %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span Sign In - %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span Register - %br - .block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"} - %a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten. - %em{"ng-if" => "formData.status", "style" => "font-size: 14px;"} {{formData.status}} - %label.center-align.block.faded — OR — - %a.block.center-align.medium-text{"ng-if" => "!formData.showAddLinkForm", "ng-click" => "formData.showAddLinkForm = true"} Add sync using secret link - %form{"ng-if" => "formData.showAddLinkForm"} - %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.secretUrl'} - %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} - Add Sync Account - %a.block.center-align.mt-5{"ng-click" => "formData.showAddLinkForm = false"} Cancel + %div{"ng-if" => "!formData.status"} + .checkbox{"ng-if" => "localNotesCount() > 0"} + %label + %input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} + Merge local notes ({{localNotesCount()}} notes) + %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span Sign In + %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span Register + %br + .block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"} + %a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten. + + %div{"ng-if" => "!formData.status"} + %label.center-align.block.faded — OR — + %a.block.center-align.medium-text{"ng-if" => "!formData.showAddLinkForm", "ng-click" => "formData.showAddLinkForm = true"} Add sync using secret link + %form{"ng-if" => "formData.showAddLinkForm"} + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.secretUrl'} + %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} + Add Sync Account + %a.block.center-align.mt-5{"ng-click" => "formData.showAddLinkForm = false"} Cancel + + %em.block.center-align.mt-10{"ng-if" => "formData.status", "style" => "font-size: 14px;"} {{formData.status}} %div{"ng-if" => "showResetForm"} %p{"style" => "font-size: 13px; text-align: center;"} diff --git a/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml index d1ca0da2e..194e7e8eb 100644 --- a/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml +++ b/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml @@ -1,7 +1,7 @@ %h3{"ng-click" => "showSection = !showSection"} %a Your sync accounts ({{syncProviders.length}}) -%div{"ng-if" => "showSection"} +%div{"ng-if" => "showSection || syncManager.syncProviders.length > 0"} .small-v-space %section.white-bg.medium-padding{"ng-repeat" => "provider in syncProviders"} %label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} @@ -17,7 +17,7 @@ %button{"ng-click" => "saveKey(provider)"} Set %button.light{"ng-if" => "!provider.enabled || !provider.primary", "ng-click" => "enableSyncProvider(provider, true)"} Set as Main - %button.light{"ng-if" => "syncProviders.length > 1 && (provider.primary || !provider.enabled)", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary + %button.light{"ng-if" => "syncProviders.length > 1 && !provider.secondary && (!provider.primary || !provider.enabled)", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Account From ce053226a55599cb4f577cc727b841902e4dd6ba Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Fri, 27 Jan 2017 18:32:27 -0600 Subject: [PATCH 13/25] before destruction --- .../app/frontend/models/local/itemParams.js | 5 ++-- .../directives/accountNewAccountSection.js | 20 +-------------- .../javascripts/app/services/keyManager.js | 4 ++- .../app/services/sync/encryptionHelper.js | 9 +++---- .../app/services/sync/syncManager.js | 11 ++++---- .../app/services/sync/syncProvider.js | 2 +- .../app/services/sync/syncRunner.js | 25 +++++++++---------- .../account-new-account-section.html.haml | 4 --- 8 files changed, 29 insertions(+), 51 deletions(-) diff --git a/app/assets/javascripts/app/frontend/models/local/itemParams.js b/app/assets/javascripts/app/frontend/models/local/itemParams.js index 033360956..396a51bd1 100644 --- a/app/assets/javascripts/app/frontend/models/local/itemParams.js +++ b/app/assets/javascripts/app/frontend/models/local/itemParams.js @@ -1,9 +1,8 @@ class ItemParams { - constructor(item, ek, encryptionHelper) { + constructor(item, ek) { this.item = item; this.ek = ek; - this.encryptionHelper = encryptionHelper; } paramsForExportFile() { @@ -34,7 +33,7 @@ class ItemParams { var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at}; if(this.ek) { - this.encryptionHelper.encryptItem(itemCopy, this.ek); + EncryptionHelper.encryptItem(itemCopy, this.ek); params.content = itemCopy.content; params.enc_item_key = itemCopy.enc_item_key; params.auth_hash = itemCopy.auth_hash; diff --git a/app/assets/javascripts/app/services/directives/accountNewAccountSection.js b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js index 06c19c181..b7b8161af 100644 --- a/app/assets/javascripts/app/services/directives/accountNewAccountSection.js +++ b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js @@ -10,7 +10,7 @@ class AccountNewAccountSection { controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) { 'ngInject'; - $scope.formData = {mergeLocal: true, url: syncManager.defaultServerURL()}; + $scope.formData = {url: syncManager.defaultServerURL()}; $scope.user = apiController.user; $scope.showForm = syncManager.syncProviders.length == 0; @@ -42,18 +42,6 @@ class AccountNewAccountSection { }) } - $scope.localNotesCount = function() { - return modelManager.filteredNotes.length; - } - - $scope.mergeLocalChanged = function() { - if(!$scope.formData.mergeLocal) { - if(!confirm("Unchecking this option means any of the notes you have written while you were signed out will be deleted. Are you sure you want to discard these notes?")) { - $scope.formData.mergeLocal = true; - } - } - } - $scope.loginSubmitPressed = function() { $scope.formData.status = "Generating Login Keys..."; console.log("logging in with url", $scope.formData.url); @@ -87,12 +75,6 @@ class AccountNewAccountSection { }); }) } - - $scope.encryptionStatusForNotes = function() { - var allNotes = modelManager.filteredNotes; - return allNotes.length + "/" + allNotes.length + " notes encrypted"; - } - } } diff --git a/app/assets/javascripts/app/services/keyManager.js b/app/assets/javascripts/app/services/keyManager.js index 292bc6b7a..9f108474d 100644 --- a/app/assets/javascripts/app/services/keyManager.js +++ b/app/assets/javascripts/app/services/keyManager.js @@ -17,9 +17,11 @@ class KeyManager { } keyForName(name) { - return _.find(this.keys, function(key){ + var keyObj = _.find(this.keys, function(key){ return key.name.toLowerCase() == name.toLowerCase(); }); + + return keyObj ? keyObj.key : null; } deleteKey(name) { diff --git a/app/assets/javascripts/app/services/sync/encryptionHelper.js b/app/assets/javascripts/app/services/sync/encryptionHelper.js index a5b547eab..076786f6d 100644 --- a/app/assets/javascripts/app/services/sync/encryptionHelper.js +++ b/app/assets/javascripts/app/services/sync/encryptionHelper.js @@ -1,8 +1,9 @@ class EncryptionHelper { - encryptItem(item, key) { + 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 { item_key = Neeto.crypto.generateRandomEncryptionKey(); @@ -19,7 +20,7 @@ class EncryptionHelper { item.local_encryption_scheme = "1.0"; } - decryptItem(item, key) { + static decryptItem(item, key) { var item_key = Neeto.crypto.decryptText(item.enc_item_key, key); var ek = Neeto.crypto.firstHalfOfKey(item_key); @@ -34,7 +35,7 @@ class EncryptionHelper { item.content = content; } - decryptMultipleItems(items, key) { + static decryptMultipleItems(items, key) { for (var item of items) { if(item.deleted == true) { continue; @@ -59,5 +60,3 @@ class EncryptionHelper { } } - -angular.module('app.frontend').service('encryptionHelper', EncryptionHelper); diff --git a/app/assets/javascripts/app/services/sync/syncManager.js b/app/assets/javascripts/app/services/sync/syncManager.js index 61c07d88c..67bc4aacf 100644 --- a/app/assets/javascripts/app/services/sync/syncManager.js +++ b/app/assets/javascripts/app/services/sync/syncManager.js @@ -60,7 +60,7 @@ class SyncManager { this.didMakeChangesToSyncProviders(); } - primarySyncProvider() { + get primarySyncProvider() { return _.find(this.syncProviders, {primary: true}); } @@ -107,7 +107,7 @@ class SyncManager { addAccountBasedSyncProvider({url, email, uuid, ek, jwt, auth_params} = {}) { var provider = new SyncProvider({ url: url + "/items/sync", - primary: !this.primarySyncProvider(), + primary: !this.primarySyncProvider, email: email, uuid: uuid, jwt: jwt, @@ -119,8 +119,6 @@ class SyncManager { this.syncProviders.push(provider); - this.didMakeChangesToSyncProviders(); - this.keyManager.addKey(provider.keyName, ek); this.enableSyncProvider(provider, this.enabledProviders == 0); @@ -136,6 +134,9 @@ class SyncManager { } enableSyncProvider(syncProvider, primary) { + // we want to sync the new provider where our current primary one is + syncProvider.syncToken = this.primarySyncProvider ? this.primarySyncProvider.syncToken : null; + if(primary) { for(var provider of this.syncProviders) { provider.primary = false; @@ -161,7 +162,7 @@ class SyncManager { } clearSyncToken() { - var primary = this.primarySyncProvider(); + var primary = this.primarySyncProvider; if(primary) { primary.syncToken = null; } diff --git a/app/assets/javascripts/app/services/sync/syncProvider.js b/app/assets/javascripts/app/services/sync/syncProvider.js index 6c6767317..2647931e1 100644 --- a/app/assets/javascripts/app/services/sync/syncProvider.js +++ b/app/assets/javascripts/app/services/sync/syncProvider.js @@ -16,7 +16,7 @@ class SyncProvider { this.pendingItems = []; } - this.pendingItems = this.pendingItems.concat(items); + this.pendingItems = _.uniqBy(this.pendingItems.concat(items), "uuid"); } removePendingItems(items) { diff --git a/app/assets/javascripts/app/services/sync/syncRunner.js b/app/assets/javascripts/app/services/sync/syncRunner.js index fc4398583..89c123182 100644 --- a/app/assets/javascripts/app/services/sync/syncRunner.js +++ b/app/assets/javascripts/app/services/sync/syncRunner.js @@ -1,10 +1,9 @@ class SyncRunner { - constructor($rootScope, modelManager, dbManager, encryptionHelper, keyManager, Restangular) { + constructor($rootScope, modelManager, dbManager, keyManager, Restangular) { this.rootScope = $rootScope; this.modelManager = modelManager; this.dbManager = dbManager; - this.encryptionHelper = encryptionHelper; this.keyManager = keyManager; this.Restangular = Restangular; } @@ -90,9 +89,9 @@ class SyncRunner { return; } - // whether this is a repeat sync (a continuation from another sync; we use this to update status accurately) - var isRepeatRun = provider.repeatOnCompletion; + var isContinuationSync = provider.needsMoreSync; + provider.repeatOnCompletion = false; provider.syncOpInProgress = true; let submitLimit = 100; @@ -100,17 +99,16 @@ class SyncRunner { var subItems = allItems.slice(0, submitLimit); if(subItems.length < allItems.length) { // more items left to be synced, repeat - provider.repeatOnCompletion = true; + provider.needsMoreSync = true; } else { - provider.repeatOnCompletion = false; + provider.needsMoreSync = false; } - if(!isRepeatRun) { + if(!isContinuationSync) { provider.syncStatus.total = allItems.length; provider.syncStatus.current = 0; } - // Remove dirty items now. If this operation fails, we'll re-add them. // This allows us to queue changes on the same item provider.removePendingItems(subItems); @@ -118,12 +116,13 @@ class SyncRunner { var request = this.Restangular.oneUrl(provider.url, provider.url); request.limit = 150; request.items = _.map(subItems, function(item){ - var itemParams = new ItemParams(item, provider.ek); + var key = this.keyManager.keyForName(provider.keyName); + var itemParams = new ItemParams(item, key); itemParams.additionalFields = options.additionalFields; return itemParams.paramsForSync(); }.bind(this)); - // request.sync_token = provider.syncToken; + request.sync_token = provider.syncToken; request.cursor_token = provider.cursorToken; console.log("Syncing with provider:", provider, "items:", subItems.length, "token", request.sync_token); @@ -155,7 +154,7 @@ class SyncRunner { provider.syncOpInProgress = false; provider.syncStatus.current += subItems.length; - if(provider.cursorToken || provider.repeatOnCompletion == true) { + if(provider.cursorToken || provider.repeatOnCompletion || provider.needsMoreSync) { this.__performSyncWithProvider(provider, options, callback); } else { if(callback) { @@ -206,8 +205,8 @@ class SyncRunner { } handleItemsResponse(responseItems, omitFields, syncProvider) { - var ek = syncProvider ? this.keyManager.keyForName(syncProvider.keyName).key : null; - this.encryptionHelper.decryptMultipleItems(responseItems, ek); + var ek = syncProvider ? this.keyManager.keyForName(syncProvider.keyName) : null; + EncryptionHelper.decryptMultipleItems(responseItems, ek); return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); } } diff --git a/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml index b015a1443..6a9b3cbfa 100644 --- a/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml +++ b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml @@ -10,10 +10,6 @@ %input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'} %div{"ng-if" => "!formData.status"} - .checkbox{"ng-if" => "localNotesCount() > 0"} - %label - %input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} - Merge local notes ({{localNotesCount()}} notes) %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} %span Sign In %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} From f296b0e49ef7f2f04a300a07821327563a27def5 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Fri, 27 Jan 2017 19:39:10 -0600 Subject: [PATCH 14/25] functional minimalism --- app/assets/javascripts/app/app.frontend.js | 16 +- .../app/frontend/controllers/editor.js | 4 +- .../app/frontend/controllers/header.js | 10 +- .../app/frontend/controllers/home.js | 6 +- .../app/frontend/controllers/notes.js | 2 +- .../app/frontend/models/app/extension.js | 4 - .../{apiController.js => authManager.js} | 37 +-- .../services/directives/accountDataMenu.js | 27 --- .../directives/accountExportSection.js | 79 ------- .../services/directives/accountKeysSection.js | 28 --- .../directives/accountNewAccountSection.js | 81 ------- .../services/directives/accountSyncSection.js | 65 ------ .../directives/{ => functional}/autofocus.js | 0 .../directives/{ => functional}/delay-hide.js | 0 .../{ => functional}/file-change.js | 0 .../directives/{ => functional}/lowercase.js | 0 .../{ => functional}/selectOnClick.js | 0 .../services/directives/views/accountMenu.js | 152 +++++++++++++ .../{ => views}/contextualExtensionsMenu.js | 0 .../{ => views}/globalExtensionsMenu.js | 0 .../app/services/extensionManager.js | 6 +- .../{sync => helpers}/encryptionHelper.js | 0 .../javascripts/app/services/keyManager.js | 37 --- .../app/services/sync/syncManager.js | 172 -------------- .../app/services/sync/syncProvider.js | 77 ------- .../app/services/sync/syncRunner.js | 214 ------------------ .../javascripts/app/services/syncManager.js | 189 ++++++++++++++++ app/assets/stylesheets/app/_header.scss | 4 - .../directives/account-data-menu.html.haml | 17 -- .../directives/account-menu.html.haml | 61 +++++ .../account-export-section.html.haml | 31 --- .../account-keys-section.html.haml | 18 -- .../account-new-account-section.html.haml | 35 --- .../account-sync-section.html.haml | 30 --- .../templates/frontend/header.html.haml | 2 +- app/assets/templates/services/.keep | 0 36 files changed, 444 insertions(+), 960 deletions(-) rename app/assets/javascripts/app/services/{apiController.js => authManager.js} (91%) delete mode 100644 app/assets/javascripts/app/services/directives/accountDataMenu.js delete mode 100644 app/assets/javascripts/app/services/directives/accountExportSection.js delete mode 100644 app/assets/javascripts/app/services/directives/accountKeysSection.js delete mode 100644 app/assets/javascripts/app/services/directives/accountNewAccountSection.js delete mode 100644 app/assets/javascripts/app/services/directives/accountSyncSection.js rename app/assets/javascripts/app/services/directives/{ => functional}/autofocus.js (100%) rename app/assets/javascripts/app/services/directives/{ => functional}/delay-hide.js (100%) rename app/assets/javascripts/app/services/directives/{ => functional}/file-change.js (100%) rename app/assets/javascripts/app/services/directives/{ => functional}/lowercase.js (100%) rename app/assets/javascripts/app/services/directives/{ => functional}/selectOnClick.js (100%) create mode 100644 app/assets/javascripts/app/services/directives/views/accountMenu.js rename app/assets/javascripts/app/services/directives/{ => views}/contextualExtensionsMenu.js (100%) rename app/assets/javascripts/app/services/directives/{ => views}/globalExtensionsMenu.js (100%) rename app/assets/javascripts/app/services/{sync => helpers}/encryptionHelper.js (100%) delete mode 100644 app/assets/javascripts/app/services/keyManager.js delete mode 100644 app/assets/javascripts/app/services/sync/syncManager.js delete mode 100644 app/assets/javascripts/app/services/sync/syncProvider.js delete mode 100644 app/assets/javascripts/app/services/sync/syncRunner.js create mode 100644 app/assets/javascripts/app/services/syncManager.js delete mode 100644 app/assets/templates/frontend/directives/account-data-menu.html.haml create mode 100644 app/assets/templates/frontend/directives/account-menu.html.haml delete mode 100644 app/assets/templates/frontend/directives/account-menu/account-export-section.html.haml delete mode 100644 app/assets/templates/frontend/directives/account-menu/account-keys-section.html.haml delete mode 100644 app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml delete mode 100644 app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml delete mode 100644 app/assets/templates/services/.keep diff --git a/app/assets/javascripts/app/app.frontend.js b/app/assets/javascripts/app/app.frontend.js index 60ec42208..da2051625 100644 --- a/app/assets/javascripts/app/app.frontend.js +++ b/app/assets/javascripts/app/app.frontend.js @@ -18,6 +18,20 @@ angular.module('app.frontend', [ 'restangular' ]) -.config(function (RestangularProvider, apiControllerProvider) { +.config(function (RestangularProvider, authManagerProvider) { RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"}); + + RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) { + var token = localStorage.getItem("jwt"); + if(token) { + headers = _.extend(headers, {Authorization: "Bearer " + localStorage.getItem("jwt")}); + } + + return { + element: element, + params: params, + headers: headers, + httpConfig: httpConfig + }; + }); }) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 3ed103bca..43fa67afb 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -88,7 +88,7 @@ angular.module('app.frontend') } } }) - .controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager, syncManager) { + .controller('EditorCtrl', function ($sce, $timeout, authManager, markdownRenderer, $rootScope, extensionManager, syncManager) { this.setNote = function(note, oldNote) { this.editorMode = 'edit'; @@ -149,7 +149,7 @@ angular.module('app.frontend') if(statusTimeout) $timeout.cancel(statusTimeout); statusTimeout = $timeout(function(){ var status = "All changes saved" - if(syncManager.offline) { + if(authManager.offline()) { status += " (offline)"; } this.saveError = false; diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js index c6ac21d32..c0e18fd99 100644 --- a/app/assets/javascripts/app/frontend/controllers/header.js +++ b/app/assets/javascripts/app/frontend/controllers/header.js @@ -1,5 +1,5 @@ angular.module('app.frontend') - .directive("header", function(apiController){ + .directive("header", function(authManager){ return { restrict: 'E', scope: {}, @@ -22,17 +22,17 @@ angular.module('app.frontend') } } }) - .controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager, syncManager) { + .controller('HeaderCtrl', function (authManager, modelManager, $timeout, dbManager, syncManager) { - this.user = apiController.user; + this.user = authManager.user; this.updateOfflineStatus = function() { - this.offline = syncManager.offline; + this.offline = authManager.offline(); } this.updateOfflineStatus(); this.findErrors = function() { - this.error = syncManager.syncProviders.filter(function(provider){return provider.error}).length > 0 ? true : false; + this.error = syncManager.error; } this.findErrors(); diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index 7f46bd22c..aaa854a66 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -1,8 +1,8 @@ angular.module('app.frontend') -.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager) { +.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager, authManager) { $rootScope.bodyClass = "app-body-class"; - syncManager.loadLocalItems(function(items){ + syncManager.loadLocalItems(function(items) { $scope.$apply(); syncManager.sync(null); @@ -138,7 +138,7 @@ angular.module('app.frontend') } syncManager.sync(function(){ - if(syncManager.offline) { + if(authManager.offline()) { // when deleting items while ofline, we need to explictly tell angular to refresh UI setTimeout(function () { $scope.safeApply(); diff --git a/app/assets/javascripts/app/frontend/controllers/notes.js b/app/assets/javascripts/app/frontend/controllers/notes.js index 0a9944c2e..a9e4c37f2 100644 --- a/app/assets/javascripts/app/frontend/controllers/notes.js +++ b/app/assets/javascripts/app/frontend/controllers/notes.js @@ -24,7 +24,7 @@ angular.module('app.frontend') } } }) - .controller('NotesCtrl', function (apiController, $timeout, $rootScope, modelManager) { + .controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager) { $rootScope.$on("editorFocused", function(){ this.showMenu = false; diff --git a/app/assets/javascripts/app/frontend/models/app/extension.js b/app/assets/javascripts/app/frontend/models/app/extension.js index 36f0a8a54..097f9212f 100644 --- a/app/assets/javascripts/app/frontend/models/app/extension.js +++ b/app/assets/javascripts/app/frontend/models/app/extension.js @@ -56,10 +56,6 @@ class Extension extends Item { this.content_type = "Extension"; } - get syncProviderAction() { - return _.find(this.actions, {sync_provider: true}) - } - actionsInGlobalContext() { return this.actions.filter(function(action){ return action.context == "global" || action.sync_provider == true; diff --git a/app/assets/javascripts/app/services/apiController.js b/app/assets/javascripts/app/services/authManager.js similarity index 91% rename from app/assets/javascripts/app/services/apiController.js rename to app/assets/javascripts/app/services/authManager.js index fc847f9fc..8ccdbca6c 100644 --- a/app/assets/javascripts/app/services/apiController.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -1,5 +1,5 @@ angular.module('app.frontend') - .provider('apiController', function () { + .provider('authManager', function () { function domainName() { var domain_comps = location.hostname.split("."); @@ -7,11 +7,11 @@ angular.module('app.frontend') return domain; } - this.$get = function($rootScope, Restangular, modelManager, dbManager, syncManager) { - return new ApiController($rootScope, Restangular, modelManager, dbManager, syncManager); + this.$get = function($rootScope, Restangular, modelManager) { + return new AuthManager($rootScope, Restangular, modelManager); } - function ApiController($rootScope, Restangular, modelManager, dbManager, syncManager) { + function AuthManager($rootScope, Restangular, modelManager) { var userData = localStorage.getItem("user"); if(userData) { @@ -24,9 +24,9 @@ angular.module('app.frontend') } } - /* - Auth - */ + this.offline = function() { + return !this.user; + } this.getAuthParams = function() { return JSON.parse(localStorage.getItem("auth_params")); @@ -91,15 +91,11 @@ angular.module('app.frontend') } this.handleAuthResponse = function(response, email, url, authParams, mk) { - var params = { - url: url, - email: email, - uuid: response.user.uuid, - ek: mk, - jwt: response.token, - auth_params: _.omit(authParams, ["pw_nonce"]) - } - syncManager.addAccountBasedSyncProvider(params); + localStorage.setItem("server", url); + localStorage.setItem("user", JSON.stringify(response.plain())); + localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); + localStorage.setItem("mk", mk); + localStorage.setItem("jwt", response.token); } this.register = function(url, email, password, callback) { @@ -259,14 +255,5 @@ angular.module('app.frontend') return JSON.parse(JSON.stringify(object)); } - this.destroyLocalData = function(callback) { - dbManager.clearAllItems(function(){ - localStorage.clear(); - if(callback) { - callback(); - } - }); - } - } }); diff --git a/app/assets/javascripts/app/services/directives/accountDataMenu.js b/app/assets/javascripts/app/services/directives/accountDataMenu.js deleted file mode 100644 index 244101b28..000000000 --- a/app/assets/javascripts/app/services/directives/accountDataMenu.js +++ /dev/null @@ -1,27 +0,0 @@ -class AccountDataMenu { - - constructor() { - this.restrict = "E"; - this.templateUrl = "frontend/directives/account-data-menu.html"; - this.scope = {}; - } - - controller($scope, apiController, modelManager, keyManager) { - 'ngInject'; - - $scope.keys = keyManager.keys; - - $scope.destroyLocalData = function() { - if(!confirm("Are you sure you want to end your session? This will delete all local items, sync accounts, keys, and extensions.")) { - return; - } - - apiController.destroyLocalData(function(){ - window.location.reload(); - }) - } - - } -} - -angular.module('app.frontend').directive('accountDataMenu', () => new AccountDataMenu); diff --git a/app/assets/javascripts/app/services/directives/accountExportSection.js b/app/assets/javascripts/app/services/directives/accountExportSection.js deleted file mode 100644 index 335ae347b..000000000 --- a/app/assets/javascripts/app/services/directives/accountExportSection.js +++ /dev/null @@ -1,79 +0,0 @@ -class AccountExportSection { - - constructor() { - this.restrict = "E"; - this.templateUrl = "frontend/directives/account-menu/account-export-section.html"; - this.scope = { - }; - } - - controller($scope, apiController, $timeout) { - 'ngInject'; - - $scope.archiveFormData = {encryption_type: $scope.user ? 'mk' : 'ek'}; - $scope.user = apiController.user; - - $scope.downloadDataArchive = function() { - if($scope.archiveFormData.encryption_type == 'ek') { - if(!$scope.archiveFormData.ek) { - alert("You must set an encryption key to export the data encrypted.") - return; - } - } - - var link = document.createElement('a'); - link.setAttribute('download', 'notes.json'); - - var ek = $scope.archiveFormData.encryption_type == 'ek' ? $scope.archiveFormData.ek : null; - var encrypted = $scope.archiveFormData.encryption_type != 'none'; - - link.href = apiController.itemsDataFile(encrypted, ek); - link.click(); - } - - $scope.performImport = function(data, password) { - $scope.importData.loading = true; - // allow loading indicator to come up with timeout - $timeout(function(){ - apiController.importJSONData(data, password, function(success, response){ - console.log("Import response:", success, response); - $scope.importData.loading = false; - if(success) { - $scope.importData = null; - } else { - alert("There was an error importing your data. Please try again."); - } - }) - }) - } - - $scope.submitImportPassword = function() { - $scope.performImport($scope.importData.data, $scope.importData.password); - } - - $scope.importFileSelected = function(files) { - $scope.importData = {}; - - var file = files[0]; - var reader = new FileReader(); - reader.onload = function(e) { - var data = JSON.parse(e.target.result); - $timeout(function(){ - if(data.auth_params) { - // request password - $scope.importData.requestPassword = true; - $scope.importData.data = data; - } else { - $scope.performImport(data, null); - } - }) - } - - reader.readAsText(file); - } - - } - -} - -angular.module('app.frontend').directive('accountExportSection', () => new AccountExportSection); diff --git a/app/assets/javascripts/app/services/directives/accountKeysSection.js b/app/assets/javascripts/app/services/directives/accountKeysSection.js deleted file mode 100644 index db9526488..000000000 --- a/app/assets/javascripts/app/services/directives/accountKeysSection.js +++ /dev/null @@ -1,28 +0,0 @@ -class AccountKeysSection { - - constructor() { - this.restrict = "E"; - this.templateUrl = "frontend/directives/account-menu/account-keys-section.html"; - this.scope = { - }; - } - - controller($scope, apiController, keyManager) { - 'ngInject'; - - $scope.newKeyData = {}; - $scope.keys = keyManager.keys; - - $scope.submitNewKeyForm = function() { - var key = keyManager.addKey($scope.newKeyData.name, $scope.newKeyData.key); - if(!key) { - alert("This key name is already in use. Please use a different name."); - return; - } - - $scope.newKeyData.showForm = false; - } - } -} - -angular.module('app.frontend').directive('accountKeysSection', () => new AccountKeysSection); diff --git a/app/assets/javascripts/app/services/directives/accountNewAccountSection.js b/app/assets/javascripts/app/services/directives/accountNewAccountSection.js deleted file mode 100644 index b7b8161af..000000000 --- a/app/assets/javascripts/app/services/directives/accountNewAccountSection.js +++ /dev/null @@ -1,81 +0,0 @@ -class AccountNewAccountSection { - - constructor() { - this.restrict = "E"; - this.templateUrl = "frontend/directives/account-menu/account-new-account-section.html"; - this.scope = { - }; - } - - controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) { - 'ngInject'; - - $scope.formData = {url: syncManager.defaultServerURL()}; - $scope.user = apiController.user; - - $scope.showForm = syncManager.syncProviders.length == 0; - - $scope.changePasswordPressed = function() { - $scope.showNewPasswordForm = !$scope.showNewPasswordForm; - } - - $scope.submitExternalSyncURL = function() { - syncManager.addSyncProviderFromURL($scope.formData.secretUrl); - $scope.formData.showAddLinkForm = false; - $scope.formData.secretUrl = null; - $scope.showForm = false; - } - - $scope.submitPasswordChange = function() { - $scope.passwordChangeData.status = "Generating New Keys..."; - - $timeout(function(){ - if(data.password != data.password_confirmation) { - alert("Your new password does not match its confirmation."); - return; - } - - apiController.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){ - - }) - - }) - } - - $scope.loginSubmitPressed = function() { - $scope.formData.status = "Generating Login Keys..."; - console.log("logging in with url", $scope.formData.url); - $timeout(function(){ - apiController.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ - $scope.formData.status = null; - if(!response || response.error) { - var error = response ? response.error : {message: "An unknown error occured."} - if(!response || (response && !response.didDisplayAlert)) { - alert(error.message); - } - } else { - $scope.showForm = false; - } - }); - }) - } - - $scope.submitRegistrationForm = function() { - $scope.formData.status = "Generating Account Keys..."; - - $timeout(function(){ - apiController.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ - $scope.formData.status = null; - if(!response || response.error) { - var error = response ? response.error : {message: "An unknown error occured."} - alert(error.message); - } else { - $scope.showForm = false; - } - }); - }) - } - } -} - -angular.module('app.frontend').directive('accountNewAccountSection', () => new AccountNewAccountSection); diff --git a/app/assets/javascripts/app/services/directives/accountSyncSection.js b/app/assets/javascripts/app/services/directives/accountSyncSection.js deleted file mode 100644 index 7bb03f4fd..000000000 --- a/app/assets/javascripts/app/services/directives/accountSyncSection.js +++ /dev/null @@ -1,65 +0,0 @@ -class AccountSyncSection { - - constructor() { - this.restrict = "E"; - this.templateUrl = "frontend/directives/account-menu/account-sync-section.html"; - this.scope = { - }; - } - - controller($scope, modelManager, keyManager, syncManager) { - 'ngInject'; - - $scope.syncManager = syncManager; - $scope.syncProviders = syncManager.syncProviders; - $scope.keys = keyManager.keys; - // $scope.showSection = syncManager.syncProviders.length > 0; - - $scope.enableSyncProvider = function(provider, primary) { - if(!provider.keyName) { - alert("You must choose an encryption key for this account before enabling it."); - return; - } - - syncManager.enableSyncProvider(provider, primary); - } - - $scope.removeSyncProvider = function(provider) { - if(provider.primary) { - alert("You cannot remove your main sync account. Instead, end your session by destroying all local data. Or, choose another account to be your primary sync account.") - return; - } - - if(confirm("Are you sure you want to remove this sync account?")) { - syncManager.removeSyncProvider(provider); - } - } - - $scope.changeEncryptionKey = function(provider) { - if(provider.isStandardNotesAccount) { - alert("To change your encryption key for your Standard File account, you need to change your password. However, this functionality is not currently available."); - return; - } - - if(!confirm("Changing your encryption key will re-encrypt all your notes with the new key and sync them back to the server. This can take several minutes. We strongly recommend downloading a backup of your notes before continuing.")) { - return; - } - - provider.formData = {keyName: provider.keyName}; - provider.showKeyForm = true; - } - - $scope.saveKey = function(provider) { - provider.showKeyForm = false; - provider.keyName = provider.formData.keyName; - syncManager.didMakeChangesToSyncProviders(); - - if(provider.enabled) { - syncManager.addAllDataAsNeedingSyncForProvider(provider); - syncManager.sync(); - } - } - } -} - -angular.module('app.frontend').directive('accountSyncSection', () => new AccountSyncSection); diff --git a/app/assets/javascripts/app/services/directives/autofocus.js b/app/assets/javascripts/app/services/directives/functional/autofocus.js similarity index 100% rename from app/assets/javascripts/app/services/directives/autofocus.js rename to app/assets/javascripts/app/services/directives/functional/autofocus.js diff --git a/app/assets/javascripts/app/services/directives/delay-hide.js b/app/assets/javascripts/app/services/directives/functional/delay-hide.js similarity index 100% rename from app/assets/javascripts/app/services/directives/delay-hide.js rename to app/assets/javascripts/app/services/directives/functional/delay-hide.js diff --git a/app/assets/javascripts/app/services/directives/file-change.js b/app/assets/javascripts/app/services/directives/functional/file-change.js similarity index 100% rename from app/assets/javascripts/app/services/directives/file-change.js rename to app/assets/javascripts/app/services/directives/functional/file-change.js diff --git a/app/assets/javascripts/app/services/directives/lowercase.js b/app/assets/javascripts/app/services/directives/functional/lowercase.js similarity index 100% rename from app/assets/javascripts/app/services/directives/lowercase.js rename to app/assets/javascripts/app/services/directives/functional/lowercase.js diff --git a/app/assets/javascripts/app/services/directives/selectOnClick.js b/app/assets/javascripts/app/services/directives/functional/selectOnClick.js similarity index 100% rename from app/assets/javascripts/app/services/directives/selectOnClick.js rename to app/assets/javascripts/app/services/directives/functional/selectOnClick.js diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js new file mode 100644 index 000000000..c04148bfd --- /dev/null +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -0,0 +1,152 @@ +class AccountMenu { + + constructor() { + this.restrict = "E"; + this.templateUrl = "frontend/directives/account-menu.html"; + this.scope = {}; + } + + controller($scope, authManager, modelManager, syncManager, $timeout) { + 'ngInject'; + + $scope.formData = {url: syncManager.serverURL}; + $scope.user = authManager.user; + + $scope.changePasswordPressed = function() { + $scope.showNewPasswordForm = !$scope.showNewPasswordForm; + } + + $scope.submitPasswordChange = function() { + $scope.passwordChangeData.status = "Generating New Keys..."; + + $timeout(function(){ + if(data.password != data.password_confirmation) { + alert("Your new password does not match its confirmation."); + return; + } + + authManager.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){ + + }) + + }) + } + + $scope.loginSubmitPressed = function() { + $scope.formData.status = "Generating Login Keys..."; + console.log("logging in with url", $scope.formData.url); + $timeout(function(){ + authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ + $scope.formData.status = null; + if(!response || response.error) { + var error = response ? response.error : {message: "An unknown error occured."} + if(!response || (response && !response.didDisplayAlert)) { + alert(error.message); + } + } else { + window.location.reload(); + } + }); + }) + } + + $scope.submitRegistrationForm = function() { + $scope.formData.status = "Generating Account Keys..."; + + $timeout(function(){ + authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ + $scope.formData.status = null; + if(!response || response.error) { + var error = response ? response.error : {message: "An unknown error occured."} + alert(error.message); + } else { + window.location.reload(); + } + }); + }) + } + + $scope.destroyLocalData = function() { + if(!confirm("Are you sure you want to end your session? This will delete all local items and extensions.")) { + return; + } + + syncManager.destroyLocalData(function(){ + window.location.reload(); + }) + } + + + /* Import/Export */ + + $scope.archiveFormData = {encryption_type: $scope.user ? 'mk' : 'ek'}; + $scope.user = authManager.user; + + $scope.downloadDataArchive = function() { + if($scope.archiveFormData.encryption_type == 'ek') { + if(!$scope.archiveFormData.ek) { + alert("You must set an encryption key to export the data encrypted.") + return; + } + } + + var link = document.createElement('a'); + link.setAttribute('download', 'notes.json'); + + var ek = $scope.archiveFormData.encryption_type == 'ek' ? $scope.archiveFormData.ek : null; + var encrypted = $scope.archiveFormData.encryption_type != 'none'; + + link.href = authManager.itemsDataFile(encrypted, ek); + link.click(); + } + + $scope.performImport = function(data, password) { + $scope.importData.loading = true; + // allow loading indicator to come up with timeout + $timeout(function(){ + authManager.importJSONData(data, password, function(success, response){ + console.log("Import response:", success, response); + $scope.importData.loading = false; + if(success) { + $scope.importData = null; + } else { + alert("There was an error importing your data. Please try again."); + } + }) + }) + } + + $scope.submitImportPassword = function() { + $scope.performImport($scope.importData.data, $scope.importData.password); + } + + $scope.importFileSelected = function(files) { + $scope.importData = {}; + + var file = files[0]; + var reader = new FileReader(); + reader.onload = function(e) { + var data = JSON.parse(e.target.result); + $timeout(function(){ + if(data.auth_params) { + // request password + $scope.importData.requestPassword = true; + $scope.importData.data = data; + } else { + $scope.performImport(data, null); + } + }) + } + + reader.readAsText(file); + } + + $scope.encryptionStatusForNotes = function() { + var allNotes = modelManager.filteredNotes; + return allNotes.length + "/" + allNotes.length + " notes encrypted"; + } + + } +} + +angular.module('app.frontend').directive('accountMenu', () => new AccountMenu); diff --git a/app/assets/javascripts/app/services/directives/contextualExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/contextualExtensionsMenu.js similarity index 100% rename from app/assets/javascripts/app/services/directives/contextualExtensionsMenu.js rename to app/assets/javascripts/app/services/directives/views/contextualExtensionsMenu.js diff --git a/app/assets/javascripts/app/services/directives/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js similarity index 100% rename from app/assets/javascripts/app/services/directives/globalExtensionsMenu.js rename to app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index fd691b79a..9dadd8b34 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -1,9 +1,9 @@ class ExtensionManager { - constructor(Restangular, modelManager, apiController, syncManager) { + constructor(Restangular, modelManager, authManager, syncManager) { this.Restangular = Restangular; this.modelManager = modelManager; - this.apiController = apiController; + this.authManager = authManager; this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || []; this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || []; this.extensionEks = JSON.parse(localStorage.getItem("extensionEks")) || {}; @@ -288,7 +288,7 @@ class ExtensionManager { performPost(action, extension, params, callback) { var request = this.Restangular.oneUrl(action.url, action.url); if(this.extensionUsesEncryptedData(extension)) { - request.auth_params = this.apiController.getAuthParams(); + request.auth_params = this.authManager.getAuthParams(); } _.merge(request, params); diff --git a/app/assets/javascripts/app/services/sync/encryptionHelper.js b/app/assets/javascripts/app/services/helpers/encryptionHelper.js similarity index 100% rename from app/assets/javascripts/app/services/sync/encryptionHelper.js rename to app/assets/javascripts/app/services/helpers/encryptionHelper.js diff --git a/app/assets/javascripts/app/services/keyManager.js b/app/assets/javascripts/app/services/keyManager.js deleted file mode 100644 index 9f108474d..000000000 --- a/app/assets/javascripts/app/services/keyManager.js +++ /dev/null @@ -1,37 +0,0 @@ -class KeyManager { - - constructor() { - this.keys = JSON.parse(localStorage.getItem("keys")) || []; - } - - addKey(name, key) { - var existing = this.keyForName(name); - if(existing) { - return null; - } - - var newKey = {name: name, key: key}; - this.keys.push(newKey); - this.persist(); - return newKey; - } - - keyForName(name) { - var keyObj = _.find(this.keys, function(key){ - return key.name.toLowerCase() == name.toLowerCase(); - }); - - return keyObj ? keyObj.key : null; - } - - deleteKey(name) { - _.pull(this.keys, {name: name}); - this.persist(); - } - - persist() { - localStorage.setItem("keys", JSON.stringify(this.keys)); - } -} - -angular.module('app.frontend').service('keyManager', KeyManager); diff --git a/app/assets/javascripts/app/services/sync/syncManager.js b/app/assets/javascripts/app/services/sync/syncManager.js deleted file mode 100644 index 67bc4aacf..000000000 --- a/app/assets/javascripts/app/services/sync/syncManager.js +++ /dev/null @@ -1,172 +0,0 @@ -export const SNKeyName = "Standard Notes Key"; - -class SyncManager { - - constructor(modelManager, syncRunner, keyManager) { - this.modelManager = modelManager; - this.keyManager = keyManager; - this.syncRunner = syncRunner; - this.syncRunner.setOnChangeProviderCallback(function(){ - this.didMakeChangesToSyncProviders(); - }.bind(this)) - this.loadSyncProviders(); - } - - get offline() { - return this.enabledProviders.length == 0; - } - - defaultServerURL() { - // return "https://n3.standardnotes.org"; - return "http://localhost:3000"; - } - - get enabledProviders() { - return this.syncProviders.filter(function(provider){return provider.enabled == true}); - } - - /* Used when adding a new account with */ - markAllOfflineItemsDirtyAndSave() { - - } - - sync(callback) { - this.syncRunner.sync(this.enabledProviders, callback); - } - - syncWithProvider(provider, callback) { - this.syncRunner.performSyncWithProvider(provider, callback); - } - - loadLocalItems(callback) { - this.syncRunner.loadLocalItems(callback); - } - - syncProviderForURL(url) { - var provider = _.find(this.syncProviders, {url: url}); - return provider; - } - - findOrCreateSyncProviderForUrl(url) { - var provider = _.find(this.syncProviders, {url: url}); - if(!provider) { - provider = new SyncProvider({url: url}) - } - return provider; - } - - setEncryptionStatusForProviderURL(providerURL, encrypted) { - this.providerForURL(providerURL).encrypted = encrypted; - this.didMakeChangesToSyncProviders(); - } - - get primarySyncProvider() { - return _.find(this.syncProviders, {primary: true}); - } - - didMakeChangesToSyncProviders() { - localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) { - return provider.asJSON() - }))); - } - - loadSyncProviders() { - this.syncProviders = []; - var saved = localStorage.getItem("syncProviders"); - if(saved) { - var parsed = JSON.parse(saved); - for(var p of parsed) { - this.syncProviders.push(new SyncProvider(p)); - } - } else { - // no providers saved, this means migrating from old system to new - // check if user is signed in - var userJSON = localStorage.getItem("user"); - if(this.offline && userJSON) { - var user = JSON.parse(userJSON); - var params = { - url: localStorage.getItem("server"), - email: user.email, - uuid: user.uuid, - ek: localStorage.getItem("mk"), - jwt: response.token, - auth_params: JSON.parse(localStorage.getItem("auth_params")), - } - var defaultProvider = this.addAccountBasedSyncProvider(params); - defaultProvider.syncToken = localStorage.getItem("syncToken"); - localStorage.removeItem("mk"); - localStorage.removeItem("syncToken"); - localStorage.removeItem("auth_params"); - localStorage.removeItem("user"); - localStorage.removeItem("server"); - this.didMakeChangesToSyncProviders(); - } - } - } - - addAccountBasedSyncProvider({url, email, uuid, ek, jwt, auth_params} = {}) { - var provider = new SyncProvider({ - url: url + "/items/sync", - primary: !this.primarySyncProvider, - email: email, - uuid: uuid, - jwt: jwt, - auth_params: auth_params, - type: SN.SyncProviderType.account - }); - - provider.keyName = provider.name; - - this.syncProviders.push(provider); - - this.keyManager.addKey(provider.keyName, ek); - - this.enableSyncProvider(provider, this.enabledProviders == 0); - - return provider; - } - - addSyncProviderFromURL(url) { - var provider = new SyncProvider({url: url}); - provider.type = SN.SyncProviderType.URL; - this.syncProviders.push(provider); - this.didMakeChangesToSyncProviders(); - } - - enableSyncProvider(syncProvider, primary) { - // we want to sync the new provider where our current primary one is - syncProvider.syncToken = this.primarySyncProvider ? this.primarySyncProvider.syncToken : null; - - if(primary) { - for(var provider of this.syncProviders) { - provider.primary = false; - } - } - - syncProvider.enabled = true; - syncProvider.primary = primary; - - // since we're enabling a new provider, we need to send it EVERYTHING we have now. - this.addAllDataAsNeedingSyncForProvider(syncProvider); - this.didMakeChangesToSyncProviders(); - this.syncWithProvider(syncProvider); - } - - addAllDataAsNeedingSyncForProvider(syncProvider) { - syncProvider.addPendingItems(this.modelManager.allItems); - } - - removeSyncProvider(provider) { - _.pull(this.syncProviders, provider); - this.didMakeChangesToSyncProviders(); - } - - clearSyncToken() { - var primary = this.primarySyncProvider; - if(primary) { - primary.syncToken = null; - } - } -} - -angular.module('app.frontend').service('syncManager', SyncManager); diff --git a/app/assets/javascripts/app/services/sync/syncProvider.js b/app/assets/javascripts/app/services/sync/syncProvider.js deleted file mode 100644 index 2647931e1..000000000 --- a/app/assets/javascripts/app/services/sync/syncProvider.js +++ /dev/null @@ -1,77 +0,0 @@ -SN.SyncProviderType = { - Account: 1, - URL: 2 -} - -class SyncProvider { - - constructor(obj) { - this.encrypted = true; - this.syncStatus = new SyncStatus(); - _.merge(this, obj); - } - - addPendingItems(items) { - if(!this.pendingItems) { - this.pendingItems = []; - } - - this.pendingItems = _.uniqBy(this.pendingItems.concat(items), "uuid"); - } - - removePendingItems(items) { - this.pendingItems = _.difference(this.pendingItems, items); - } - - get isStandardNotesAccount() { - return this.keyName == SNKeyName; - } - - get secondary() { - return this.status == "secondary"; - } - - get status() { - if(!this.enabled) { - return null; - } - - if(this.primary) return "primary"; - else return "secondary"; - } - - get name() { - if(this.type == SN.SyncProviderType.account) { - return this.email + "@" + this.url; - } else { - return this.url; - } - } - - asJSON() { - return { - enabled: this.enabled, - url: this.url, - type: this.type, - primary: this.primary, - keyName: this.keyName, - syncToken: this.syncToken, - - // account based - email: this.email, - uuid: this.uuid, - jwt: this.jwt, - auth_params: this.auth_params - } - } -} - -class SyncStatus { - constructor() { - - } - - get statusString() { - return `${this.current}/${this.total}` - } -} diff --git a/app/assets/javascripts/app/services/sync/syncRunner.js b/app/assets/javascripts/app/services/sync/syncRunner.js deleted file mode 100644 index 89c123182..000000000 --- a/app/assets/javascripts/app/services/sync/syncRunner.js +++ /dev/null @@ -1,214 +0,0 @@ -class SyncRunner { - - constructor($rootScope, modelManager, dbManager, keyManager, Restangular) { - this.rootScope = $rootScope; - this.modelManager = modelManager; - this.dbManager = dbManager; - this.keyManager = keyManager; - this.Restangular = Restangular; - } - - setOnChangeProviderCallback(callback) { - this.onChangeProviderCallback = callback; - } - - didMakeChangesToSyncProvider(provider) { - this.onChangeProviderCallback(provider); - } - - writeItemsToLocalStorage(items, offlineOnly, callback) { - var params = items.map(function(item) { - var itemParams = new ItemParams(item, null); - itemParams = itemParams.paramsForLocalStorage(); - if(offlineOnly) { - delete itemParams.dirty; - } - return itemParams; - }.bind(this)); - - this.dbManager.saveItems(params, callback); - } - - loadLocalItems(callback) { - var params = this.dbManager.getAllItems(function(items){ - var items = this.handleItemsResponse(items, null, null); - Item.sortItemsByDate(items); - callback(items); - }.bind(this)) - } - - syncOffline(items, callback) { - this.writeItemsToLocalStorage(items, true, function(responseItems){ - // delete anything needing to be deleted - for(var item of items) { - if(item.deleted) { - this.modelManager.removeItemLocally(item); - } - } - }.bind(this)) - - if(callback) { - callback(); - } - } - - sync(providers, callback, options = {}) { - - var allDirtyItems = this.modelManager.getDirtyItems(); - - // we want to write all dirty items to disk only if the user has no sync providers, or if the sync op fails - // if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server - if(providers.length == 0) { - this.syncOffline(allDirtyItems, callback); - } - - for(let provider of providers) { - provider.addPendingItems(allDirtyItems); - this.didMakeChangesToSyncProvider(provider); - - this.__performSyncWithProvider(provider, options, function(response){ - if(provider.primary) { - if(callback) { - callback(response) - } - } - }) - } - - this.modelManager.clearDirtyItems(allDirtyItems); - } - - performSyncWithProvider(provider, callback) { - this.__performSyncWithProvider(provider, {}, callback); - } - - __performSyncWithProvider(provider, options, callback) { - if(provider.syncOpInProgress) { - provider.repeatOnCompletion = true; - console.log("Sync op in progress for provider; returning.", provider); - return; - } - - var isContinuationSync = provider.needsMoreSync; - - provider.repeatOnCompletion = false; - provider.syncOpInProgress = true; - - let submitLimit = 100; - var allItems = provider.pendingItems; - var subItems = allItems.slice(0, submitLimit); - if(subItems.length < allItems.length) { - // more items left to be synced, repeat - provider.needsMoreSync = true; - } else { - provider.needsMoreSync = false; - } - - if(!isContinuationSync) { - provider.syncStatus.total = allItems.length; - provider.syncStatus.current = 0; - } - - // Remove dirty items now. If this operation fails, we'll re-add them. - // This allows us to queue changes on the same item - provider.removePendingItems(subItems); - - var request = this.Restangular.oneUrl(provider.url, provider.url); - request.limit = 150; - request.items = _.map(subItems, function(item){ - var key = this.keyManager.keyForName(provider.keyName); - var itemParams = new ItemParams(item, key); - itemParams.additionalFields = options.additionalFields; - return itemParams.paramsForSync(); - }.bind(this)); - - request.sync_token = provider.syncToken; - request.cursor_token = provider.cursorToken; - console.log("Syncing with provider:", provider, "items:", subItems.length, "token", request.sync_token); - - var headers = provider.jwt ? {Authorization: "Bearer " + provider.jwt} : {}; - request.post("", undefined, undefined, headers).then(function(response) { - provider.error = null; - - console.log("Completed sync for provider:", provider.url, "Response:", response.plain()); - - provider.syncToken = response.sync_token; - - if(provider.primary) { - this.rootScope.$broadcast("sync:updated_token", provider.syncToken); - - // handle cursor token (more results waiting, perform another sync) - provider.cursorToken = response.cursor_token; - - var retrieved = this.handleItemsResponse(response.retrieved_items, null, provider); - // merge only metadata for saved items - var omitFields = ["content", "auth_hash"]; - var saved = this.handleItemsResponse(response.saved_items, omitFields, provider); - - this.handleUnsavedItemsResponse(response.unsaved, provider) - - this.writeItemsToLocalStorage(saved, false, null); - this.writeItemsToLocalStorage(retrieved, false, null); - } - - provider.syncOpInProgress = false; - provider.syncStatus.current += subItems.length; - - if(provider.cursorToken || provider.repeatOnCompletion || provider.needsMoreSync) { - this.__performSyncWithProvider(provider, options, callback); - } else { - if(callback) { - callback(response); - } - } - - }.bind(this)) - .catch(function(response){ - console.log("Sync error: ", response); - var error = response.data ? response.data.error : {message: "Could not connect to server."}; - - // Re-add subItems since this operation failed. We'll have to try again. - provider.addPendingItems(subItems); - provider.syncOpInProgress = false; - provider.error = error; - - if(provider.primary) { - this.writeItemsToLocalStorage(allItems, false, null); - } - - this.rootScope.$broadcast("sync:error", error); - - if(callback) { - callback({error: "Sync error"}); - } - }.bind(this)) - } - - handleUnsavedItemsResponse(unsaved, provider) { - if(unsaved.length == 0) { - return; - } - - console.log("Handle unsaved", unsaved); - for(var mapping of unsaved) { - var itemResponse = mapping.item; - var item = this.modelManager.findItem(itemResponse.uuid); - var error = mapping.error; - if(error.tag == "uuid_conflict") { - item.alternateUUID(); - item.setDirty(true); - item.markAllReferencesDirty(); - } - } - - this.__performSyncWithProvider(provider, {additionalFields: ["created_at", "updated_at"]}, null); - } - - handleItemsResponse(responseItems, omitFields, syncProvider) { - var ek = syncProvider ? this.keyManager.keyForName(syncProvider.keyName) : null; - EncryptionHelper.decryptMultipleItems(responseItems, ek); - return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); - } -} - -angular.module('app.frontend').service('syncRunner', SyncRunner); diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js new file mode 100644 index 000000000..90239e622 --- /dev/null +++ b/app/assets/javascripts/app/services/syncManager.js @@ -0,0 +1,189 @@ +class SyncManager { + + constructor($rootScope, modelManager, authManager, dbManager, Restangular) { + this.$rootScope = $rootScope; + this.modelManager = modelManager; + this.authManager = authManager; + this.Restangular = Restangular; + this.dbManager = dbManager; + this.syncStatus = {}; + } + + get serverURL() { + return localStorage.getItem("server") || "http://localhost:3000"; + } + + writeItemsToLocalStorage(items, offlineOnly, callback) { + var params = items.map(function(item) { + var itemParams = new ItemParams(item, null); + itemParams = itemParams.paramsForLocalStorage(); + if(offlineOnly) { + delete itemParams.dirty; + } + return itemParams; + }.bind(this)); + + this.dbManager.saveItems(params, callback); + } + + loadLocalItems(callback) { + var params = this.dbManager.getAllItems(function(items){ + var items = this.handleItemsResponse(items, null, null); + Item.sortItemsByDate(items); + callback(items); + }.bind(this)) + } + + syncOffline(items, callback) { + this.writeItemsToLocalStorage(items, true, function(responseItems){ + // delete anything needing to be deleted + for(var item of items) { + if(item.deleted) { + this.modelManager.removeItemLocally(item); + } + } + }.bind(this)) + + if(callback) { + callback(); + } + } + + get syncURL() { + return this.serverURL + "/items/sync"; + } + + sync(callback, options = {}) { + + if(this.syncOpInProgress) { + this.repeatOnCompletion = true; + console.log("Sync op in progress; returning."); + return; + } + + var allDirtyItems = this.modelManager.getDirtyItems(); + + // we want to write all dirty items to disk only if the user is offline, or if the sync op fails + // if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server + if(this.authManager.offline()) { + this.syncOffline(allDirtyItems, callback); + this.modelManager.clearDirtyItems(allDirtyItems); + return; + } + + var isContinuationSync = this.needsMoreSync; + + this.repeatOnCompletion = false; + this.syncOpInProgress = true; + + let submitLimit = 100; + var subItems = allDirtyItems.slice(0, submitLimit); + if(subItems.length < allDirtyItems.length) { + // more items left to be synced, repeat + this.needsMoreSync = true; + } else { + this.needsMoreSync = false; + } + + if(!isContinuationSync) { + this.syncStatus.total = allDirtyItems.length; + this.syncStatus.current = 0; + } + + var request = this.Restangular.oneUrl(this.syncURL, this.syncURL); + request.limit = 150; + request.items = _.map(subItems, function(item){ + var itemParams = new ItemParams(item, localStorage.getItem("mk")); + itemParams.additionalFields = options.additionalFields; + return itemParams.paramsForSync(); + }.bind(this)); + + request.sync_token = this.syncToken; + request.cursor_token = this.cursorToken; + + request.post().then(function(response) { + this.modelManager.clearDirtyItems(subItems); + this.error = null; + this.syncToken = response.sync_token; + this.cursorToken = response.cursor_token; + + this.$rootScope.$broadcast("sync:updated_token", this.syncToken); + + var retrieved = this.handleItemsResponse(response.retrieved_items, null); + // merge only metadata for saved items + var omitFields = ["content", "auth_hash"]; + var saved = this.handleItemsResponse(response.saved_items, omitFields); + + this.handleUnsavedItemsResponse(response.unsaved) + + this.writeItemsToLocalStorage(saved, false, null); + this.writeItemsToLocalStorage(retrieved, false, null); + + this.syncOpInProgress = false; + this.syncStatus.current += subItems.length; + + if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) { + this.sync(callback, options); + } else { + if(callback) { + callback(response); + } + } + + }.bind(this)) + .catch(function(response){ + console.log("Sync error: ", response); + var error = response.data ? response.data.error : {message: "Could not connect to server."}; + + this.syncOpInProgress = false; + this.error = error; + this.writeItemsToLocalStorage(allDirtyItems, false, null); + + this.$rootScope.$broadcast("sync:error", error); + + if(callback) { + callback({error: "Sync error"}); + } + }.bind(this)) + } + + handleUnsavedItemsResponse(unsaved) { + if(unsaved.length == 0) { + return; + } + + console.log("Handle unsaved", unsaved); + for(var mapping of unsaved) { + var itemResponse = mapping.item; + var item = this.modelManager.findItem(itemResponse.uuid); + var error = mapping.error; + if(error.tag == "uuid_conflict") { + item.alternateUUID(); + item.setDirty(true); + item.markAllReferencesDirty(); + } + } + + this.sync(null, {additionalFields: ["created_at", "updated_at"]}); + } + + handleItemsResponse(responseItems, omitFields) { + EncryptionHelper.decryptMultipleItems(responseItems, localStorage.getItem("mk")); + return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields); + } + + clearSyncToken() { + localStorage.removeItem("syncToken"); + } + + destroyLocalData(callback) { + this.dbManager.clearAllItems(function(){ + localStorage.clear(); + if(callback) { + callback(); + } + }); + } +} + +angular.module('app.frontend').service('syncManager', SyncManager); diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index 54663a566..c0c912a7c 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -239,10 +239,6 @@ cursor: default !important; } -.account-panel { - width: 350px; -} - .import-password { margin-top: 14px; diff --git a/app/assets/templates/frontend/directives/account-data-menu.html.haml b/app/assets/templates/frontend/directives/account-data-menu.html.haml deleted file mode 100644 index 2b5128125..000000000 --- a/app/assets/templates/frontend/directives/account-data-menu.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -.panel.panel-default.account-panel.panel-right.account-data-menu - .panel-body - - %section.gray-bg.medium-padding{"ng-init" => "showSN = true"} - %account-new-account-section - - %section.gray-bg.medium-padding - %account-sync-section - - %section.gray-bg.medium-padding - %account-export-section - - %section.gray-bg.medium-padding - %account-keys-section - - %h4 - %a{"ng-click" => "destroyLocalData()"} Destroy all local data diff --git a/app/assets/templates/frontend/directives/account-menu.html.haml b/app/assets/templates/frontend/directives/account-menu.html.haml new file mode 100644 index 000000000..167b4117e --- /dev/null +++ b/app/assets/templates/frontend/directives/account-menu.html.haml @@ -0,0 +1,61 @@ +.panel.panel-default.panel-right.account-data-menu + .panel-body + + -# If not user + %div{"ng-if" => "!user"} + %p Enter your Standard File account information. You can also register for free using the default server address. + .small-v-space + + %form.account-form.mt-5{'name' => "loginForm"} + %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'} + %input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'formData.email'} + %input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'} + + %div{"ng-if" => "!formData.status"} + %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span Sign In + %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} + %span Register + %br + .block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"} + %a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten. + + %em.block.center-align.mt-10{"ng-if" => "formData.status", "style" => "font-size: 14px;"} {{formData.status}} + + %div{"ng-if" => "showResetForm"} + %p{"style" => "font-size: 13px; text-align: center;"} + Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password. + For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password. + -# End if not user + + -# If user + %div{"ng-if" => "user"} + %label Local Encryption + %p Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes. + %label Status: + {{encryptionStatusForNotes()}} + -# End if user + + .mt-5{"ng-if" => "user"} + %label{"ng-if" => "user"} + %input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"} + Encrypted + %label + %input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"} + Decrypted + %a{"ng-click" => "downloadDataArchive()"} Download Data Archive + + %div{"ng-if" => "!importData.loading"} + %label#import-archive + %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} + %a.disabled + %span + Import Data from Archive + %div{"ng-if" => "importData.requestPassword"} + Enter the account password associated with the import file. + %input{"type" => "text", "ng-model" => "importData.password"} + %button{"ng-click" => "submitImportPassword()"} Decrypt & Import + + .spinner{"ng-if" => "importData.loading"} + + %a{"ng-click" => "destroyLocalData()"} Destroy all local data diff --git a/app/assets/templates/frontend/directives/account-menu/account-export-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-export-section.html.haml deleted file mode 100644 index b22b6d178..000000000 --- a/app/assets/templates/frontend/directives/account-menu/account-export-section.html.haml +++ /dev/null @@ -1,31 +0,0 @@ -%h3{"ng-click" => "showSection = !showSection"} - %a Import or export data - -%div{"ng-if" => "showSection"} - .options{"style" => "font-size: 12px; margin-top: 4px;"} - %label{"ng-if" => "user"} - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"} - Encrypted with Standard File key - %label - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"} - {{user ? 'Encrypted with custom key' : 'Encrypted' }} - %div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"} - %input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"} - %label - %input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"} - Decrypted - - %a{"ng-click" => "downloadDataArchive()"} Download Data Archive - - %div{"ng-if" => "!importData.loading"} - %label#import-archive - %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} - %a.disabled - %span - Import Data from Archive - %div{"ng-if" => "importData.requestPassword"} - Enter the account password associated with the import file. - %input{"type" => "text", "ng-model" => "importData.password"} - %button{"ng-click" => "submitImportPassword()"} Decrypt & Import - - .spinner{"ng-if" => "importData.loading"} diff --git a/app/assets/templates/frontend/directives/account-menu/account-keys-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-keys-section.html.haml deleted file mode 100644 index 7295cb611..000000000 --- a/app/assets/templates/frontend/directives/account-menu/account-keys-section.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%h3{"ng-click" => "showSection = !showSection"} - %a Manage keys - -%div{"ng-if" => "showSection"} - %h4 Encryption Keys - - %div{"ng-if" => "showSection"} - %p Keys are used to encrypt and decrypt your data. - .mt-10 - %section.white-bg{"ng-repeat" => "key in keys track by key.name"} - %label {{key.name}} - %p.wrap {{key.key}} - - %a.block.mt-10{"ng-click" => "newKeyData.showForm = !newKeyData.showForm"} Add New Key - %form{"ng-if" => "newKeyData.showForm"} - %input{"ng-model" => "newKeyData.name", "placeholder" => "Name your key"} - %input{"ng-model" => "newKeyData.key", "placeholder" => "Key"} - %button.light{"ng-click" => "submitNewKeyForm()"} Add Key diff --git a/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml deleted file mode 100644 index 6a9b3cbfa..000000000 --- a/app/assets/templates/frontend/directives/account-menu/account-new-account-section.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -%h3{"ng-click" => "showForm = !showForm"} - %a Add a sync account -%div{"ng-if" => "showForm"} - %p Enter your Standard File account information. - .small-v-space - - %form.account-form{'name' => "loginForm"} - %input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'} - %input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'formData.email'} - %input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'} - - %div{"ng-if" => "!formData.status"} - %button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span Sign In - %button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span Register - %br - .block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"} - %a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten. - - %div{"ng-if" => "!formData.status"} - %label.center-align.block.faded — OR — - %a.block.center-align.medium-text{"ng-if" => "!formData.showAddLinkForm", "ng-click" => "formData.showAddLinkForm = true"} Add sync using secret link - %form{"ng-if" => "formData.showAddLinkForm"} - %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.secretUrl'} - %button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"} - Add Sync Account - %a.block.center-align.mt-5{"ng-click" => "formData.showAddLinkForm = false"} Cancel - - %em.block.center-align.mt-10{"ng-if" => "formData.status", "style" => "font-size: 14px;"} {{formData.status}} - - %div{"ng-if" => "showResetForm"} - %p{"style" => "font-size: 13px; text-align: center;"} - Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password. - For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password. diff --git a/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml b/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml deleted file mode 100644 index 194e7e8eb..000000000 --- a/app/assets/templates/frontend/directives/account-menu/account-sync-section.html.haml +++ /dev/null @@ -1,30 +0,0 @@ -%h3{"ng-click" => "showSection = !showSection"} - %a Your sync accounts ({{syncProviders.length}}) - -%div{"ng-if" => "showSection || syncManager.syncProviders.length > 0"} - .small-v-space - %section.white-bg.medium-padding{"ng-repeat" => "provider in syncProviders"} - %label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}} - %em{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}} - %p {{provider.url}} - %section.inline-h - %div{"ng-if" => "!provider.keyName || provider.showKeyForm"} - %p - %strong Choose encryption key: - %select{"ng-model" => "provider.formData.keyName"} - %option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"} - {{key.name}} - %button{"ng-click" => "saveKey(provider)"} Set - - %button.light{"ng-if" => "!provider.enabled || !provider.primary", "ng-click" => "enableSyncProvider(provider, true)"} Set as Main - %button.light{"ng-if" => "syncProviders.length > 1 && !provider.secondary && (!provider.primary || !provider.enabled)", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary - - %button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key - %button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Account - - .mt-15{"ng-if" => "provider.error"} - %strong.red Error syncing: {{provider.error.message}} - .mt-15{"style" => "height: 15px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"} - .spinner{"style" => "float: left; margin-top: 3px; margin-left: 2px;"} - %strong{"style" => "float: left; margin-left: 7px;"} Syncing: -   {{provider.syncStatus.statusString}} diff --git a/app/assets/templates/frontend/header.html.haml b/app/assets/templates/frontend/header.html.haml index f1a72f35f..f385f293c 100644 --- a/app/assets/templates/frontend/header.html.haml +++ b/app/assets/templates/frontend/header.html.haml @@ -2,7 +2,7 @@ .pull-left .footer-bar-link %a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account - %account-data-menu{"ng-if" => "ctrl.showAccountMenu"} + %account-menu{"ng-if" => "ctrl.showAccountMenu"} .footer-bar-link %a{"ng-click" => "ctrl.toggleExtensions()"} Extensions diff --git a/app/assets/templates/services/.keep b/app/assets/templates/services/.keep deleted file mode 100644 index e69de29bb..000000000 From 2b0aa2d4ca8e6c1e2eabf36755e6620cf3318ac8 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sat, 28 Jan 2017 00:09:44 -0600 Subject: [PATCH 15/25] account menu complete --- .../app/frontend/controllers/header.js | 3 +- .../javascripts/app/services/authManager.js | 8 ++- .../services/directives/views/accountMenu.js | 31 +++++------ .../javascripts/app/services/syncManager.js | 24 ++++++--- app/assets/stylesheets/app/_header.scss | 44 ++++++++++++++- .../directives/account-menu.html.haml | 53 ++++++++++--------- 6 files changed, 110 insertions(+), 53 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/header.js b/app/assets/javascripts/app/frontend/controllers/header.js index c0e18fd99..624d3b391 100644 --- a/app/assets/javascripts/app/frontend/controllers/header.js +++ b/app/assets/javascripts/app/frontend/controllers/header.js @@ -32,11 +32,10 @@ angular.module('app.frontend') this.updateOfflineStatus(); this.findErrors = function() { - this.error = syncManager.error; + this.error = syncManager.syncStatus.error; } this.findErrors(); - this.accountMenuPressed = function() { this.serverData = {}; this.showAccountMenu = !this.showAccountMenu; diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index 8ccdbca6c..d2b0ea24c 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -92,7 +92,7 @@ angular.module('app.frontend') this.handleAuthResponse = function(response, email, url, authParams, mk) { localStorage.setItem("server", url); - localStorage.setItem("user", JSON.stringify(response.plain())); + localStorage.setItem("user", JSON.stringify(response.plain().user)); localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"]))); localStorage.setItem("mk", mk); localStorage.setItem("jwt", response.token); @@ -243,10 +243,8 @@ angular.module('app.frontend') items: items } - if(ek.name == SNKeyName) { - // auth params are only needed when encrypted with a standard file key - data["auth_params"] = this.getAuthParams(); - } + // auth params are only needed when encrypted with a standard file key + data["auth_params"] = this.getAuthParams(); return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */)); } diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index c04148bfd..a29a90db8 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -11,6 +11,9 @@ class AccountMenu { $scope.formData = {url: syncManager.serverURL}; $scope.user = authManager.user; + $scope.server = syncManager.serverURL; + + $scope.syncStatus = syncManager.syncStatus; $scope.changePasswordPressed = function() { $scope.showNewPasswordForm = !$scope.showNewPasswordForm; @@ -37,14 +40,14 @@ class AccountMenu { console.log("logging in with url", $scope.formData.url); $timeout(function(){ authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ - $scope.formData.status = null; if(!response || response.error) { + $scope.formData.status = null; var error = response ? response.error : {message: "An unknown error occured."} if(!response || (response && !response.didDisplayAlert)) { alert(error.message); } } else { - window.location.reload(); + $scope.onAuthSuccess(); } }); }) @@ -55,17 +58,23 @@ class AccountMenu { $timeout(function(){ authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){ - $scope.formData.status = null; if(!response || response.error) { + $scope.formData.status = null; var error = response ? response.error : {message: "An unknown error occured."} alert(error.message); } else { - window.location.reload(); + $scope.onAuthSuccess(); } }); }) } + $scope.onAuthSuccess = function() { + syncManager.markAllItemsDirtyAndSaveOffline(function(){ + window.location.reload(); + }) + } + $scope.destroyLocalData = function() { if(!confirm("Are you sure you want to end your session? This will delete all local items and extensions.")) { return; @@ -79,24 +88,16 @@ class AccountMenu { /* Import/Export */ - $scope.archiveFormData = {encryption_type: $scope.user ? 'mk' : 'ek'}; + $scope.archiveFormData = {encrypted: $scope.user ? true : false}; $scope.user = authManager.user; $scope.downloadDataArchive = function() { - if($scope.archiveFormData.encryption_type == 'ek') { - if(!$scope.archiveFormData.ek) { - alert("You must set an encryption key to export the data encrypted.") - return; - } - } - var link = document.createElement('a'); link.setAttribute('download', 'notes.json'); - var ek = $scope.archiveFormData.encryption_type == 'ek' ? $scope.archiveFormData.ek : null; - var encrypted = $scope.archiveFormData.encryption_type != 'none'; + var ek = $scope.archiveFormData.encrypted ? syncManager.masterKey : null; - link.href = authManager.itemsDataFile(encrypted, ek); + link.href = authManager.itemsDataFile(ek); link.click(); } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 90239e622..e2c8aa1ef 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -13,6 +13,10 @@ class SyncManager { return localStorage.getItem("server") || "http://localhost:3000"; } + get masterKey() { + return localStorage.getItem("mk"); + } + writeItemsToLocalStorage(items, offlineOnly, callback) { var params = items.map(function(item) { var itemParams = new ItemParams(item, null); @@ -49,13 +53,21 @@ class SyncManager { } } + markAllItemsDirtyAndSaveOffline(callback) { + var items = this.modelManager.allItems; + for(var item of items) { + item.setDirty(true); + } + this.writeItemsToLocalStorage(items, false, callback); + } + get syncURL() { return this.serverURL + "/items/sync"; } sync(callback, options = {}) { - if(this.syncOpInProgress) { + if(this.syncStatus.syncOpInProgress) { this.repeatOnCompletion = true; console.log("Sync op in progress; returning."); return; @@ -74,7 +86,7 @@ class SyncManager { var isContinuationSync = this.needsMoreSync; this.repeatOnCompletion = false; - this.syncOpInProgress = true; + this.syncStatus.syncOpInProgress = true; let submitLimit = 100; var subItems = allDirtyItems.slice(0, submitLimit); @@ -103,7 +115,7 @@ class SyncManager { request.post().then(function(response) { this.modelManager.clearDirtyItems(subItems); - this.error = null; + this.syncStatus.error = null; this.syncToken = response.sync_token; this.cursorToken = response.cursor_token; @@ -119,7 +131,7 @@ class SyncManager { this.writeItemsToLocalStorage(saved, false, null); this.writeItemsToLocalStorage(retrieved, false, null); - this.syncOpInProgress = false; + this.syncStatus.syncOpInProgress = false; this.syncStatus.current += subItems.length; if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) { @@ -135,8 +147,8 @@ class SyncManager { console.log("Sync error: ", response); var error = response.data ? response.data.error : {message: "Could not connect to server."}; - this.syncOpInProgress = false; - this.error = error; + this.syncStatus.syncOpInProgress = false; + this.syncStatus.error = error; this.writeItemsToLocalStorage(allDirtyItems, false, null); this.$rootScope.$broadcast("sync:error", error); diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index c0c912a7c..9f4bb069a 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -18,6 +18,10 @@ margin-top: 15px !important; } +.mt-25 { + margin-top: 25px !important; +} + .mb-10 { margin-bottom: 10px !important; } @@ -54,22 +58,54 @@ display: block; } +.medium-v-space { + height: 12px; + display: block; +} + +.large-v-space { + height: 24px; + display: block; +} + .medium-padding { padding: 10px !important; } .large-padding { - padding: 15px !important; + padding: 22px !important; } .red { color: red !important; } +.blue { + color: $blue-color; +} + .bold { font-weight: bold !important; } +.normal { + font-weight: normal !important; +} + +.inline { + display: inline-block; +} + +.fake-link { + font-weight: bold; + cursor: pointer; + color: $blue-color; + + &:hover { + text-decoration: underline; + } +} + .footer-bar { position: relative; width: 100%; @@ -114,6 +150,11 @@ display: block; } + h2 { + margin-bottom: 0px; + margin-top: 0px; + } + h3 { font-size: 14px !important; margin-top: 4px !important; @@ -122,6 +163,7 @@ h4 { margin-bottom: 0px !important; + font-size: 13px !important; } section { diff --git a/app/assets/templates/frontend/directives/account-menu.html.haml b/app/assets/templates/frontend/directives/account-menu.html.haml index 167b4117e..18df4f476 100644 --- a/app/assets/templates/frontend/directives/account-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-menu.html.haml @@ -1,7 +1,5 @@ .panel.panel-default.panel-right.account-data-menu - .panel-body - - -# If not user + .panel-body.large-padding %div{"ng-if" => "!user"} %p Enter your Standard File account information. You can also register for free using the default server address. .small-v-space @@ -26,31 +24,38 @@ %p{"style" => "font-size: 13px; text-align: center;"} Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password. For this reason, Standard Notes cannot offer a password reset option. You must make sure to store or remember your password. - -# End if not user - -# If user %div{"ng-if" => "user"} - %label Local Encryption + %h2 {{user.email}} + %p {{server}} + + %p.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress", "delay" => "1000"} Syncing: {{syncStatus.current}}/{{syncStatus.total}} + %p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}} + + .medium-v-space + + %h4 Local Encryption %p Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes. - %label Status: - {{encryptionStatusForNotes()}} - -# End if user + %div.mt-5 + %label Status: + {{encryptionStatusForNotes()}} - .mt-5{"ng-if" => "user"} - %label{"ng-if" => "user"} - %input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"} - Encrypted - %label - %input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"} - Decrypted - %a{"ng-click" => "downloadDataArchive()"} Download Data Archive + .mt-25{"ng-if" => "!importData.loading"} + %h4 Data Archives + .mt-5{"ng-if" => "user"} + %label.normal.inline{"ng-if" => "user"} + %input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"} + Encrypted + %label.normal.inline + %input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"} + Decrypted + + %a.block{"ng-click" => "downloadDataArchive()"} Download Data Archive + + %label.block.mt-5 + %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "ctrl.importFileSelected(files)"} + .fake-link Import Data from Archive - %div{"ng-if" => "!importData.loading"} - %label#import-archive - %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} - %a.disabled - %span - Import Data from Archive %div{"ng-if" => "importData.requestPassword"} Enter the account password associated with the import file. %input{"type" => "text", "ng-model" => "importData.password"} @@ -58,4 +63,4 @@ .spinner{"ng-if" => "importData.loading"} - %a{"ng-click" => "destroyLocalData()"} Destroy all local data + %a.block.mt-25.red{"ng-click" => "destroyLocalData()"} Destroy all local data From 92c8892054852cb2ef671024cc8398dc6b384630 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sat, 28 Jan 2017 10:28:06 -0600 Subject: [PATCH 16/25] extensions css --- .../app/frontend/models/app/extension.js | 2 +- .../javascripts/app/services/authManager.js | 6 - .../directives/views/globalExtensionsMenu.js | 2 - .../app/services/extensionManager.js | 35 +++-- app/assets/stylesheets/app/_directives.scss | 145 ------------------ app/assets/stylesheets/app/_header.scss | 12 ++ .../global-extensions-menu.html.haml | 89 ++++++----- 7 files changed, 74 insertions(+), 217 deletions(-) diff --git a/app/assets/javascripts/app/frontend/models/app/extension.js b/app/assets/javascripts/app/frontend/models/app/extension.js index 097f9212f..acd42b4e2 100644 --- a/app/assets/javascripts/app/frontend/models/app/extension.js +++ b/app/assets/javascripts/app/frontend/models/app/extension.js @@ -58,7 +58,7 @@ class Extension extends Item { actionsInGlobalContext() { return this.actions.filter(function(action){ - return action.context == "global" || action.sync_provider == true; + return action.context == "global"; }) } diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index d2b0ea24c..72a1dbe84 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -167,12 +167,6 @@ angular.module('app.frontend') }) } - - /* - Sync - */ - - /* Import */ diff --git a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js index 5834e4bc7..9945ac81c 100644 --- a/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js +++ b/app/assets/javascripts/app/services/directives/views/globalExtensionsMenu.js @@ -31,9 +31,7 @@ class GlobalExtensionsMenu { } $scope.selectedAction = function(action, extension) { - action.running = true; extensionManager.executeAction(action, extension, null, function(response){ - action.running = false; if(response && response.error) { action.error = true; alert("There was an error performing this action. Please try again."); diff --git a/app/assets/javascripts/app/services/extensionManager.js b/app/assets/javascripts/app/services/extensionManager.js index 9dadd8b34..ff44b6350 100644 --- a/app/assets/javascripts/app/services/extensionManager.js +++ b/app/assets/javascripts/app/services/extensionManager.js @@ -6,7 +6,6 @@ class ExtensionManager { this.authManager = authManager; this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || []; this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || []; - this.extensionEks = JSON.parse(localStorage.getItem("extensionEks")) || {}; this.syncManager = syncManager; modelManager.addItemSyncObserver("extensionManager", "Extension", function(items){ @@ -33,15 +32,6 @@ class ExtensionManager { }) } - ekForExtension(extension) { - return this.extensionEks[extension.url]; - } - - setEkForExtension(extension, ek) { - this.extensionEks[extension.url] = ek; - localStorage.setItem("extensionEks", JSON.stringify(this.extensionEks)); - } - actionWithURL(url) { for (var extension of this.extensions) { return _.find(extension.actions, {url: url}) @@ -146,23 +136,30 @@ class ExtensionManager { executeAction(action, extension, item, callback) { - //todo - if(this.extensionUsesEncryptedData(extension)) { + if(this.extensionUsesEncryptedData(extension) && this.authManager.offline()) { alert("To send data encrypted, you must have an encryption key, and must therefore be signed in."); callback(null); return; } + var customCallback = function(response) { + action.running = false; + callback(response); + } + + action.running = true; + switch (action.verb) { case "get": { this.Restangular.oneUrl(action.url, action.url).get().then(function(response){ action.error = false; var items = response.items; this.modelManager.mapResponseItemsToLocalModels(items); - callback(items); + customCallback(items); }.bind(this)) .catch(function(response){ action.error = true; + customCallback(null); }) break; @@ -171,7 +168,7 @@ class ExtensionManager { case "show": { var win = window.open(action.url, '_blank'); win.focus(); - callback(); + customCallback(); break; } @@ -190,7 +187,7 @@ class ExtensionManager { } this.performPost(action, extension, params, function(response){ - callback(response); + customCallback(response); }); break; @@ -274,14 +271,18 @@ class ExtensionManager { var params = this.outgoingParamsForItem(item, extension); return params; }.bind(this)) - this.performPost(action, extension, params, null); + + action.running = true; + this.performPost(action, extension, params, function(){ + action.running = false; + }); } else { // todo } } outgoingParamsForItem(item, extension) { - var itemParams = new itemParams(item, extension.ek); + var itemParams = new ItemParams(item, this.syncManager.masterKey); return itemParams.paramsForExtension(); } diff --git a/app/assets/stylesheets/app/_directives.scss b/app/assets/stylesheets/app/_directives.scss index e9a9625bd..39eb623dc 100644 --- a/app/assets/stylesheets/app/_directives.scss +++ b/app/assets/stylesheets/app/_directives.scss @@ -5,7 +5,6 @@ margin-top: 18px; } - .ext-header { background-color: #ededed; border-bottom: 1px solid #d3d3d3; @@ -69,147 +68,3 @@ } } } - - -/** -Extensions -*/ - -.extensions-panel { - font-size: 14px; - - .extension-link { - margin-top: 8px; - - a { - color: $blue-color !important; - font-weight: bold; - } - } -} - -.extension-form { - margin-top: 8px; -} - -.registered-extensions { - - .extension { - margin-bottom: 18px; - padding: 14px 6px; - padding-bottom: 8px; - color: black; - - .ek-input-wrapper { - text-align: center; - margin-top: 14px; - height: 30px; - - > input { - height: 100%; - border: 1px solid rgba(gray, 0.15); - padding: 5px; - width: 78%; - margin-right: 0; - } - - > button { - width: 20% !important; - height: 100% !important; - display: inline-block !important; - margin: 0 !important; - } - } - - a.option-link { - margin-top: 6px; - display: block; - text-align: center; - } - - .show-ek { - > .ek { - font-weight: bold; - margin-top: 2px; - } - - > .disclaimer { - margin-top: 2px; - font-size: 10px; - font-style: italic; - width: 60%; - margin-left: auto; - margin-right: auto; - } - } - - a { - color: $blue-color !important; - font-size: 12px !important; - font-weight: bold !important; - } - - .extension-name { - font-weight: bold; - font-size: 16px; - margin-bottom: 6px; - text-align: center; - } - - .encryption-format { - margin-top: 4px; - font-size: 12px; - text-align: center; - - > .title { - font-size: 13px; - // font-weight: bold; - margin-bottom: 2px; - } - } - - .extension-subtitle { - font-size: 14px; - margin-bottom: 10px; - } - - .extension-actions { - margin-top: 15px; - font-size: 12px; - - .action { - padding: 13px; - margin-bottom: 10px; - background-color: rgba(white, 0.9); - border: 1px solid rgba(gray, 0.15); - - .action-name { - font-weight: bold; - } - - .action-permissions { - margin-top: 2px; - a { - font-weight: normal !important; - } - } - - > .execute-type { - font-size: 12px; - margin-bottom: 1px; - } - - > .error { - color: red; - margin-top: 6px; - } - - > .last-run { - opacity: 0.5; - font-size: 11px; - margin-top: 6px; - } - } - } - } -} diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index 9f4bb069a..be5c8c8ce 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -22,6 +22,10 @@ margin-top: 25px !important; } +.mb-5 { + margin-bottom: 5px !important; +} + .mb-10 { margin-bottom: 10px !important; } @@ -72,6 +76,10 @@ padding: 10px !important; } +.pb-4 { + padding-bottom: 4px !important; +} + .large-padding { padding: 22px !important; } @@ -92,6 +100,10 @@ font-weight: normal !important; } +.small { + font-size: 10px !important; +} + .inline { display: inline-block; } diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index e1119a642..9f9f8d57a 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -1,58 +1,55 @@ -.panel.panel-default.account-panel.panel-right.extensions-panel +.panel.panel-default.account-panel.panel-right .panel-body %div{"style" => "font-size: 18px;", "ng-if" => "!extensionManager.extensions.length"} No extensions installed - .registered-extensions{"ng-if" => "extensionManager.extensions.length"} - .extension{"ng-repeat" => "extension in extensionManager.extensions", "ng-init" => "extension.formData = {}"} - .extension-name {{extension.name}} - .encryption-format - .title Send data: - %label + %div{"ng-if" => "extensionManager.extensions.length"} + %section.gray-bg.inline-h.mb-10.medium-padding{"ng-repeat" => "extension in extensionManager.extensions", "ng-init" => "extension.formData = {}"} + %h3.center-align {{extension.name}} + .center-align.centered.mt-10 + %label.block.normal Send data: + %label.normal %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "true", "ng-change" => "changeExtensionEncryptionFormat(true, extension)"} Encrypted - %label + %label.normal %input{"type" => "radio", "ng-model" => "extension.encrypted", "ng-value" => "false", "ng-change" => "changeExtensionEncryptionFormat(false, extension)"} Decrypted - .extension-actions - .action{"ng-repeat" => "action in extension.actionsInGlobalContext()"} - .action-name {{action.label}} - .action-desc{"style" => "font-style: italic;"} {{action.desc}} - .execute-type{"ng-if" => "action.repeat_mode == 'watch'"} - Repeats when a change is made to your items. - .execute-type{"ng-if" => "action.repeat_mode == 'loop'"} - Repeats at most once every {{action.repeat_timeout}} seconds - .action-permissions - %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} - %div{"ng-if" => "action.showPermissions"} - {{action.permissionsString}} - .encryption-type - %span {{action.encryptionModeString}} - .execute - %div{"ng-if" => "action.repeat_mode"} - %div{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension)"} Disable - %div{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable - %div{"ng-if" => "!action.repeat_mode", "ng-click" => "selectedAction(action, extension)"} - %div{"ng-if" => "!action.running"} - Perform Action - %div{"ng-if" => "action.running"} - .spinner.execution-spinner - .last-run{"ng-if" => "!action.error && action.lastExecuted && !action.running"} - Last run {{action.lastExecuted | appDateTime}} - .error{"ng-if" => "action.error"} - Error performing action. + .small-v-space - %a.option-link{"ng-click" => "deleteExtension(extension)"} Remove extension + %section.inline-h.white-bg.medium-padding.mb-10.pb-4{"ng-repeat" => "action in extension.actionsInGlobalContext()"} + %label.block {{action.label}} + %em{"style" => "font-style: italic;"} {{action.desc}} + %em{"ng-if" => "action.repeat_mode == 'watch'"} + Repeats when a change is made to your items. + %em{"ng-if" => "action.repeat_mode == 'loop'"} + Repeats at most once every {{action.repeat_timeout}} seconds + %div + %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} + %div{"ng-if" => "action.showPermissions"} + {{action.permissionsString}} + %label.block.normal {{action.encryptionModeString}} - .extension-link - %a{"ng-click" => "toggleExtensionForm()"} Add new extension + %div + .mt-5{"ng-if" => "action.repeat_mode"} + %button.light{"ng-if" => "extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.disableRepeatAction(action, extension)"} Disable + %button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable + %button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension)"} + Perform Action + .spinner.execution-spinner.mb-5.centered.center-align.block{"ng-if" => "action.running"} + %p.mb-5.mt-5.small{"ng-if" => "!action.error && action.lastExecuted && !action.running"} + Last run {{action.lastExecuted | appDateTime}} + %label.red{"ng-if" => "action.error"} + Error performing action. - %form.extension-form{"ng-if" => "showNewExtensionForm"} - .form-tag.has-feedback - %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'newExtensionData.url'} + %a.block.center-align.mt-10{"ng-click" => "deleteExtension(extension)"} Remove extension + + .large-v-space + + %a.block{"ng-click" => "toggleExtensionForm()"} Add new extension + + %form.mt-10.mb-10{"ng-if" => "showNewExtensionForm"} + %input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Extension URL', :required => true, :type => 'url', 'ng-model' => 'newExtensionData.url'} %button.btn.dark-button.btn-block{"ng-click" => "submitNewExtensionForm()", :type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"} - %span.ladda-label Add Extension + Add Extension - .extension-link - %a{"ng-click" => "reloadExtensionsPressed()", "ng-if" => "extensionManager.extensions.length > 0"} Reload all extensions - .extension-link - %a{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions + %a.block.mt-5{"ng-click" => "reloadExtensionsPressed()", "ng-if" => "extensionManager.extensions.length > 0"} Reload all extensions + %a.block.mt-5{"href" => "https://standardnotes.org/extensions", "target" => "_blank"} List of available extensions From 5d9c1c341312f938e8a0cf7e77625729348c712f Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sat, 28 Jan 2017 13:58:02 -0600 Subject: [PATCH 17/25] notes infinite scroll --- .../app/frontend/controllers/notes.js | 5 ++ .../javascripts/app/services/authManager.js | 76 ---------------- .../directives/functional/infiniteScroll.js | 19 ++++ .../services/directives/views/accountMenu.js | 88 +++++++++++++++++-- .../app/services/filters/startFrom.js | 6 ++ .../javascripts/app/services/modelManager.js | 2 +- .../javascripts/app/services/syncManager.js | 27 +++++- app/assets/stylesheets/app/_header.scss | 82 ++++++----------- .../directives/account-menu.html.haml | 8 +- .../global-extensions-menu.html.haml | 6 +- app/assets/templates/frontend/notes.html.haml | 15 ++-- 11 files changed, 180 insertions(+), 154 deletions(-) create mode 100644 app/assets/javascripts/app/services/directives/functional/infiniteScroll.js create mode 100644 app/assets/javascripts/app/services/filters/startFrom.js diff --git a/app/assets/javascripts/app/frontend/controllers/notes.js b/app/assets/javascripts/app/frontend/controllers/notes.js index a9e4c37f2..5bb7fba5a 100644 --- a/app/assets/javascripts/app/frontend/controllers/notes.js +++ b/app/assets/javascripts/app/frontend/controllers/notes.js @@ -32,6 +32,11 @@ angular.module('app.frontend') var isFirstLoad = true; + this.notesToDisplay = 20; + this.paginate = function() { + this.notesToDisplay += 20 + } + this.tagDidChange = function(tag, oldTag) { this.showMenu = false; diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index 72a1dbe84..f874b1a7d 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -167,82 +167,6 @@ angular.module('app.frontend') }) } - /* - Import - */ - - this.importJSONData = function(data, password, callback) { - console.log("Importing data", data); - - var onDataReady = function() { - var items = modelManager.mapResponseItemsToLocalModels(data.items); - items.forEach(function(item){ - item.setDirty(true); - item.markAllReferencesDirty(); - }) - this.syncWithOptions(callback, {additionalFields: ["created_at", "updated_at"]}); - }.bind(this) - - if(data.auth_params) { - Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){ - var mk = keys.mk; - try { - this.decryptItemsWithKey(data.items, mk); - // delete items enc_item_key since the user's actually key will do the encrypting once its passed off - data.items.forEach(function(item){ - item.enc_item_key = null; - item.auth_hash = null; - }) - onDataReady(); - } - catch (e) { - console.log("Error decrypting", e); - alert("There was an error decrypting your items. Make sure the password you entered is correct and try again."); - callback(false, null); - return; - } - }.bind(this)); - } else { - onDataReady(); - } - } - - /* - Export - */ - - this.itemsDataFile = function(ek) { - var textFile = null; - var makeTextFile = function (text) { - var data = new Blob([text], {type: 'text/json'}); - - // If we are replacing a previously generated file we need to - // manually revoke the object URL to avoid memory leaks. - if (textFile !== null) { - window.URL.revokeObjectURL(textFile); - } - - textFile = window.URL.createObjectURL(data); - - // returns a URL you can use as a href - return textFile; - }.bind(this); - - var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){ - var itemParams = new ItemParams(item, ek); - return itemParams.paramsForExportFile(); - }.bind(this)); - - var data = { - items: items - } - - // auth params are only needed when encrypted with a standard file key - data["auth_params"] = this.getAuthParams(); - - return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */)); - } - this.staticifyObject = function(object) { return JSON.parse(JSON.stringify(object)); } diff --git a/app/assets/javascripts/app/services/directives/functional/infiniteScroll.js b/app/assets/javascripts/app/services/directives/functional/infiniteScroll.js new file mode 100644 index 000000000..a9f565db3 --- /dev/null +++ b/app/assets/javascripts/app/services/directives/functional/infiniteScroll.js @@ -0,0 +1,19 @@ +angular.module('app.frontend').directive('infiniteScroll', [ +'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) { + return { + link: function(scope, elem, attrs) { + elem.css('overflow-x', 'hidden'); + elem.css('height', 'inherit'); + + var offset = parseInt(attrs.threshold) || 0; + var e = elem[0] + + elem.on('scroll', function(){ + if(scope.$eval(attrs.canLoad) && e.scrollTop + e.offsetHeight >= e.scrollHeight - offset) { + scope.$apply(attrs.infiniteScroll); + } + }); + } + }; +} +]); diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index a29a90db8..dfc1a6457 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -85,7 +85,6 @@ class AccountMenu { }) } - /* Import/Export */ $scope.archiveFormData = {encrypted: $scope.user ? true : false}; @@ -97,16 +96,20 @@ class AccountMenu { var ek = $scope.archiveFormData.encrypted ? syncManager.masterKey : null; - link.href = authManager.itemsDataFile(ek); + link.href = $scope.itemsDataFile(ek); link.click(); } + $scope.submitImportPassword = function() { + $scope.performImport($scope.importData.data, $scope.importData.password); + } + $scope.performImport = function(data, password) { $scope.importData.loading = true; // allow loading indicator to come up with timeout $timeout(function(){ - authManager.importJSONData(data, password, function(success, response){ - console.log("Import response:", success, response); + $scope.importJSONData(data, password, function(success, response){ + // console.log("Import response:", success, response); $scope.importData.loading = false; if(success) { $scope.importData = null; @@ -117,10 +120,6 @@ class AccountMenu { }) } - $scope.submitImportPassword = function() { - $scope.performImport($scope.importData.data, $scope.importData.password); - } - $scope.importFileSelected = function(files) { $scope.importData = {}; @@ -147,6 +146,79 @@ class AccountMenu { return allNotes.length + "/" + allNotes.length + " notes encrypted"; } + $scope.importJSONData = function(data, password, callback) { + console.log("Importing data", data); + + var onDataReady = function() { + var items = modelManager.mapResponseItemsToLocalModels(data.items); + items.forEach(function(item){ + item.setDirty(true); + item.markAllReferencesDirty(); + }) + + syncManager.sync(callback, {additionalFields: ["created_at", "updated_at"]}); + }.bind(this) + + if(data.auth_params) { + Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){ + var mk = keys.mk; + try { + EncryptionHelper.decryptMultipleItems(data.items, mk); + // delete items enc_item_key since the user's actually key will do the encrypting once its passed off + data.items.forEach(function(item){ + item.enc_item_key = null; + item.auth_hash = null; + }) + onDataReady(); + } + catch (e) { + console.log("Error decrypting", e); + alert("There was an error decrypting your items. Make sure the password you entered is correct and try again."); + callback(false, null); + return; + } + }.bind(this)); + } else { + onDataReady(); + } + } + + /* + Export + */ + + $scope.itemsDataFile = function(ek) { + var textFile = null; + var makeTextFile = function (text) { + var data = new Blob([text], {type: 'text/json'}); + + // If we are replacing a previously generated file we need to + // manually revoke the object URL to avoid memory leaks. + if (textFile !== null) { + window.URL.revokeObjectURL(textFile); + } + + textFile = window.URL.createObjectURL(data); + + // returns a URL you can use as a href + return textFile; + }.bind(this); + + var items = _.map(modelManager.allItemsMatchingTypes(["Tag", "Note"]), function(item){ + var itemParams = new ItemParams(item, ek); + return itemParams.paramsForExportFile(); + }.bind(this)); + + var data = { + items: items + } + + // auth params are only needed when encrypted with a standard file key + data["auth_params"] = authManager.getAuthParams(); + + return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */)); + } + } } diff --git a/app/assets/javascripts/app/services/filters/startFrom.js b/app/assets/javascripts/app/services/filters/startFrom.js new file mode 100644 index 000000000..2ebd72e42 --- /dev/null +++ b/app/assets/javascripts/app/services/filters/startFrom.js @@ -0,0 +1,6 @@ +// Start from filter +angular.module('app.frontend').filter('startFrom', function() { + return function(input, start) { + return input.slice(start); + }; +}); diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index 9b8d62e70..b919c2712 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -76,7 +76,7 @@ class ModelManager { this.notifySyncObserversOfModels(models); - this.sortItems(); + // this.sortItems(); return models; } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index e2c8aa1ef..07cff13ec 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -65,6 +65,19 @@ class SyncManager { return this.serverURL + "/items/sync"; } + set syncToken(token) { + console.log("setting token", token); + this._syncToken = token; + localStorage.setItem("syncToken", token); + } + + get syncToken() { + if(!this._syncToken) { + this._syncToken = localStorage.getItem("syncToken"); + } + return this._syncToken; + } + sync(callback, options = {}) { if(this.syncStatus.syncOpInProgress) { @@ -75,6 +88,8 @@ class SyncManager { var allDirtyItems = this.modelManager.getDirtyItems(); + console.log("Syncing dirty items", allDirtyItems); + // we want to write all dirty items to disk only if the user is offline, or if the sync op fails // if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server if(this.authManager.offline()) { @@ -113,10 +128,13 @@ class SyncManager { request.sync_token = this.syncToken; request.cursor_token = this.cursorToken; + console.log("Syncing with token", request.sync_token, request.cursor_token); + request.post().then(function(response) { + console.log("Sync completion", response.plain()); + this.modelManager.clearDirtyItems(subItems); this.syncStatus.error = null; - this.syncToken = response.sync_token; this.cursorToken = response.cursor_token; this.$rootScope.$broadcast("sync:updated_token", this.syncToken); @@ -134,8 +152,13 @@ class SyncManager { this.syncStatus.syncOpInProgress = false; this.syncStatus.current += subItems.length; + // set the sync token at the end, so that if any errors happen above, you can resync + this.syncToken = response.sync_token; + if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) { - this.sync(callback, options); + setTimeout(function () { + this.sync(callback, options); + }.bind(this), 10); // wait 10ms to allow UI to update } else { if(callback) { callback(response); diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index be5c8c8ce..cee146988 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -30,6 +30,10 @@ margin-bottom: 10px !important; } +.mr-5 { + margin-right: 5px; +} + .faded { opacity: 0.5; } @@ -230,44 +234,26 @@ cursor: default; overflow: auto; background-color: white; +} - button.light { - font-weight: bold; - margin-bottom: 0px; - font-size: 12px; - height: 30px; - padding-top: 3px; - text-align: center; - margin-bottom: 6px; - background-color: white; - display: block; - width: 100%; - border: 1px solid rgba(gray, 0.15); - cursor: pointer; - color: $blue-color; +button.light { + font-weight: bold; + margin-bottom: 0px; + font-size: 12px; + height: 30px; + padding-top: 3px; + text-align: center; + margin-bottom: 6px; + background-color: white; + display: block; + width: 100%; + border: 1px solid rgba(gray, 0.15); + cursor: pointer; + color: $blue-color; - &:hover { - background-color: rgba(gray, 0.10); - } - - .execution-spinner { - margin-left: auto; - margin-right: auto; - text-align: center; - margin-top: 3px; - } - } - - .storage-text { - font-size: 14px; - } - - .checkbox { - font-size: 14px; - font-weight: normal; - margin-left: auto; - margin-right: auto; - } + &:hover { + background-color: rgba(gray, 0.10); + } } .half-button { @@ -293,25 +279,6 @@ cursor: default !important; } -.import-password { - margin-top: 14px; - - > .field { - display: block; - margin: 5px 0px; - } -} - -.encryption-confirmation { - position: relative; - .buttons { - .cancel { - font-weight: normal; - margin-right: 3px; - } - } -} - a.disabled { pointer-events: none; } @@ -323,6 +290,11 @@ a.disabled { border: 1px solid #515263; border-right-color: transparent; border-radius: 50%; + + &.blue { + border: 1px solid $blue-color; + border-right-color: transparent; + } } @keyframes rotate { diff --git a/app/assets/templates/frontend/directives/account-menu.html.haml b/app/assets/templates/frontend/directives/account-menu.html.haml index 18df4f476..726ab702d 100644 --- a/app/assets/templates/frontend/directives/account-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-menu.html.haml @@ -29,7 +29,9 @@ %h2 {{user.email}} %p {{server}} - %p.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress", "delay" => "1000"} Syncing: {{syncStatus.current}}/{{syncStatus.total}} + %div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress", "delay" => "1000"} + .spinner.inline.mr-5.blue + Syncing: {{syncStatus.current}}/{{syncStatus.total}} %p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}} .medium-v-space @@ -53,7 +55,7 @@ %a.block{"ng-click" => "downloadDataArchive()"} Download Data Archive %label.block.mt-5 - %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "ctrl.importFileSelected(files)"} + %input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} .fake-link Import Data from Archive %div{"ng-if" => "importData.requestPassword"} @@ -61,6 +63,6 @@ %input{"type" => "text", "ng-model" => "importData.password"} %button{"ng-click" => "submitImportPassword()"} Decrypt & Import - .spinner{"ng-if" => "importData.loading"} + .spinner.mt-10{"ng-if" => "importData.loading"} %a.block.mt-25.red{"ng-click" => "destroyLocalData()"} Destroy all local data diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index 9f9f8d57a..2c33eac60 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -34,13 +34,15 @@ %button.light{"ng-if" => "!extensionManager.isRepeatActionEnabled(action)", "ng-click" => "extensionManager.enableRepeatAction(action, extension)"} Enable %button.light.mt-10{"ng-if" => "!action.running && !action.repeat_mode", "ng-click" => "selectedAction(action, extension)"} Perform Action - .spinner.execution-spinner.mb-5.centered.center-align.block{"ng-if" => "action.running"} + .spinner.mb-5.centered.center-align.block{"ng-if" => "action.running"} %p.mb-5.mt-5.small{"ng-if" => "!action.error && action.lastExecuted && !action.running"} Last run {{action.lastExecuted | appDateTime}} %label.red{"ng-if" => "action.error"} Error performing action. - %a.block.center-align.mt-10{"ng-click" => "deleteExtension(extension)"} Remove extension + %a.block.center-align.mt-10{"ng-click" => "extension.showURL = !extension.showURL"} Show URL + %p.center-align.wrap{"ng-if" => "extension.showURL"} {{extension.url}} + %a.block.center-align.mt-5{"ng-click" => "deleteExtension(extension)"} Remove extension .large-v-space diff --git a/app/assets/templates/frontend/notes.html.haml b/app/assets/templates/frontend/notes.html.haml index 0d5b71103..a185c4b90 100644 --- a/app/assets/templates/frontend/notes.html.haml +++ b/app/assets/templates/frontend/notes.html.haml @@ -18,10 +18,11 @@ %li %a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedTagDelete()"} Delete Tag - .note{"ng-repeat" => "note in ctrl.tag.notes | filter: ctrl.filterNotes", - "ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"} - .name{"ng-if" => "note.title"} - {{note.title}} - .note-preview - {{note.text}} - .date {{(note.created_at | appDateTime) || 'Now'}} + %div{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"} + .note{"ng-repeat" => "note in ctrl.tag.notes | limitTo:ctrl.notesToDisplay | filter: ctrl.filterNotes", + "ng-click" => "ctrl.selectNote(note)"} + .name{"ng-if" => "note.title"} + {{note.title}} + .note-preview + {{note.text}} + .date {{(note.created_at | appDateTime) || 'Now'}} From 46e9e91f463a2ff56632e8467121a565e9d250c9 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sat, 28 Jan 2017 22:44:40 -0600 Subject: [PATCH 18/25] sync status, show credentials --- .../javascripts/app/services/authManager.js | 7 +++--- .../services/directives/views/accountMenu.js | 12 +++++++-- .../javascripts/app/services/syncManager.js | 25 ++++++++++++++++--- app/assets/stylesheets/app/_header.scss | 14 ++++++++++- .../directives/account-menu.html.haml | 10 +++++++- 5 files changed, 58 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index f874b1a7d..f0976e1ee 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -79,7 +79,7 @@ angular.module('app.frontend') var params = {password: keys.pw, email: email}; _.merge(request, params); request.post().then(function(response){ - this.handleAuthResponse(response, email, url, authParams, mk); + this.handleAuthResponse(response, email, url, authParams, mk, keys.pw); callback(response); }.bind(this)) .catch(function(response){ @@ -90,11 +90,12 @@ angular.module('app.frontend') }.bind(this)) } - this.handleAuthResponse = function(response, email, url, authParams, mk) { + this.handleAuthResponse = function(response, email, url, authParams, mk, pw) { 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); + localStorage.setItem("pw", pw); localStorage.setItem("jwt", response.token); } @@ -106,7 +107,7 @@ angular.module('app.frontend') var params = _.merge({password: keys.pw, email: email}, authParams); _.merge(request, params); request.post().then(function(response){ - this.handleAuthResponse(response, email, url, authParams, mk); + this.handleAuthResponse(response, email, url, authParams, mk, keys.pw); callback(response); }.bind(this)) .catch(function(response){ diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index dfc1a6457..34b18dc4f 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -19,6 +19,14 @@ class AccountMenu { $scope.showNewPasswordForm = !$scope.showNewPasswordForm; } + $scope.encryptionKey = function() { + return syncManager.masterKey; + } + + $scope.serverPassword = function() { + return syncManager.serverPassword; + } + $scope.submitPasswordChange = function() { $scope.passwordChangeData.status = "Generating New Keys..."; @@ -108,10 +116,10 @@ class AccountMenu { $scope.importData.loading = true; // allow loading indicator to come up with timeout $timeout(function(){ - $scope.importJSONData(data, password, function(success, response){ + $scope.importJSONData(data, password, function(response){ // console.log("Import response:", success, response); $scope.importData.loading = false; - if(success) { + if(response) { $scope.importData = null; } else { alert("There was an error importing your data. Please try again."); diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 07cff13ec..035d69247 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -17,6 +17,10 @@ class SyncManager { return localStorage.getItem("mk"); } + get serverPassword() { + return localStorage.getItem("pw"); + } + writeItemsToLocalStorage(items, offlineOnly, callback) { var params = items.map(function(item) { var itemParams = new ItemParams(item, null); @@ -49,7 +53,7 @@ class SyncManager { }.bind(this)) if(callback) { - callback(); + callback({success: true}); } } @@ -66,7 +70,6 @@ class SyncManager { } set syncToken(token) { - console.log("setting token", token); this._syncToken = token; localStorage.setItem("syncToken", token); } @@ -78,6 +81,22 @@ class SyncManager { return this._syncToken; } + set cursorToken(token) { + this._cursorToken = token; + if(token) { + localStorage.setItem("cursorToken", token); + } else { + localStorage.removeItem("cursorToken"); + } + } + + get cursorToken() { + if(!this._cursorToken) { + this._cursorToken = localStorage.getItem("cursorToken"); + } + return this._cursorToken; + } + sync(callback, options = {}) { if(this.syncStatus.syncOpInProgress) { @@ -135,7 +154,6 @@ class SyncManager { this.modelManager.clearDirtyItems(subItems); this.syncStatus.error = null; - this.cursorToken = response.cursor_token; this.$rootScope.$broadcast("sync:updated_token", this.syncToken); @@ -154,6 +172,7 @@ class SyncManager { // set the sync token at the end, so that if any errors happen above, you can resync this.syncToken = response.sync_token; + this.cursorToken = response.cursor_token; if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) { setTimeout(function () { diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index cee146988..cdd9582b3 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -76,6 +76,10 @@ display: block; } +.small-padding { + padding: 5px !important; +} + .medium-padding { padding: 10px !important; } @@ -84,6 +88,14 @@ padding-bottom: 4px !important; } +.pb-6 { + padding-bottom: 6px !important; +} + +.pb-10 { + padding-bottom: 10px !important; +} + .large-padding { padding: 22px !important; } @@ -178,7 +190,7 @@ } h4 { - margin-bottom: 0px !important; + margin-bottom: 4px !important; font-size: 13px !important; } diff --git a/app/assets/templates/frontend/directives/account-menu.html.haml b/app/assets/templates/frontend/directives/account-menu.html.haml index 726ab702d..029a84f12 100644 --- a/app/assets/templates/frontend/directives/account-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-menu.html.haml @@ -28,10 +28,18 @@ %div{"ng-if" => "user"} %h2 {{user.email}} %p {{server}} + %a.block.mt-5{"ng-click" => "showCredentials = !showCredentials"} Show Credentials + %section.gray-bg.mt-10.medium-padding{"ng-if" => "showCredentials"} + %label.block + Encryption key: + %span.wrap.normal {{encryptionKey()}} + %label.block.mt-5 Server password: + %span.wrap {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}} %div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress", "delay" => "1000"} .spinner.inline.mr-5.blue - Syncing: {{syncStatus.current}}/{{syncStatus.total}} + Syncing + %span{"ng-if" => "syncStatus.total > 0"}: {{syncStatus.current}}/{{syncStatus.total}} %p.bold.mt-10.red.block{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}} .medium-v-space From aa6388d8b05864d8187b1449a16a4c32cbcac3b5 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sat, 28 Jan 2017 23:16:22 -0600 Subject: [PATCH 19/25] handle local deletion relationships --- .../javascripts/app/frontend/models/api/item.js | 4 ++++ .../javascripts/app/frontend/models/app/note.js | 7 +++++++ .../javascripts/app/frontend/models/app/tag.js | 7 +++++++ .../javascripts/app/services/modelManager.js | 16 ++++++---------- .../javascripts/app/services/syncManager.js | 6 ------ app/assets/templates/frontend/notes.html.haml | 2 +- 6 files changed, 25 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/app/frontend/models/api/item.js b/app/assets/javascripts/app/frontend/models/api/item.js index b87e66c8d..0409580eb 100644 --- a/app/assets/javascripts/app/frontend/models/api/item.js +++ b/app/assets/javascripts/app/frontend/models/api/item.js @@ -107,6 +107,10 @@ class Item { // must override } + isBeingRemovedLocally() { + + } + removeAllRelationships() { // must override this.setDirty(true); diff --git a/app/assets/javascripts/app/frontend/models/app/note.js b/app/assets/javascripts/app/frontend/models/app/note.js index 31d27cf80..b7b4a6c06 100644 --- a/app/assets/javascripts/app/frontend/models/app/note.js +++ b/app/assets/javascripts/app/frontend/models/app/note.js @@ -56,6 +56,13 @@ class Note extends Item { this.tags = []; } + isBeingRemovedLocally() { + this.tags.forEach(function(tag){ + _.pull(tag.notes, this); + }.bind(this)) + super.isBeingRemovedLocally(); + } + static filterDummyNotes(notes) { var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null}); return filtered; diff --git a/app/assets/javascripts/app/frontend/models/app/tag.js b/app/assets/javascripts/app/frontend/models/app/tag.js index f94c2eba4..f8361ca5a 100644 --- a/app/assets/javascripts/app/frontend/models/app/tag.js +++ b/app/assets/javascripts/app/frontend/models/app/tag.js @@ -55,6 +55,13 @@ class Tag extends Item { this.notes = []; } + isBeingRemovedLocally() { + this.notes.forEach(function(note){ + _.pull(note.tags, this); + }.bind(this)) + super.isBeingRemovedLocally(); + } + get content_type() { return "Tag"; } diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index b919c2712..e07cd0893 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -76,7 +76,6 @@ class ModelManager { this.notifySyncObserversOfModels(models); - // this.sortItems(); return models; } @@ -133,7 +132,9 @@ class ModelManager { } } else if(item.content_type == "Note") { if(!_.find(this.notes, {uuid: item.uuid})) { - this.notes.unshift(item); + this.notes.splice(_.sortedLastIndexBy(this.notes, item, function(item){ + return -item.created_at; + }), 0, item); } } else if(item.content_type == "Extension") { if(!_.find(this._extensions, {uuid: item.uuid})) { @@ -170,14 +171,6 @@ class ModelManager { } } - sortItems() { - Item.sortItemsByDate(this.notes); - - this.tags.forEach(function(tag){ - Item.sortItemsByDate(tag.notes); - }) - } - addItemSyncObserver(id, type, callback) { this.itemSyncObservers.push({id: id, type: type, callback: callback}); } @@ -223,10 +216,13 @@ class ModelManager { removeItemLocally(item) { _.pull(this.items, item); + item.isBeingRemovedLocally(); + if(item.content_type == "Tag") { _.pull(this.tags, item); } else if(item.content_type == "Note") { _.pull(this.notes, item); + } else if(item.content_type == "Extension") { _.pull(this._extensions, item); } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index 035d69247..b774cf615 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -107,8 +107,6 @@ class SyncManager { var allDirtyItems = this.modelManager.getDirtyItems(); - console.log("Syncing dirty items", allDirtyItems); - // we want to write all dirty items to disk only if the user is offline, or if the sync op fails // if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server if(this.authManager.offline()) { @@ -147,11 +145,7 @@ class SyncManager { request.sync_token = this.syncToken; request.cursor_token = this.cursorToken; - console.log("Syncing with token", request.sync_token, request.cursor_token); - request.post().then(function(response) { - console.log("Sync completion", response.plain()); - this.modelManager.clearDirtyItems(subItems); this.syncStatus.error = null; diff --git a/app/assets/templates/frontend/notes.html.haml b/app/assets/templates/frontend/notes.html.haml index a185c4b90..28fb4654b 100644 --- a/app/assets/templates/frontend/notes.html.haml +++ b/app/assets/templates/frontend/notes.html.haml @@ -20,7 +20,7 @@ %div{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"} .note{"ng-repeat" => "note in ctrl.tag.notes | limitTo:ctrl.notesToDisplay | filter: ctrl.filterNotes", - "ng-click" => "ctrl.selectNote(note)"} + "ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"} .name{"ng-if" => "note.title"} {{note.title}} .note-preview From f132da004d27e6d89eecbad3956d52548d7d41b9 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sat, 28 Jan 2017 23:34:06 -0600 Subject: [PATCH 20/25] offline callback --- app/assets/javascripts/app/frontend/controllers/home.js | 5 +---- app/assets/javascripts/app/services/syncManager.js | 7 ++++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index aaa854a66..b2b7cbb22 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -71,10 +71,7 @@ angular.module('app.frontend') // if no more notes, delete tag syncManager.sync(function(){ // force scope tags to update on sub directives - $scope.tags = []; - $timeout(function(){ - $scope.tags = modelManager.tags; - }) + $scope.safeApply(); }); } else { alert("To delete this tag, remove all its notes first."); diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index b774cf615..ca5cd19f1 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -50,11 +50,12 @@ class SyncManager { this.modelManager.removeItemLocally(item); } } + + if(callback) { + callback({success: true}); + } }.bind(this)) - if(callback) { - callback({success: true}); - } } markAllItemsDirtyAndSaveOffline(callback) { From 79f8430d56e0d9d759e3fe69e76f6d30a33cf07c Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sat, 28 Jan 2017 23:43:59 -0600 Subject: [PATCH 21/25] style --- .../app/services/helpers/encryptionHelper.js | 1 - app/assets/stylesheets/app/_header.scss | 12 ++++++++++++ .../frontend/directives/account-menu.html.haml | 7 ++++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/app/services/helpers/encryptionHelper.js b/app/assets/javascripts/app/services/helpers/encryptionHelper.js index 076786f6d..49f9fba77 100644 --- a/app/assets/javascripts/app/services/helpers/encryptionHelper.js +++ b/app/assets/javascripts/app/services/helpers/encryptionHelper.js @@ -17,7 +17,6 @@ class EncryptionHelper { item.content = encryptedContent; item.auth_hash = authHash; - item.local_encryption_scheme = "1.0"; } static decryptItem(item, key) { diff --git a/app/assets/stylesheets/app/_header.scss b/app/assets/stylesheets/app/_header.scss index cdd9582b3..74efb241a 100644 --- a/app/assets/stylesheets/app/_header.scss +++ b/app/assets/stylesheets/app/_header.scss @@ -6,6 +6,14 @@ float: right !important; } +.mt-1 { + margin-top: 1px !important; +} + +.mt-2 { + margin-top: 2px !important; +} + .mt-5 { margin-top: 5px !important; } @@ -22,6 +30,10 @@ margin-top: 25px !important; } +.mb-0 { + margin-bottom: 0px !important; +} + .mb-5 { margin-bottom: 5px !important; } diff --git a/app/assets/templates/frontend/directives/account-menu.html.haml b/app/assets/templates/frontend/directives/account-menu.html.haml index 029a84f12..168b1ebf4 100644 --- a/app/assets/templates/frontend/directives/account-menu.html.haml +++ b/app/assets/templates/frontend/directives/account-menu.html.haml @@ -32,9 +32,10 @@ %section.gray-bg.mt-10.medium-padding{"ng-if" => "showCredentials"} %label.block Encryption key: - %span.wrap.normal {{encryptionKey()}} - %label.block.mt-5 Server password: - %span.wrap {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}} + .wrap.normal.mt-1 {{encryptionKey()}} + %label.block.mt-5.mb-0 + Server password: + .wrap.normal.mt-1 {{serverPassword() ? serverPassword() : 'Not available. Sign out then sign back in to compute.'}} %div.bold.mt-10.blue{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress", "delay" => "1000"} .spinner.inline.mr-5.blue From 5ec6fc4831022477028a719e49673dfdd64c3842 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 29 Jan 2017 00:04:56 -0600 Subject: [PATCH 22/25] delete undefined tag --- .../app/frontend/controllers/home.js | 5 +++++ .../app/frontend/controllers/tags.js | 19 ++++++++++--------- app/assets/templates/frontend/tags.html.haml | 2 +- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index b2b7cbb22..bee9f6c50 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -56,6 +56,11 @@ angular.module('app.frontend') } $scope.tagsSave = function(tag, callback) { + console.log("saving tag", tag); + if(!tag.title || tag.title.length == 0) { + $scope.notesRemoveTag(tag); + return; + } tag.setDirty(true); syncManager.sync(callback); } diff --git a/app/assets/javascripts/app/frontend/controllers/tags.js b/app/assets/javascripts/app/frontend/controllers/tags.js index 5f673c2da..422c308f2 100644 --- a/app/assets/javascripts/app/frontend/controllers/tags.js +++ b/app/assets/javascripts/app/frontend/controllers/tags.js @@ -79,19 +79,20 @@ angular.module('app.frontend') this.saveTag = function($event, tag) { this.editingTag = null; - if(tag.title.length == 0) { - tag.title = originalTagName; - originalTagName = ""; + $event.target.blur(); + + if(!tag.title || tag.title.length == 0) { + if(originalTagName) { + tag.title = originalTagName; + originalTagName = null; + } else { + // newly created tag without content + modelManager.removeItemLocally(tag); + } return; } - $event.target.blur(); - if(!tag.title || tag.title.length == 0) { - return; - } - this.save()(tag, function(savedTag){ - // _.merge(tag, savedTag); this.selectTag(tag); this.newTag = null; }.bind(this)); diff --git a/app/assets/templates/frontend/tags.html.haml b/app/assets/templates/frontend/tags.html.haml index 69e96b946..abdc92dc5 100644 --- a/app/assets/templates/frontend/tags.html.haml +++ b/app/assets/templates/frontend/tags.html.haml @@ -13,5 +13,5 @@ %input.title{"ng-disabled" => "tag != ctrl.selectedTag", "ng-model" => "tag.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTag($event, tag)", "mb-autofocus" => "true", "should-focus" => "ctrl.newTag", - "ng-change" => "ctrl.tagTitleDidChange(tag)", "ng-focus" => "ctrl.onTagTitleFocus(tag)"} + "ng-change" => "ctrl.tagTitleDidChange(tag)", "ng-focus" => "ctrl.onTagTitleFocus(tag)", "ng-blur" => "ctrl.saveTag($event, tag)"} .count {{ctrl.noteCount(tag)}} From 48ea5dc22c18ba846616ee09e6c281381019edc4 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 29 Jan 2017 00:19:32 -0600 Subject: [PATCH 23/25] import error check --- .../javascripts/app/frontend/controllers/home.js | 1 - .../app/services/directives/views/accountMenu.js | 16 ++++++++-------- .../app/services/helpers/encryptionHelper.js | 5 ++++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/home.js b/app/assets/javascripts/app/frontend/controllers/home.js index bee9f6c50..8d1ebcb03 100644 --- a/app/assets/javascripts/app/frontend/controllers/home.js +++ b/app/assets/javascripts/app/frontend/controllers/home.js @@ -56,7 +56,6 @@ angular.module('app.frontend') } $scope.tagsSave = function(tag, callback) { - console.log("saving tag", tag); if(!tag.title || tag.title.length == 0) { $scope.notesRemoveTag(tag); return; diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index 34b18dc4f..42766057f 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -117,13 +117,13 @@ class AccountMenu { // allow loading indicator to come up with timeout $timeout(function(){ $scope.importJSONData(data, password, function(response){ - // console.log("Import response:", success, response); - $scope.importData.loading = false; - if(response) { + $timeout(function(){ + $scope.importData.loading = false; $scope.importData = null; - } else { - alert("There was an error importing your data. Please try again."); - } + if(!response) { + alert("There was an error importing your data. Please try again."); + } + }) }) }) } @@ -171,7 +171,7 @@ class AccountMenu { Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){ var mk = keys.mk; try { - EncryptionHelper.decryptMultipleItems(data.items, mk); + EncryptionHelper.decryptMultipleItems(data.items, mk, true); // delete items enc_item_key since the user's actually key will do the encrypting once its passed off data.items.forEach(function(item){ item.enc_item_key = null; @@ -182,7 +182,7 @@ class AccountMenu { catch (e) { console.log("Error decrypting", e); alert("There was an error decrypting your items. Make sure the password you entered is correct and try again."); - callback(false, null); + callback(null); return; } }.bind(this)); diff --git a/app/assets/javascripts/app/services/helpers/encryptionHelper.js b/app/assets/javascripts/app/services/helpers/encryptionHelper.js index 49f9fba77..7cf011ac8 100644 --- a/app/assets/javascripts/app/services/helpers/encryptionHelper.js +++ b/app/assets/javascripts/app/services/helpers/encryptionHelper.js @@ -34,7 +34,7 @@ class EncryptionHelper { item.content = content; } - static decryptMultipleItems(items, key) { + static decryptMultipleItems(items, key, throws) { for (var item of items) { if(item.deleted == true) { continue; @@ -51,6 +51,9 @@ class EncryptionHelper { item.content = Neeto.crypto.base64Decode(item.content.substring(3, item.content.length)) } } catch (e) { + if(throws) { + throw e; + } console.log("Error decrypting item", item, e); continue; } From 2274e6835df9bc297e76386bf97f25f8615ff1de Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 29 Jan 2017 11:36:20 -0600 Subject: [PATCH 24/25] handle uuid conflicts --- .../app/frontend/models/api/item.js | 4 --- .../javascripts/app/services/dbManager.js | 3 +- .../services/directives/views/accountMenu.js | 6 ++-- .../javascripts/app/services/modelManager.js | 16 ++++++++-- .../javascripts/app/services/syncManager.js | 31 ++++++++++++------- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/app/frontend/models/api/item.js b/app/assets/javascripts/app/frontend/models/api/item.js index 0409580eb..f6129f5a0 100644 --- a/app/assets/javascripts/app/frontend/models/api/item.js +++ b/app/assets/javascripts/app/frontend/models/api/item.js @@ -50,10 +50,6 @@ class Item { } } - alternateUUID() { - this.uuid = Neeto.crypto.generateUUID(); - } - setDirty(dirty) { this.dirty = dirty; diff --git a/app/assets/javascripts/app/services/dbManager.js b/app/assets/javascripts/app/services/dbManager.js index 0d702b032..5c33bc5de 100644 --- a/app/assets/javascripts/app/services/dbManager.js +++ b/app/assets/javascripts/app/services/dbManager.js @@ -98,11 +98,12 @@ class DBManager { }, null) } - deleteItem(item) { + deleteItem(item, callback) { this.openDatabase((db) => { var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid); request.onsuccess = function(event) { console.log("Successfully deleted item", item.uuid); + callback(true); }; }, null) } diff --git a/app/assets/javascripts/app/services/directives/views/accountMenu.js b/app/assets/javascripts/app/services/directives/views/accountMenu.js index 42766057f..5e9d74e62 100644 --- a/app/assets/javascripts/app/services/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/services/directives/views/accountMenu.js @@ -221,8 +221,10 @@ class AccountMenu { items: items } - // auth params are only needed when encrypted with a standard file key - data["auth_params"] = authManager.getAuthParams(); + if(ek) { + // auth params are only needed when encrypted with a standard file key + data["auth_params"] = authManager.getAuthParams(); + } return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */)); } diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index e07cd0893..aa436b6e2 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -22,6 +22,18 @@ class ModelManager { }) } + alternateUUIDForItem(item, callback) { + // we need to clone this item and give it a new uuid, then delete item with old uuid from db (you can't mofidy uuid's in our indexeddb setup) + var newItem = this.createItem(item); + newItem.uuid = Neeto.crypto.generateUUID(); + this.removeItemLocally(item, function(){ + this.addItem(newItem); + newItem.setDirty(true); + newItem.markAllReferencesDirty(); + callback(); + }.bind(this)); + } + allItemsMatchingTypes(contentTypes) { return this.items.filter(function(item){ return (_.includes(contentTypes, item.content_type) || _.includes(contentTypes, "*")) && !item.dummy; @@ -213,7 +225,7 @@ class ModelManager { item.removeAllRelationships(); } - removeItemLocally(item) { + removeItemLocally(item, callback) { _.pull(this.items, item); item.isBeingRemovedLocally(); @@ -227,7 +239,7 @@ class ModelManager { _.pull(this._extensions, item); } - this.dbManager.deleteItem(item); + this.dbManager.deleteItem(item, callback); } /* diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index ca5cd19f1..fadb6977e 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -50,7 +50,7 @@ class SyncManager { this.modelManager.removeItemLocally(item); } } - + if(callback) { callback({success: true}); } @@ -202,18 +202,25 @@ class SyncManager { } console.log("Handle unsaved", unsaved); - for(var mapping of unsaved) { - var itemResponse = mapping.item; - var item = this.modelManager.findItem(itemResponse.uuid); - var error = mapping.error; - if(error.tag == "uuid_conflict") { - item.alternateUUID(); - item.setDirty(true); - item.markAllReferencesDirty(); - } - } - this.sync(null, {additionalFields: ["created_at", "updated_at"]}); + var i = 0; + var handleNext = function() { + if (i < unsaved.length) { + var mapping = unsaved[i]; + var itemResponse = mapping.item; + var item = this.modelManager.findItem(itemResponse.uuid); + var error = mapping.error; + if(error.tag == "uuid_conflict") { + // uuid conflicts can occur if a user attempts to import an old data archive with uuids form the old account into a new account + this.modelManager.alternateUUIDForItem(item, handleNext); + } + ++i; + } else { + this.sync(null, {additionalFields: ["created_at", "updated_at"]}); + } + }.bind(this); + + handleNext(); } handleItemsResponse(responseItems, omitFields) { From 72afcd7da4c34c2e0a5f72fe42b6ce88df296fb3 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 29 Jan 2017 16:59:06 -0600 Subject: [PATCH 25/25] confirm delete, extension load actions --- .../app/frontend/controllers/editor.js | 6 +++-- .../app/frontend/models/app/extension.js | 26 ++++++++++++------- .../javascripts/app/services/authManager.js | 10 ++++--- .../javascripts/app/services/dbManager.js | 4 ++- .../javascripts/app/services/syncManager.js | 2 +- app/assets/stylesheets/app/_notes.scss | 3 +-- .../global-extensions-menu.html.haml | 4 +-- app/assets/templates/frontend/home.html.haml | 3 ++- 8 files changed, 36 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/app/frontend/controllers/editor.js b/app/assets/javascripts/app/frontend/controllers/editor.js index 43fa67afb..c0c8e2909 100644 --- a/app/assets/javascripts/app/frontend/controllers/editor.js +++ b/app/assets/javascripts/app/frontend/controllers/editor.js @@ -236,8 +236,10 @@ angular.module('app.frontend') } this.deleteNote = function() { - this.remove()(this.note); - this.showMenu = false; + if(confirm("Are you sure you want to delete this note?")) { + this.remove()(this.note); + this.showMenu = false; + } } this.clickedEditNote = function() { diff --git a/app/assets/javascripts/app/frontend/models/app/extension.js b/app/assets/javascripts/app/frontend/models/app/extension.js index acd42b4e2..d6a46aad2 100644 --- a/app/assets/javascripts/app/frontend/models/app/extension.js +++ b/app/assets/javascripts/app/frontend/models/app/extension.js @@ -1,18 +1,20 @@ class Action { constructor(json) { - _.merge(this, json); - this.running = false; // in case running=true was synced with server since model is uploaded nondiscriminatory - this.error = false; - if(this.lastExecuted) { - // is string - this.lastExecuted = new Date(this.lastExecuted); - } + _.merge(this, json); + this.running = false; // in case running=true was synced with server since model is uploaded nondiscriminatory + this.error = false; + if(this.lastExecuted) { + // is string + this.lastExecuted = new Date(this.lastExecuted); + } } - get permissionsString() { + permissionsString() { + console.log("permissions", this.permissions); if(!this.permissions) { return ""; } + var permission = this.permissions.charAt(0).toUpperCase() + this.permissions.slice(1); // capitalize first letter permission += ": "; for(var contentType of this.content_types) { @@ -28,7 +30,7 @@ class Action { return permission; } - get encryptionModeString() { + encryptionModeString() { if(this.verb != "post") { return null; } @@ -54,6 +56,12 @@ class Extension extends Item { this.encrypted = true; this.content_type = "Extension"; + + if(json.actions) { + this.actions = json.actions.map(function(action){ + return new Action(action); + }) + } } actionsInGlobalContext() { diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index f0976e1ee..cd936b929 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -39,8 +39,8 @@ angular.module('app.frontend') callback(response.plain()); }) .catch(function(response){ - console.log("Error getting current user", response); - callback(response.data); + console.log("Error getting auth params", response); + callback(null); }) } @@ -59,7 +59,7 @@ angular.module('app.frontend') this.login = function(url, email, password, callback) { this.getAuthParamsForEmail(url, email, function(authParams){ if(!authParams) { - callback({error: "Unable to get authentication parameters."}); + callback({error : {message: "Unable to get authentication parameters."}}); return; } @@ -72,6 +72,8 @@ angular.module('app.frontend') return; } + console.log("compute encryption keys", password, authParams); + Neeto.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){ var mk = keys.mk; var requestUrl = url + "/auth/sign_in"; @@ -112,7 +114,7 @@ angular.module('app.frontend') }.bind(this)) .catch(function(response){ console.log("Registration error", response); - callback(response.data); + callback(null); }) }.bind(this)); } diff --git a/app/assets/javascripts/app/services/dbManager.js b/app/assets/javascripts/app/services/dbManager.js index 5c33bc5de..d7e3edabb 100644 --- a/app/assets/javascripts/app/services/dbManager.js +++ b/app/assets/javascripts/app/services/dbManager.js @@ -103,7 +103,9 @@ class DBManager { var request = db.transaction("items", "readwrite").objectStore("items").delete(item.uuid); request.onsuccess = function(event) { console.log("Successfully deleted item", item.uuid); - callback(true); + if(callback) { + callback(true); + } }; }, null) } diff --git a/app/assets/javascripts/app/services/syncManager.js b/app/assets/javascripts/app/services/syncManager.js index fadb6977e..66344b15a 100644 --- a/app/assets/javascripts/app/services/syncManager.js +++ b/app/assets/javascripts/app/services/syncManager.js @@ -10,7 +10,7 @@ class SyncManager { } get serverURL() { - return localStorage.getItem("server") || "http://localhost:3000"; + return localStorage.getItem("server") || "https://n3.standardnotes.org"; } get masterKey() { diff --git a/app/assets/stylesheets/app/_notes.scss b/app/assets/stylesheets/app/_notes.scss index 6cb3adcce..d71413110 100644 --- a/app/assets/stylesheets/app/_notes.scss +++ b/app/assets/stylesheets/app/_notes.scss @@ -41,12 +41,11 @@ .note { width: 100%; - // max-width: 100%; padding: 15px; - // height: 70px; border-bottom: 1px solid $bg-color; cursor: pointer; background-color: white; + > .name { font-weight: 600; overflow: hidden; diff --git a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml index 2c33eac60..2dd5df9d1 100644 --- a/app/assets/templates/frontend/directives/global-extensions-menu.html.haml +++ b/app/assets/templates/frontend/directives/global-extensions-menu.html.haml @@ -25,8 +25,8 @@ %div %a{"ng-click" => "action.showPermissions = !action.showPermissions"} {{action.showPermissions ? "Hide permissions" : "Show permissions"}} %div{"ng-if" => "action.showPermissions"} - {{action.permissionsString}} - %label.block.normal {{action.encryptionModeString}} + {{action.permissionsString()}} + %label.block.normal {{action.encryptionModeString()}} %div .mt-5{"ng-if" => "action.repeat_mode"} diff --git a/app/assets/templates/frontend/home.html.haml b/app/assets/templates/frontend/home.html.haml index f2f40d0bf..f7057c955 100644 --- a/app/assets/templates/frontend/home.html.haml +++ b/app/assets/templates/frontend/home.html.haml @@ -8,4 +8,5 @@ "tag" => "selectedTag", "remove" => "deleteNote"} %editor-section{"ng-if" => "selectedNote", "note" => "selectedNote", "remove" => "deleteNote", "save" => "saveNote", "update-tags" => "updateTagsForNote"} - %header{"user" => "defaultUser"} + + %header