From a76f725f7fa1917510f6242bdd81276c618bcf46 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 25 Jan 2017 15:52:03 -0600 Subject: [PATCH] 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"}