From 29c9d8f36a44e5859564c1abd4d9cf6467f8ec2f Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 8 Nov 2018 14:26:05 -0600 Subject: [PATCH 01/12] Privs wip --- .../javascripts/app/controllers/footer.js | 25 +++- .../javascripts/app/controllers/home.js | 3 +- .../javascripts/app/controllers/lockScreen.js | 1 - .../app/directives/views/accountMenu.js | 14 ++- .../directives/views/privilegesAuthModal.js | 60 ++++++++++ .../javascripts/app/services/authManager.js | 7 ++ .../app/services/passcodeManager.js | 12 ++ .../app/services/privilegesManager.js | 107 ++++++++++++++++++ .../privileges-auth-modal.html.haml | 17 +++ app/assets/templates/footer.html.haml | 2 +- 10 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/app/directives/views/privilegesAuthModal.js create mode 100644 app/assets/javascripts/app/services/privilegesManager.js create mode 100644 app/assets/templates/directives/privileges-auth-modal.html.haml diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js index d408d1be4..3131e7c36 100644 --- a/app/assets/javascripts/app/controllers/footer.js +++ b/app/assets/javascripts/app/controllers/footer.js @@ -23,7 +23,8 @@ angular.module('app') } }) .controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, - syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager) { + syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager, + privilegesManager) { authManager.checkForSecurityUpdate().then((available) => { this.securityUpdateAvailable = available; @@ -173,6 +174,26 @@ angular.module('app') } this.selectRoom = function(room) { - room.showRoom = !room.showRoom; + let run = () => { + room.showRoom = !room.showRoom; + } + + if(!room.showRoom) { + // About to show, check if has privileges + if(privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageExtensions, () => { + run(); + }); + } + } else { + run(); + } + } + + this.clickOutsideAccountMenu = function() { + if(privilegesManager.authenticationInProgress()) { + return; + } + this.showAccountMenu = false; } }); diff --git a/app/assets/javascripts/app/controllers/home.js b/app/assets/javascripts/app/controllers/home.js index 6d8805094..c83a1c0a4 100644 --- a/app/assets/javascripts/app/controllers/home.js +++ b/app/assets/javascripts/app/controllers/home.js @@ -1,6 +1,7 @@ angular.module('app') .controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager, - dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager) { + dbManager, syncManager, authManager, themeManager, passcodeManager, storageManager, migrationManager, + privilegesManager) { storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession()); diff --git a/app/assets/javascripts/app/controllers/lockScreen.js b/app/assets/javascripts/app/controllers/lockScreen.js index e38eff9f6..8f3a9efd0 100644 --- a/app/assets/javascripts/app/controllers/lockScreen.js +++ b/app/assets/javascripts/app/controllers/lockScreen.js @@ -41,7 +41,6 @@ class LockScreen { }) } } - } angular.module('app').directive('lockScreen', () => new LockScreen); diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index d24684515..b49dfff81 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -10,7 +10,7 @@ class AccountMenu { } controller($scope, $rootScope, authManager, modelManager, syncManager, storageManager, dbManager, passcodeManager, - $timeout, $compile, archiveManager) { + $timeout, $compile, archiveManager, privilegesManager) { 'ngInject'; $scope.formData = {mergeLocal: true, ephemeral: false}; @@ -317,7 +317,17 @@ class AccountMenu { */ $scope.downloadDataArchive = function() { - archiveManager.downloadBackup($scope.archiveFormData.encrypted); + let run = () => { + archiveManager.downloadBackup($scope.archiveFormData.encrypted); + } + + if(privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDownloadBackup)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDownloadBackup, () => { + run(); + }); + } else { + run(); + } } /* diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js new file mode 100644 index 000000000..56dd04aba --- /dev/null +++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js @@ -0,0 +1,60 @@ +/* + The purpose of the conflict resoltion modal is to present two versions of a conflicted item, + and allow the user to choose which to keep (or to keep both.) +*/ + +class PrivilegesAuthModal { + + constructor() { + this.restrict = "E"; + this.templateUrl = "directives/privileges-auth-modal.html"; + this.scope = { + action: "=", + onSuccess: "=", + onCancel: "=", + }; + } + + link($scope, el, attrs) { + $scope.dismiss = function() { + el.remove(); + } + } + + controller($scope, privilegesManager, $timeout) { + 'ngInject'; + + $scope.privileges = privilegesManager.privilegesForAction($scope.action); + + $scope.cancel = function() { + $scope.dismiss(); + $scope.onCancel && $scope.onCancel(); + } + + $scope.doesPrivHaveFail = function(priv) { + if(!$scope.failedPrivs) { + return false; + } + return $scope.failedPrivs.find((failedPriv) => { + return failedPriv.name == priv.name; + }) != null; + } + + $scope.submit = function() { + privilegesManager.verifyPrivilegesForAction($scope.action, $scope.privileges).then((result) => { + console.log("Result", result); + $timeout(() => { + if(result.success) { + $scope.onSuccess(); + $scope.dismiss(); + } else { + $scope.failedPrivs = result.failedPrivs; + } + }) + }) + } + + } +} + +angular.module('app').directive('privilegesAuthModal', () => new PrivilegesAuthModal); diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index 6787d3709..47e169a68 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -92,6 +92,13 @@ class AuthManager extends SFAuthManager { } } + async verifyAccountPassword(password) { + let authParams = await this.getAuthParams(); + let keys = await SFJS.crypto.computeEncryptionKeysForUser(password, authParams); + let success = keys.mk === (await this.keys()).mk; + return success; + } + async checkForSecurityUpdate() { if(this.offline()) { return false; diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js index 47c9e763b..8d6fad7fa 100644 --- a/app/assets/javascripts/app/services/passcodeManager.js +++ b/app/assets/javascripts/app/services/passcodeManager.js @@ -36,6 +36,18 @@ angular.module('app') return authParams; } + this.verifyPasscode = async function(passcode) { + return new Promise(async (resolve, reject) => { + var params = this.passcodeAuthParams(); + let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params); + if(keys.pw !== params.hash) { + resolve(false); + } else { + resolve(true); + } + }) + } + this.unlock = function(passcode, callback) { var params = this.passcodeAuthParams(); SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => { diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js new file mode 100644 index 000000000..a621e6d5b --- /dev/null +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -0,0 +1,107 @@ +class PrivilegesManager { + + constructor(passcodeManager, authManager, $rootScope, $compile) { + this.passcodeManager = passcodeManager; + this.authManager = authManager; + this.$rootScope = $rootScope; + this.$compile = $compile; + + PrivilegesManager.PrivilegeAccountPassword = "PrivilegeAccountPassword"; + PrivilegesManager.PrivilegeLocalPasscode = "PrivilegeLocalPasscode"; + + PrivilegesManager.ActionManageExtensions = "ActionManageExtensions"; + PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup"; + } + + presentPrivilegesModal(action, onSuccess, onCancel) { + + let customSuccess = () => { + onSuccess(); + this.currentAuthenticationElement = null; + } + + let customCancel = () => { + onCancel(); + this.currentAuthenticationElement = null; + } + + var scope = this.$rootScope.$new(true); + scope.action = action; + scope.onSuccess = customSuccess; + scope.onCancel = customCancel; + var el = this.$compile( "" )(scope); + angular.element(document.body).append(el); + + this.currentAuthenticationElement = el; + } + + authenticationInProgress() { + return this.currentAuthenticationElement != null; + } + + privilegesForAction(action) { + return [ + { + name: PrivilegesManager.PrivilegeAccountPassword, + label: "Account Password", + prompt: "Please enter your account password." + }, + { + name: PrivilegesManager.PrivilegeLocalPasscode, + label: "Local Passcode", + prompt: "Please enter your local passcode." + } + ] + } + + actionRequiresPrivilege(action) { + return this.privilegesForAction(action).length > 0; + } + + async verifyPrivilegesForAction(action, inputPrivs) { + + let findInputPriv = (name) => { + return inputPrivs.find((priv) => { + return priv.name == name; + }) + } + + var requiredPrivileges = this.privilegesForAction(action); + var successfulPrivs = [], failedPrivs = []; + for(let requiredPriv of requiredPrivileges) { + var matchingPriv = findInputPriv(requiredPriv.name); + var passesAuth = await this.verifyAuthenticationParameters(matchingPriv); + if(passesAuth) { + successfulPrivs.push(matchingPriv); + } else { + failedPrivs.push(matchingPriv); + } + } + + return { + success: failedPrivs.length == 0, + successfulPrivs: successfulPrivs, + failedPrivs: failedPrivs + } + } + + async verifyAuthenticationParameters(parameters) { + + let verifyAccountPassword = async (password) => { + return this.authManager.verifyAccountPassword(password); + } + + let verifyLocalPasscode = async (passcode) => { + return this.passcodeManager.verifyPasscode(passcode); + } + + if(parameters.name == PrivilegesManager.PrivilegeAccountPassword) { + return verifyAccountPassword(parameters.authenticationValue); + } else if(parameters.name == PrivilegesManager.PrivilegeLocalPasscode) { + return verifyLocalPasscode(parameters.authenticationValue); + } + } + +} + +angular.module('app').service('privilegesManager', PrivilegesManager); diff --git a/app/assets/templates/directives/privileges-auth-modal.html.haml b/app/assets/templates/directives/privileges-auth-modal.html.haml new file mode 100644 index 000000000..a83f99b80 --- /dev/null +++ b/app/assets/templates/directives/privileges-auth-modal.html.haml @@ -0,0 +1,17 @@ +.background{"ng-click" => "cancel()"} + +.content#privileges-modal + .sn-component + .panel + .header + %h1.title Authentication Required + %a.close-button.info{"ng-click" => "cancel()"} Cancel + .content + .panel-section + .panel-row{"ng-repeat" => "priv in privileges"} + %p {{priv.prompt}} + %input{"type" => "password", "ng-model" => "priv.authenticationValue"} + %label.danger{"ng-if" => "doesPrivHaveFail(priv)"} Invalid authentication. Please try again. + + .footer + .button.info.big.block.bold{"ng-click" => "submit()"} Submit diff --git a/app/assets/templates/footer.html.haml b/app/assets/templates/footer.html.haml index c4dfd0171..8ba4a11cf 100644 --- a/app/assets/templates/footer.html.haml +++ b/app/assets/templates/footer.html.haml @@ -1,7 +1,7 @@ .sn-component #footer-bar.app-bar.no-edges .left - .item{"ng-click" => "ctrl.accountMenuPressed()", "click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"} + .item{"ng-click" => "ctrl.accountMenuPressed()", "click-outside" => "ctrl.clickOutsideAccountMenu()", "is-open" => "ctrl.showAccountMenu"} .column .circle.small{"ng-class" => "ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'default')"} .column From 0410391fc5721ca6a71d2c47e2c0398c63d32432 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Fri, 9 Nov 2018 13:49:49 -0600 Subject: [PATCH 02/12] Privileges modal --- .../javascripts/app/controllers/footer.js | 9 +- .../app/directives/views/accountMenu.js | 11 +- .../directives/views/privilegesAuthModal.js | 8 +- .../views/privilegesManagementModal.js | 61 ++++++++ .../app/services/privilegesManager.js | 142 ++++++++++++++---- .../directives/account-menu.html.haml | 5 +- .../privileges-management-modal.html.haml | 25 +++ 7 files changed, 225 insertions(+), 36 deletions(-) create mode 100644 app/assets/javascripts/app/directives/views/privilegesManagementModal.js create mode 100644 app/assets/templates/directives/privileges-management-modal.html.haml diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js index 3131e7c36..47ac00b76 100644 --- a/app/assets/javascripts/app/controllers/footer.js +++ b/app/assets/javascripts/app/controllers/footer.js @@ -173,14 +173,17 @@ angular.module('app') } } - this.selectRoom = function(room) { + this.selectRoom = async function(room) { let run = () => { - room.showRoom = !room.showRoom; + $timeout(() => { + room.showRoom = !room.showRoom; + }) } if(!room.showRoom) { // About to show, check if has privileges - if(privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) { + + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) { privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageExtensions, () => { run(); }); diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index b49dfff81..c59f7267d 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -167,10 +167,13 @@ class AccountMenu { $scope.openPasswordWizard = function(type) { // Close the account menu $scope.close(); - authManager.presentPasswordWizard(type); } + $scope.openPrivilegesModal = function() { + privilegesManager.presentPrivilegesManagementModal(); + } + // Allows indexeddb unencrypted logs to be deleted // clearAllModels will remove data from backing store, but not from working memory // See: https://github.com/standardnotes/desktop/issues/131 @@ -316,12 +319,12 @@ class AccountMenu { Export */ - $scope.downloadDataArchive = function() { + $scope.downloadDataArchive = async function() { let run = () => { archiveManager.downloadBackup($scope.archiveFormData.encrypted); } - - if(privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDownloadBackup)) { + + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDownloadBackup)) { privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDownloadBackup, () => { run(); }); diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js index 56dd04aba..087d1ae04 100644 --- a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js @@ -24,7 +24,11 @@ class PrivilegesAuthModal { controller($scope, privilegesManager, $timeout) { 'ngInject'; - $scope.privileges = privilegesManager.privilegesForAction($scope.action); + privilegesManager.requiredCredentialsForAction($scope.action).then((privs) => { + $timeout(() => { + $scope.privileges = privs; + }) + }) $scope.cancel = function() { $scope.dismiss(); @@ -41,7 +45,7 @@ class PrivilegesAuthModal { } $scope.submit = function() { - privilegesManager.verifyPrivilegesForAction($scope.action, $scope.privileges).then((result) => { + privilegesManager.authenticateAction($scope.action, $scope.privileges).then((result) => { console.log("Result", result); $timeout(() => { if(result.success) { diff --git a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js new file mode 100644 index 000000000..a1c4aaa45 --- /dev/null +++ b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js @@ -0,0 +1,61 @@ +class PrivilegesManagementModal { + + constructor() { + this.restrict = "E"; + this.templateUrl = "directives/privileges-management-modal.html"; + this.scope = { + + }; + } + + link($scope, el, attrs) { + $scope.dismiss = function() { + el.remove(); + } + } + + controller($scope, privilegesManager, $timeout) { + 'ngInject'; + + $scope.reloadPrivileges = async function() { + console.log("Reloading privs"); + $scope.availableActions = privilegesManager.getAvailableActions(); + $scope.availableCredentials = privilegesManager.getAvailableCredentials(); + + let metadata = {}; + for(let action of $scope.availableActions) { + var requiredCreds = await privilegesManager.requiredCredentialsForAction(action); + metadata[action] = { + displayInfo: privilegesManager.displayInfoForAction(action), + requiredCredentials: requiredCreds + } + + metadata[action]["credentialValues"] = {}; + for(var availableCred of $scope.availableCredentials) { + metadata[action]["credentialValues"][availableCred] = requiredCreds.includes(availableCred); + } + } + + $timeout(() => { + $scope.metadata = metadata; + }) + } + + $scope.checkboxValueChanged = function(action) { + let credentialValues = $scope.metadata[action]["credentialValues"]; + let keys = Object.keys(credentialValues).filter((key) => { + return credentialValues[key] == true; + }); + privilegesManager.setCredentialsForAction(action, keys); + } + + $scope.reloadPrivileges(); + + $scope.cancel = function() { + $scope.dismiss(); + $scope.onCancel && $scope.onCancel(); + } + } +} + +angular.module('app').directive('privilegesManagementModal', () => new PrivilegesManagementModal); diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index a621e6d5b..857a55d9f 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -1,27 +1,49 @@ class PrivilegesManager { - constructor(passcodeManager, authManager, $rootScope, $compile) { + constructor(passcodeManager, authManager, singletonManager, modelManager, $rootScope, $compile) { this.passcodeManager = passcodeManager; this.authManager = authManager; + this.singletonManager = singletonManager; + this.modelManager = modelManager; this.$rootScope = $rootScope; this.$compile = $compile; - PrivilegesManager.PrivilegeAccountPassword = "PrivilegeAccountPassword"; - PrivilegesManager.PrivilegeLocalPasscode = "PrivilegeLocalPasscode"; + this.loadPrivileges(); + + PrivilegesManager.CredentialAccountPassword = "CredentialAccountPassword"; + PrivilegesManager.CredentialLocalPasscode = "CredentialLocalPasscode"; PrivilegesManager.ActionManageExtensions = "ActionManageExtensions"; PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup"; + + this.availableActions = [ + PrivilegesManager.ActionManageExtensions, + PrivilegesManager.ActionDownloadBackup + ] + + this.availableCredentials = [ + PrivilegesManager.CredentialAccountPassword, + PrivilegesManager.CredentialLocalPasscode + ]; + } + + getAvailableActions() { + return this.availableActions; + } + + getAvailableCredentials() { + return this.availableCredentials; } presentPrivilegesModal(action, onSuccess, onCancel) { let customSuccess = () => { - onSuccess(); + onSuccess && onSuccess(); this.currentAuthenticationElement = null; } let customCancel = () => { - onCancel(); + onCancel && onCancel(); this.currentAuthenticationElement = null; } @@ -35,30 +57,98 @@ class PrivilegesManager { this.currentAuthenticationElement = el; } + presentPrivilegesManagementModal() { + var scope = this.$rootScope.$new(true); + var el = this.$compile( "")(scope); + angular.element(document.body).append(el); + } + authenticationInProgress() { return this.currentAuthenticationElement != null; } - privilegesForAction(action) { - return [ - { - name: PrivilegesManager.PrivilegeAccountPassword, - label: "Account Password", - prompt: "Please enter your account password." - }, - { - name: PrivilegesManager.PrivilegeLocalPasscode, - label: "Local Passcode", - prompt: "Please enter your local passcode." - } - ] + async loadPrivileges() { + return new Promise((resolve, reject) => { + let prefsContentType = "SN|Privileges"; + let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType); + this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => { + this.privileges = resolvedSingleton; + if(!this.privileges.content.desktopPrivileges) { + this.privileges.content.desktopPrivileges = []; + } + resolve(resolvedSingleton); + }, (valueCallback) => { + // Safe to create. Create and return object. + var privs = new SFItem({content_type: prefsContentType}); + this.modelManager.addItem(privs); + privs.setDirty(true); + this.$rootScope.sync(); + valueCallback(privs); + resolve(privs); + }); + }); } - actionRequiresPrivilege(action) { - return this.privilegesForAction(action).length > 0; + async getPrivileges() { + if(this.privileges) { + return this.privileges; + } else { + return this.loadPrivileges(); + } } - async verifyPrivilegesForAction(action, inputPrivs) { + async requiredCredentialsForAction(action) { + let privs = await this.getPrivileges(); + return privs.content.desktopPrivileges[action] || []; + } + + displayInfoForCredential(credential) { + let metadata = {} + + metadata[PrivilegesManager.CredentialAccountPassword] = { + label: "Account Password", + prompt: "Please enter your account password." + } + + metadata[PrivilegesManager.CredentialLocalPasscode] = { + label: "Local Passcode", + prompt: "Please enter your local passcode." + } + + return metadata[credential]; + } + + displayInfoForAction(action) { + let metadata = {}; + + metadata[PrivilegesManager.ActionManageExtensions] = { + label: "Manage Extensions" + } + metadata[PrivilegesManager.ActionDownloadBackup] = { + label: "Download Backups" + }; + + return metadata[action]; + } + + async actionRequiresPrivilege(action) { + return (await this.requiredCredentialsForAction(action)).length > 0; + } + + async setCredentialsForAction(action, credentials) { + console.log("Setting credentials for action", action, credentials); + let privs = await this.getPrivileges(); + privs.content.desktopPrivileges[action] = credentials; + this.savePrivileges(); + } + + async savePrivileges() { + let privs = await this.getPrivileges(); + privs.setDirty(true); + this.$rootScope.sync(); + } + + async authenticateAction(action, inputPrivs) { let findInputPriv = (name) => { return inputPrivs.find((priv) => { @@ -66,11 +156,11 @@ class PrivilegesManager { }) } - var requiredPrivileges = this.privilegesForAction(action); + var requiredPrivileges = await this.requiredCredentialsForAction(action); var successfulPrivs = [], failedPrivs = []; for(let requiredPriv of requiredPrivileges) { var matchingPriv = findInputPriv(requiredPriv.name); - var passesAuth = await this.verifyAuthenticationParameters(matchingPriv); + var passesAuth = await this._verifyAuthenticationParameters(matchingPriv); if(passesAuth) { successfulPrivs.push(matchingPriv); } else { @@ -85,7 +175,7 @@ class PrivilegesManager { } } - async verifyAuthenticationParameters(parameters) { + async _verifyAuthenticationParameters(parameters) { let verifyAccountPassword = async (password) => { return this.authManager.verifyAccountPassword(password); @@ -95,9 +185,9 @@ class PrivilegesManager { return this.passcodeManager.verifyPasscode(passcode); } - if(parameters.name == PrivilegesManager.PrivilegeAccountPassword) { + if(parameters.name == PrivilegesManager.CredentialAccountPassword) { return verifyAccountPassword(parameters.authenticationValue); - } else if(parameters.name == PrivilegesManager.PrivilegeLocalPasscode) { + } else if(parameters.name == PrivilegesManager.CredentialLocalPasscode) { return verifyLocalPasscode(parameters.authenticationValue); } } diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml index f0fb6736c..ca512a6e4 100644 --- a/app/assets/templates/directives/account-menu.html.haml +++ b/app/assets/templates/directives/account-menu.html.haml @@ -85,7 +85,10 @@ .panel-row - %a.panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"} Change Password + %a.panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"} + Change Password + %a.panel-row.condensed{"ng-click" => "openPrivilegesModal('')"} + Manage Privileges %a.panel-row.justify-left.condensed.success{"ng-if" => "securityUpdateAvailable", "ng-click" => "openPasswordWizard('upgrade-security')"} .inline.circle.small.success.mr-8 .inline Security Update Available diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml new file mode 100644 index 000000000..028393d74 --- /dev/null +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -0,0 +1,25 @@ +.background{"ng-click" => "cancel()"} + +.content#privileges-modal + .sn-component + .panel + .header + %h1.title Manage Privileges + %a.close-button.info{"ng-click" => "cancel()"} Cancel + .content + .panel-section + %table + %thead + %tr + %th + %th{"ng-repeat" => "cred in availableCredentials"} + {{cred}} + %tbody + %tr{"ng-repeat" => "action in availableActions"} + %td + %p {{metadata[action].displayInfo.label}} + %th{"ng-repeat" => "cred in availableCredentials"} + %input{"type" => "checkbox", "ng-model" => "metadata[action]['credentialValues'][cred]", "ng-change" => "checkboxValueChanged(action)"} + + .footer + .button.info.big.block.bold{"ng-click" => "submit()"} Save From 9835992e166c03334f3106f104f939cdc8e88522 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Mon, 12 Nov 2018 15:16:50 -0600 Subject: [PATCH 03/12] Privs small refactor --- .../directives/functional/infiniteScroll.js | 3 - .../directives/views/privilegesAuthModal.js | 26 +++++---- .../views/privilegesManagementModal.js | 47 +++++++--------- .../javascripts/app/models/privileges.js | 34 ++++++++++++ .../javascripts/app/services/modelManager.js | 3 +- .../app/services/privilegesManager.js | 55 ++++++------------- .../privileges-auth-modal.html.haml | 8 +-- .../privileges-management-modal.html.haml | 13 ++--- 8 files changed, 99 insertions(+), 90 deletions(-) create mode 100644 app/assets/javascripts/app/models/privileges.js diff --git a/app/assets/javascripts/app/directives/functional/infiniteScroll.js b/app/assets/javascripts/app/directives/functional/infiniteScroll.js index 3e1e633ef..21bacd325 100644 --- a/app/assets/javascripts/app/directives/functional/infiniteScroll.js +++ b/app/assets/javascripts/app/directives/functional/infiniteScroll.js @@ -2,9 +2,6 @@ angular.module('app').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] diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js index 087d1ae04..99dad564d 100644 --- a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js @@ -24,35 +24,41 @@ class PrivilegesAuthModal { controller($scope, privilegesManager, $timeout) { 'ngInject'; - privilegesManager.requiredCredentialsForAction($scope.action).then((privs) => { + $scope.authenticationParameters = {}; + + privilegesManager.getPrivileges().then((privileges) => { $timeout(() => { - $scope.privileges = privs; + $scope.privileges = privileges; + $scope.requiredCredentials = privileges.getCredentialsForAction($scope.action); }) - }) + }); + + $scope.promptForCredential = function(credential) { + return privilegesManager.displayInfoForCredential(credential).prompt; + } $scope.cancel = function() { $scope.dismiss(); $scope.onCancel && $scope.onCancel(); } - $scope.doesPrivHaveFail = function(priv) { - if(!$scope.failedPrivs) { + $scope.isCredentialInFailureState = function(credential) { + if(!$scope.failedCredentials) { return false; } - return $scope.failedPrivs.find((failedPriv) => { - return failedPriv.name == priv.name; + return $scope.failedCredentials.find((candidate) => { + return candidate == credential; }) != null; } $scope.submit = function() { - privilegesManager.authenticateAction($scope.action, $scope.privileges).then((result) => { - console.log("Result", result); + privilegesManager.authenticateAction($scope.action, $scope.authenticationParameters).then((result) => { $timeout(() => { if(result.success) { $scope.onSuccess(); $scope.dismiss(); } else { - $scope.failedPrivs = result.failedPrivs; + $scope.failedCredentials = result.failedCredentials; } }) }) diff --git a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js index a1c4aaa45..b792d3858 100644 --- a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js @@ -17,36 +17,29 @@ class PrivilegesManagementModal { controller($scope, privilegesManager, $timeout) { 'ngInject'; - $scope.reloadPrivileges = async function() { - console.log("Reloading privs"); - $scope.availableActions = privilegesManager.getAvailableActions(); - $scope.availableCredentials = privilegesManager.getAvailableCredentials(); + $scope.dummy = {}; - let metadata = {}; - for(let action of $scope.availableActions) { - var requiredCreds = await privilegesManager.requiredCredentialsForAction(action); - metadata[action] = { - displayInfo: privilegesManager.displayInfoForAction(action), - requiredCredentials: requiredCreds - } - - metadata[action]["credentialValues"] = {}; - for(var availableCred of $scope.availableCredentials) { - metadata[action]["credentialValues"][availableCred] = requiredCreds.includes(availableCred); - } - } - - $timeout(() => { - $scope.metadata = metadata; - }) + $scope.displayInfoForCredential = function(credential) { + return privilegesManager.displayInfoForCredential(credential).label; } - $scope.checkboxValueChanged = function(action) { - let credentialValues = $scope.metadata[action]["credentialValues"]; - let keys = Object.keys(credentialValues).filter((key) => { - return credentialValues[key] == true; - }); - privilegesManager.setCredentialsForAction(action, keys); + $scope.displayInfoForAction = function(action) { + return privilegesManager.displayInfoForAction(action).label; + } + + $scope.isCredentialRequiredForAction = function(action, credential) { + return $scope.privileges.isCredentialRequiredForAction(action, credential); + } + + $scope.reloadPrivileges = async function() { + $scope.privileges = await privilegesManager.getPrivileges(); + $scope.availableActions = privilegesManager.getAvailableActions(); + $scope.availableCredentials = privilegesManager.getAvailableCredentials(); + } + + $scope.checkboxValueChanged = function(action, credential) { + $scope.privileges.toggleCredentialForAction(action, credential); + privilegesManager.savePrivileges(); } $scope.reloadPrivileges(); diff --git a/app/assets/javascripts/app/models/privileges.js b/app/assets/javascripts/app/models/privileges.js new file mode 100644 index 000000000..3fb5f96fa --- /dev/null +++ b/app/assets/javascripts/app/models/privileges.js @@ -0,0 +1,34 @@ +class SNPrivileges extends SFItem { + + setCredentialsForAction(action, credentials) { + this.content.desktopPrivileges[action] = credentials; + } + + getCredentialsForAction(action) { + return this.content.desktopPrivileges[action] || []; + } + + toggleCredentialForAction(action, credential) { + if(this.isCredentialRequiredForAction(action, credential)) { + this.removeCredentialForAction(action, credential); + } else { + this.addCredentialForAction(action, credential); + } + } + + removeCredentialForAction(action, credential) { + _.pull(this.content.desktopPrivileges[action], credential); + } + + addCredentialForAction(action, credential) { + var credentials = this.getCredentialsForAction(action); + credentials.push(credential); + this.setCredentialsForAction(action, credentials); + } + + isCredentialRequiredForAction(action, credential) { + var credentialsRequired = this.getCredentialsForAction(action); + return credentialsRequired.includes(credential); + } + +} diff --git a/app/assets/javascripts/app/services/modelManager.js b/app/assets/javascripts/app/services/modelManager.js index c3f30c207..9af2df562 100644 --- a/app/assets/javascripts/app/services/modelManager.js +++ b/app/assets/javascripts/app/services/modelManager.js @@ -7,7 +7,8 @@ SFModelManager.ContentTypeClassMapping = { "SN|Theme" : SNTheme, "SN|Component" : SNComponent, "SF|Extension" : SNServerExtension, - "SF|MFA" : SNMfa + "SF|MFA" : SNMfa, + "SN|Privileges" : SNPrivileges }; SFItem.AppDomain = "org.standardnotes.sn"; diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index 857a55d9f..18dd7c386 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -74,12 +74,12 @@ class PrivilegesManager { this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => { this.privileges = resolvedSingleton; if(!this.privileges.content.desktopPrivileges) { - this.privileges.content.desktopPrivileges = []; + this.privileges.content.desktopPrivileges = {}; } resolve(resolvedSingleton); }, (valueCallback) => { // Safe to create. Create and return object. - var privs = new SFItem({content_type: prefsContentType}); + var privs = new Privilege({content_type: prefsContentType}); this.modelManager.addItem(privs); privs.setDirty(true); this.$rootScope.sync(); @@ -97,11 +97,6 @@ class PrivilegesManager { } } - async requiredCredentialsForAction(action) { - let privs = await this.getPrivileges(); - return privs.content.desktopPrivileges[action] || []; - } - displayInfoForCredential(credential) { let metadata = {} @@ -132,14 +127,7 @@ class PrivilegesManager { } async actionRequiresPrivilege(action) { - return (await this.requiredCredentialsForAction(action)).length > 0; - } - - async setCredentialsForAction(action, credentials) { - console.log("Setting credentials for action", action, credentials); - let privs = await this.getPrivileges(); - privs.content.desktopPrivileges[action] = credentials; - this.savePrivileges(); + return (await this.getPrivileges()).getCredentialsForAction(action).length > 0; } async savePrivileges() { @@ -148,34 +136,27 @@ class PrivilegesManager { this.$rootScope.sync(); } - async authenticateAction(action, inputPrivs) { + async authenticateAction(action, credentialAuthMapping) { + var requiredCredentials = (await this.getPrivileges()).getCredentialsForAction(action); + var successfulCredentials = [], failedCredentials = []; - let findInputPriv = (name) => { - return inputPrivs.find((priv) => { - return priv.name == name; - }) - } - - var requiredPrivileges = await this.requiredCredentialsForAction(action); - var successfulPrivs = [], failedPrivs = []; - for(let requiredPriv of requiredPrivileges) { - var matchingPriv = findInputPriv(requiredPriv.name); - var passesAuth = await this._verifyAuthenticationParameters(matchingPriv); + for(let requiredCredential of requiredCredentials) { + var passesAuth = await this._verifyAuthenticationParameters(requiredCredential, credentialAuthMapping[requiredCredential]); if(passesAuth) { - successfulPrivs.push(matchingPriv); + successfulCredentials.push(requiredCredential); } else { - failedPrivs.push(matchingPriv); + failedCredentials.push(requiredCredential); } } return { - success: failedPrivs.length == 0, - successfulPrivs: successfulPrivs, - failedPrivs: failedPrivs + success: failedCredentials.length == 0, + successfulCredentials: successfulCredentials, + failedCredentials: failedCredentials } } - async _verifyAuthenticationParameters(parameters) { + async _verifyAuthenticationParameters(credential, value) { let verifyAccountPassword = async (password) => { return this.authManager.verifyAccountPassword(password); @@ -185,10 +166,10 @@ class PrivilegesManager { return this.passcodeManager.verifyPasscode(passcode); } - if(parameters.name == PrivilegesManager.CredentialAccountPassword) { - return verifyAccountPassword(parameters.authenticationValue); - } else if(parameters.name == PrivilegesManager.CredentialLocalPasscode) { - return verifyLocalPasscode(parameters.authenticationValue); + if(credential == PrivilegesManager.CredentialAccountPassword) { + return verifyAccountPassword(value); + } else if(credential == PrivilegesManager.CredentialLocalPasscode) { + return verifyLocalPasscode(value); } } diff --git a/app/assets/templates/directives/privileges-auth-modal.html.haml b/app/assets/templates/directives/privileges-auth-modal.html.haml index a83f99b80..2603614a0 100644 --- a/app/assets/templates/directives/privileges-auth-modal.html.haml +++ b/app/assets/templates/directives/privileges-auth-modal.html.haml @@ -8,10 +8,10 @@ %a.close-button.info{"ng-click" => "cancel()"} Cancel .content .panel-section - .panel-row{"ng-repeat" => "priv in privileges"} - %p {{priv.prompt}} - %input{"type" => "password", "ng-model" => "priv.authenticationValue"} - %label.danger{"ng-if" => "doesPrivHaveFail(priv)"} Invalid authentication. Please try again. + .panel-row{"ng-repeat" => "credential in requiredCredentials"} + %p {{promptForCredential(credential)}} + %input{"type" => "password", "ng-model" => "authenticationParameters[credential]"} + %label.danger{"ng-if" => "isCredentialInFailureState(credential)"} Invalid authentication. Please try again. .footer .button.info.big.block.bold{"ng-click" => "submit()"} Submit diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml index 028393d74..94292cf7e 100644 --- a/app/assets/templates/directives/privileges-management-modal.html.haml +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -5,7 +5,7 @@ .panel .header %h1.title Manage Privileges - %a.close-button.info{"ng-click" => "cancel()"} Cancel + %a.close-button.info{"ng-click" => "cancel()"} Done .content .panel-section %table @@ -13,13 +13,10 @@ %tr %th %th{"ng-repeat" => "cred in availableCredentials"} - {{cred}} + {{displayInfoForCredential(cred)}} %tbody %tr{"ng-repeat" => "action in availableActions"} %td - %p {{metadata[action].displayInfo.label}} - %th{"ng-repeat" => "cred in availableCredentials"} - %input{"type" => "checkbox", "ng-model" => "metadata[action]['credentialValues'][cred]", "ng-change" => "checkboxValueChanged(action)"} - - .footer - .button.info.big.block.bold{"ng-click" => "submit()"} Save + %p {{displayInfoForAction(action)}} + %th{"ng-repeat" => "credential in availableCredentials"} + %input{"type" => "checkbox", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"} From 091f4cff7f05bb8d8df1bf3bcd0473239279d8f2 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 13 Nov 2018 15:05:57 -0600 Subject: [PATCH 04/12] Autolock wip --- .../javascripts/app/controllers/notes.js | 29 +- .../app/directives/views/accountMenu.js | 19 +- .../javascripts/app/services/httpManager.js | 2 +- .../app/services/passcodeManager.js | 273 +++++++++++------- .../app/services/privilegesManager.js | 15 +- app/assets/stylesheets/app/_editor.scss | 11 +- app/assets/stylesheets/app/_modals.scss | 4 + .../directives/account-menu.html.haml | 7 +- .../privileges-management-modal.html.haml | 11 + app/assets/templates/footer.html.haml | 2 +- app/assets/templates/notes.html.haml | 4 +- 11 files changed, 258 insertions(+), 119 deletions(-) diff --git a/app/assets/javascripts/app/controllers/notes.js b/app/assets/javascripts/app/controllers/notes.js index 1c33e0d1b..6c0f54644 100644 --- a/app/assets/javascripts/app/controllers/notes.js +++ b/app/assets/javascripts/app/controllers/notes.js @@ -31,7 +31,8 @@ angular.module('app') } } }) - .controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, storageManager, desktopManager) { + .controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager, + storageManager, desktopManager, privilegesManager) { this.panelController = {}; @@ -198,19 +199,31 @@ angular.module('app') } } - this.selectNote = function(note, viaClick = false) { + this.selectNote = async function(note, viaClick = false) { if(!note) { this.createNewNote(); return; } - this.selectedNote = note; - note.conflict_of = null; // clear conflict - this.selectionMade()(note); - this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0); + let run = () => { + $timeout(() => { + this.selectedNote = note; + note.conflict_of = null; // clear conflict + this.selectionMade()(note); + this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0); - if(viaClick && this.isFiltering()) { - desktopManager.searchText(this.noteFilter.text); + if(viaClick && this.isFiltering()) { + desktopManager.searchText(this.noteFilter.text); + } + }) + } + + if(note.locked && await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionViewLockedNotes)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionViewLockedNotes, () => { + run(); + }); + } else { + run(); } } diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index c59f7267d..5a6c17450 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -38,7 +38,6 @@ class AccountMenu { } $scope.canAddPasscode = !authManager.isEphemeralSession(); - $scope.syncStatus = syncManager.syncStatus; $scope.submitMfaForm = function() { @@ -375,6 +374,24 @@ class AccountMenu { Passcode Lock */ + $scope.passcodeAutoLockOptions = passcodeManager.getAutoLockIntervalOptions(); + + $scope.reloadAutoLockInterval = function() { + passcodeManager.getAutoLockInterval().then((interval) => { + $timeout(() => { + $scope.selectedAutoLockInterval = interval; + console.log("selectedAutoLockInterval", $scope.selectedAutoLockInterval); + }) + }) + } + + $scope.reloadAutoLockInterval(); + + $scope.selectAutoLockInterval = async function(interval) { + await passcodeManager.setAutoLockInterval(interval); + $scope.reloadAutoLockInterval(); + } + $scope.hasPasscode = function() { return passcodeManager.hasPasscode(); } diff --git a/app/assets/javascripts/app/services/httpManager.js b/app/assets/javascripts/app/services/httpManager.js index 637db6041..a3415b4b5 100644 --- a/app/assets/javascripts/app/services/httpManager.js +++ b/app/assets/javascripts/app/services/httpManager.js @@ -5,7 +5,7 @@ class HttpManager extends SFHttpManager { super($timeout); this.setJWTRequestHandler(async () => { - return storageManager.getItem("jwt");; + return storageManager.getItem("jwt"); }) } } diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js index 8d6fad7fa..0210ee96b 100644 --- a/app/assets/javascripts/app/services/passcodeManager.js +++ b/app/assets/javascripts/app/services/passcodeManager.js @@ -1,112 +1,183 @@ -angular.module('app') - .provider('passcodeManager', function () { +class PasscodeManager { - this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) { - return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager); - } + constructor(authManager, storageManager) { + document.addEventListener('visibilitychange', () => { + this.documentVisibilityChanged(document.visibilityState); + }); - function PasscodeManager($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) { + this.authManager = authManager; + this.storageManager = storageManager; - this._hasPasscode = storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null; + this._hasPasscode = this.storageManager.getItemSync("offlineParams", StorageManager.Fixed) != null; this._locked = this._hasPasscode; - this.isLocked = function() { - return this._locked; - } + const MillisecondsPerSecond = 1000; + PasscodeManager.AutoLockIntervalNone = 0; + PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond; + PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond; + PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond; - this.hasPasscode = function() { - return this._hasPasscode; - } + PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey"; + } - this.keys = function() { - return this._keys; - } - - this.passcodeAuthParams = function() { - var authParams = JSON.parse(storageManager.getItemSync("offlineParams", StorageManager.Fixed)); - if(authParams && !authParams.version) { - var keys = this.keys(); - if(keys && keys.ak) { - // If there's no version stored, and there's an ak, it has to be 002. Newer versions would have thier version stored in authParams. - authParams.version = "002"; - } else { - authParams.version = "001"; - } + getAutoLockIntervalOptions() { + return [ + { + value: PasscodeManager.AutoLockIntervalNone, + label: "None" + }, + { + value: PasscodeManager.AutoLockIntervalOneMinute, + label: "1 Min" + }, + { + value: PasscodeManager.AutoLockIntervalFiveMinutes, + label: "5 Min" + }, + { + value: PasscodeManager.AutoLockIntervalOneHour, + label: "1 Hr" } - return authParams; - } + ] + } - this.verifyPasscode = async function(passcode) { - return new Promise(async (resolve, reject) => { - var params = this.passcodeAuthParams(); - let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params); - if(keys.pw !== params.hash) { - resolve(false); - } else { - resolve(true); - } - }) - } - - this.unlock = function(passcode, callback) { - var params = this.passcodeAuthParams(); - SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => { - if(keys.pw !== params.hash) { - callback(false); - return; - } - - this._keys = keys; - this._authParams = params; - this.decryptLocalStorage(keys, params).then(() => { - this._locked = false; - callback(true); - }) - }); - } - - this.setPasscode = (passcode, callback) => { - var uuid = SFJS.crypto.generateUUIDSync(); - - SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => { - let keys = results.keys; - let authParams = results.authParams; - - authParams.hash = keys.pw; - this._keys = keys; - this._hasPasscode = true; - this._authParams = authParams; - - // Encrypting will initially clear localStorage - this.encryptLocalStorage(keys, authParams); - - // After it's cleared, it's safe to write to it - storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed); - callback(true); - }); - } - - this.changePasscode = (newPasscode, callback) => { - this.setPasscode(newPasscode, callback); - } - - this.clearPasscode = function() { - storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral - storageManager.removeItem("offlineParams", StorageManager.Fixed); - this._keys = null; - this._hasPasscode = false; - } - - this.encryptLocalStorage = function(keys, authParams) { - storageManager.setKeys(keys, authParams); - // Switch to Ephemeral storage, wiping Fixed storage - // Last argument is `force`, which we set to true because in the case of changing passcode - storageManager.setItemsMode(authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true); - } - - this.decryptLocalStorage = async function(keys, authParams) { - storageManager.setKeys(keys, authParams); - return storageManager.decryptStorage(); + documentVisibilityChanged(visbility) { + let visible = document.visibilityState == "visible"; + if(!visible) { + this.beginAutoLockTimer(); + } else { + this.cancelAutoLockTimer(); } } -}); + + async beginAutoLockTimer() { + console.log("beginAutoLockTimer"); + var interval = await this.getAutoLockInterval(); + this.lockTimeout = setTimeout(() => { + this.lockApplication(); + }, interval); + } + + cancelAutoLockTimer() { + console.log("cancelAutoLockTimer"); + clearTimeout(this.lockTimeout); + } + + lockApplication() { + console.log("lockApplication"); + window.location.reload(); + this.cancelAutoLockTimer(); + } + + isLocked() { + return this._locked; + } + + hasPasscode() { + return this._hasPasscode; + } + + keys() { + return this._keys; + } + + async setAutoLockInterval(interval) { + console.log("Set autolock interval", interval); + return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.Fixed); + } + + async getAutoLockInterval() { + let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.Fixed); + console.log("Got interval", interval); + return interval && JSON.parse(interval); + } + + passcodeAuthParams() { + var authParams = JSON.parse(this.storageManager.getItemSync("offlineParams", StorageManager.Fixed)); + if(authParams && !authParams.version) { + var keys = this.keys(); + if(keys && keys.ak) { + // If there's no version stored, and there's an ak, it has to be 002. Newer versions would have their version stored in authParams. + authParams.version = "002"; + } else { + authParams.version = "001"; + } + } + return authParams; + } + + async verifyPasscode(passcode) { + return new Promise(async (resolve, reject) => { + var params = this.passcodeAuthParams(); + let keys = await SFJS.crypto.computeEncryptionKeysForUser(passcode, params); + if(keys.pw !== params.hash) { + resolve(false); + } else { + resolve(true); + } + }) + } + + unlock(passcode, callback) { + var params = this.passcodeAuthParams(); + SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => { + if(keys.pw !== params.hash) { + callback(false); + return; + } + + this._keys = keys; + this._authParams = params; + this.decryptLocalStorage(keys, params).then(() => { + this._locked = false; + callback(true); + }) + }); + } + + setPasscode(passcode, callback) { + var uuid = SFJS.crypto.generateUUIDSync(); + + SFJS.crypto.generateInitialKeysAndAuthParamsForUser(uuid, passcode).then((results) => { + let keys = results.keys; + let authParams = results.authParams; + + authParams.hash = keys.pw; + this._keys = keys; + this._hasPasscode = true; + this._authParams = authParams; + + // Encrypting will initially clear localStorage + this.encryptLocalStorage(keys, authParams); + + // After it's cleared, it's safe to write to it + this.storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed); + callback(true); + }); + } + + changePasscode(newPasscode, callback) { + this.setPasscode(newPasscode, callback); + } + + clearPasscode() { + this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.Fixed); // Transfer from Ephemeral + this.storageManager.removeItem("offlineParams", StorageManager.Fixed); + this._keys = null; + this._hasPasscode = false; + } + + encryptLocalStorage(keys, authParams) { + this.storageManager.setKeys(keys, authParams); + // Switch to Ephemeral storage, wiping Fixed storage + // Last argument is `force`, which we set to true because in the case of changing passcode + this.storageManager.setItemsMode(this.authManager.isEphemeralSession() ? StorageManager.Ephemeral : StorageManager.FixedEncrypted, true); + } + + async decryptLocalStorage(keys, authParams) { + this.storageManager.setKeys(keys, authParams); + return this.storageManager.decryptStorage(); + } +} + +angular.module('app').service('passcodeManager', PasscodeManager); diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index 18dd7c386..8736057e4 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -15,10 +15,14 @@ class PrivilegesManager { PrivilegesManager.ActionManageExtensions = "ActionManageExtensions"; PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup"; + PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes"; + PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges"; this.availableActions = [ PrivilegesManager.ActionManageExtensions, - PrivilegesManager.ActionDownloadBackup + PrivilegesManager.ActionDownloadBackup, + PrivilegesManager.ActionViewLockedNotes, + PrivilegesManager.ActionManagePrivileges ] this.availableCredentials = [ @@ -119,10 +123,19 @@ class PrivilegesManager { metadata[PrivilegesManager.ActionManageExtensions] = { label: "Manage Extensions" } + metadata[PrivilegesManager.ActionDownloadBackup] = { label: "Download Backups" }; + metadata[PrivilegesManager.ActionViewLockedNotes] = { + label: "View Locked Notes" + }; + + metadata[PrivilegesManager.ActionManagePrivileges] = { + label: "Manage Privileges" + } + return metadata[action]; } diff --git a/app/assets/stylesheets/app/_editor.scss b/app/assets/stylesheets/app/_editor.scss index 0d4f3b225..272791b94 100644 --- a/app/assets/stylesheets/app/_editor.scss +++ b/app/assets/stylesheets/app/_editor.scss @@ -1,4 +1,11 @@ $heading-height: 75px; + +#editor-column { + .locked { + opacity: 0.8; + } +} + .editor { flex: 1 50%; display: flex; @@ -7,10 +14,6 @@ $heading-height: 75px; background-color: white; } -.locked { - opacity: 0.8; -} - #editor-title-bar { width: 100%; diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss index ca1dac561..6d6278cbd 100644 --- a/app/assets/stylesheets/app/_modals.scss +++ b/app/assets/stylesheets/app/_modals.scss @@ -12,6 +12,10 @@ } } +#privileges-modal { + width: 700px; +} + #password-wizard { font-size: 16px; } diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml index ca512a6e4..9733c4a59 100644 --- a/app/assets/templates/directives/account-menu.html.haml +++ b/app/assets/templates/directives/account-menu.html.haml @@ -130,7 +130,12 @@ .horizontal-group %a.info{"ng-click" => "changePasscodePressed()"} Change Passcode %a.danger{"ng-click" => "removePasscodePressed()"} Remove Passcode - + .panel-row + .horizontal-group + %p Autolock: + %a.info{"ng-repeat" => "option in passcodeAutoLockOptions", "ng-click" => "selectAutoLockInterval(option.value)", + "ng-class" => "{'info bold' : option.value == selectedAutoLockInterval}"} + {{option.label}} .panel-section{"ng-if" => "!importData.loading"} diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml index 94292cf7e..e4f730e2d 100644 --- a/app/assets/templates/directives/privileges-management-modal.html.haml +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -20,3 +20,14 @@ %p {{displayInfoForAction(action)}} %th{"ng-repeat" => "credential in availableCredentials"} %input{"type" => "checkbox", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"} + .footer + %h2 About Privileges + %p + Privileges represent interface level authentication for accessing certain items and features. + Note that when your application is unlocked, your data exists in temporary memory in an unencrypted state. + Privileges are meant to protect against unwanted access in the event of an unlocked application, but do not affect data encryption state. + + %p + Privileges sync across your other devices (not including mobile); however, note that if you require + a "Local Passcode" privilege, and another device does not have a local passcode set up, the local passcode + requirement will be ignored on that device. diff --git a/app/assets/templates/footer.html.haml b/app/assets/templates/footer.html.haml index 8ba4a11cf..c2a8e5c93 100644 --- a/app/assets/templates/footer.html.haml +++ b/app/assets/templates/footer.html.haml @@ -39,6 +39,6 @@ .item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} .label Refresh - .item#lock-item{"ng-if" => "ctrl.hasPasscode()"} + .item#lock-item{"ng-if" => "ctrl.hasPasscode()", "title" => "Locks application and wipes unencrypted data from memory."} .label %i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"} diff --git a/app/assets/templates/notes.html.haml b/app/assets/templates/notes.html.haml index a1e48dd1b..46f1ab1a0 100644 --- a/app/assets/templates/notes.html.haml +++ b/app/assets/templates/notes.html.haml @@ -59,9 +59,11 @@ .faded {{note.savedTagsString || note.tagsString()}} .name{"ng-if" => "note.title"} + %span.locked.tinted{"ng-if" => "note.locked", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"} + %i.icon.ion-locked.medium-text {{note.title}} - .note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview"} + .note-preview{"ng-if" => "!ctrl.hideNotePreview && !note.content.hidePreview && !note.locked"} .html-preview{"ng-if" => "note.content.preview_html", "ng-bind-html" => "note.content.preview_html"} .plain-preview{"ng-if" => "!note.content.preview_html && note.content.preview_plain"} {{note.content.preview_plain}} .default-preview{"ng-if" => "!note.content.preview_html && !note.content.preview_plain"} {{note.text}} From bb8140d89c40fb0190e9f94a581c60d06ec309e7 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 14 Nov 2018 14:06:23 -0600 Subject: [PATCH 05/12] Autolock UI --- .../app/directives/views/accountMenu.js | 5 +- .../app/services/passcodeManager.js | 23 +- .../app/services/privilegesManager.js | 2 +- .../directives/account-menu.html.haml | 6 +- .../privileges-management-modal.html.haml | 19 +- package-lock.json | 882 +++++++++++++++++- package.json | 2 +- 7 files changed, 911 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index 5a6c17450..d7a9c3414 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -380,7 +380,6 @@ class AccountMenu { passcodeManager.getAutoLockInterval().then((interval) => { $timeout(() => { $scope.selectedAutoLockInterval = interval; - console.log("selectedAutoLockInterval", $scope.selectedAutoLockInterval); }) }) } @@ -407,10 +406,10 @@ class AccountMenu { return; } - let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode : passcodeManager.setPasscode; + let fn = $scope.formData.changingPasscode ? passcodeManager.changePasscode.bind(passcodeManager) : passcodeManager.setPasscode.bind(passcodeManager); fn(passcode, () => { - $timeout(function(){ + $timeout(() => { $scope.formData.showPasscodeForm = false; var offline = authManager.offline(); diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js index 0210ee96b..4abd4fba0 100644 --- a/app/assets/javascripts/app/services/passcodeManager.js +++ b/app/assets/javascripts/app/services/passcodeManager.js @@ -1,7 +1,8 @@ class PasscodeManager { constructor(authManager, storageManager) { - document.addEventListener('visibilitychange', () => { + document.addEventListener('visibilitychange', (e) => { + console.log("visibilitychange", e, document.visibilityState); this.documentVisibilityChanged(document.visibilityState); }); @@ -13,6 +14,7 @@ class PasscodeManager { const MillisecondsPerSecond = 1000; PasscodeManager.AutoLockIntervalNone = 0; + PasscodeManager.AutoLockIntervalFiveSecs = 5 * MillisecondsPerSecond; PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond; PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond; PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond; @@ -26,6 +28,10 @@ class PasscodeManager { value: PasscodeManager.AutoLockIntervalNone, label: "None" }, + { + value: PasscodeManager.AutoLockIntervalFiveSecs, + label: "5 Secs" + }, { value: PasscodeManager.AutoLockIntervalOneMinute, label: "1 Min" @@ -51,20 +57,21 @@ class PasscodeManager { } async beginAutoLockTimer() { - console.log("beginAutoLockTimer"); var interval = await this.getAutoLockInterval(); + if(interval == PasscodeManager.AutoLockIntervalNone) { + return; + } + this.lockTimeout = setTimeout(() => { this.lockApplication(); }, interval); } cancelAutoLockTimer() { - console.log("cancelAutoLockTimer"); clearTimeout(this.lockTimeout); } lockApplication() { - console.log("lockApplication"); window.location.reload(); this.cancelAutoLockTimer(); } @@ -82,14 +89,16 @@ class PasscodeManager { } async setAutoLockInterval(interval) { - console.log("Set autolock interval", interval); return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.Fixed); } async getAutoLockInterval() { let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.Fixed); - console.log("Got interval", interval); - return interval && JSON.parse(interval); + if(interval) { + return JSON.parse(interval); + } else { + return PasscodeManager.AutoLockIntervalNone; + } } passcodeAuthParams() { diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index 8736057e4..178034d92 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -83,7 +83,7 @@ class PrivilegesManager { resolve(resolvedSingleton); }, (valueCallback) => { // Safe to create. Create and return object. - var privs = new Privilege({content_type: prefsContentType}); + var privs = new SNPrivileges({content_type: prefsContentType}); this.modelManager.addItem(privs); privs.setDirty(true); this.$rootScope.sync(); diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml index 9733c4a59..ae99c67b7 100644 --- a/app/assets/templates/directives/account-menu.html.haml +++ b/app/assets/templates/directives/account-menu.html.haml @@ -132,10 +132,12 @@ %a.danger{"ng-click" => "removePasscodePressed()"} Remove Passcode .panel-row .horizontal-group - %p Autolock: + %h4 Autolock + .vertical-rule %a.info{"ng-repeat" => "option in passcodeAutoLockOptions", "ng-click" => "selectAutoLockInterval(option.value)", - "ng-class" => "{'info bold' : option.value == selectedAutoLockInterval}"} + "ng-class" => "{'info boxed' : option.value == selectedAutoLockInterval}"} {{option.label}} + %p The autolock timer begins when the window or tab loses focus. .panel-section{"ng-if" => "!importData.loading"} diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml index e4f730e2d..52898a899 100644 --- a/app/assets/templates/directives/privileges-management-modal.html.haml +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -22,12 +22,13 @@ %input{"type" => "checkbox", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"} .footer %h2 About Privileges - %p - Privileges represent interface level authentication for accessing certain items and features. - Note that when your application is unlocked, your data exists in temporary memory in an unencrypted state. - Privileges are meant to protect against unwanted access in the event of an unlocked application, but do not affect data encryption state. - - %p - Privileges sync across your other devices (not including mobile); however, note that if you require - a "Local Passcode" privilege, and another device does not have a local passcode set up, the local passcode - requirement will be ignored on that device. + .panel-section.no-bottom-pad + .text-content + %p + Privileges represent interface level authentication for accessing certain items and features. + Note that when your application is unlocked, your data exists in temporary memory in an unencrypted state. + Privileges are meant to protect against unwanted access in the event of an unlocked application, but do not affect data encryption state. + %p + Privileges sync across your other devices (not including mobile); however, note that if you require + a "Local Passcode" privilege, and another device does not have a local passcode set up, the local passcode + requirement will be ignored on that device. diff --git a/package-lock.json b/package-lock.json index 712f36497..ffdf0d56e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "2.3.16", + "version": "2.3.17", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7728,10 +7728,882 @@ "dev": true }, "sn-stylekit": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/sn-stylekit/-/sn-stylekit-1.0.15.tgz", - "integrity": "sha512-QeWlaCMHtF/VhFWWICzmx39ger92DEj1uLiCW4VVLX9LtU7nKQ5plqHgrpvnctO+wNh9LIYdPBLLWxTwgXm6Eg==", - "dev": true + "version": "file:../Stylekit", + "dev": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "ansi-styles": { + "version": "2.2.1", + "bundled": true + }, + "argparse": { + "version": "1.0.9", + "bundled": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "array-find-index": { + "version": "1.0.2", + "bundled": true + }, + "async": { + "version": "1.5.2", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "body-parser": { + "version": "1.14.2", + "bundled": true, + "requires": { + "bytes": "2.2.0", + "content-type": "~1.0.1", + "debug": "~2.2.0", + "depd": "~1.1.0", + "http-errors": "~1.3.1", + "iconv-lite": "0.4.13", + "on-finished": "~2.3.0", + "qs": "5.2.0", + "raw-body": "~2.1.5", + "type-is": "~1.6.10" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "bundled": true, + "requires": { + "ms": "0.7.1" + } + }, + "iconv-lite": { + "version": "0.4.13", + "bundled": true + }, + "ms": { + "version": "0.7.1", + "bundled": true + }, + "qs": { + "version": "5.2.0", + "bundled": true + } + } + }, + "brace-expansion": { + "version": "1.1.8", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "bundled": true + }, + "bytes": { + "version": "2.2.0", + "bundled": true + }, + "camelcase": { + "version": "2.1.1", + "bundled": true + }, + "camelcase-keys": { + "version": "2.1.0", + "bundled": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "chalk": { + "version": "1.1.3", + "bundled": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "coffee-script": { + "version": "1.10.0", + "bundled": true + }, + "colors": { + "version": "1.1.2", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "content-type": { + "version": "1.0.4", + "bundled": true + }, + "cross-spawn": { + "version": "0.2.9", + "bundled": true, + "requires": { + "lru-cache": "^2.5.0" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "bundled": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "dargs": { + "version": "4.1.0", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "dateformat": { + "version": "1.0.12", + "bundled": true, + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.3.0" + } + }, + "decamelize": { + "version": "1.2.0", + "bundled": true + }, + "depd": { + "version": "1.1.1", + "bundled": true + }, + "ee-first": { + "version": "1.1.1", + "bundled": true + }, + "error-ex": { + "version": "1.3.1", + "bundled": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "bundled": true + }, + "esprima": { + "version": "2.7.3", + "bundled": true + }, + "eventemitter2": { + "version": "0.4.14", + "bundled": true + }, + "exit": { + "version": "0.1.2", + "bundled": true + }, + "faye-websocket": { + "version": "0.10.0", + "bundled": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "find-up": { + "version": "1.1.2", + "bundled": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "findup-sync": { + "version": "0.3.0", + "bundled": true, + "requires": { + "glob": "~5.0.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "bundled": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "gaze": { + "version": "1.1.2", + "bundled": true, + "requires": { + "globule": "^1.0.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "bundled": true + }, + "getobject": { + "version": "0.1.0", + "bundled": true + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globule": { + "version": "1.2.0", + "bundled": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.4", + "minimatch": "~3.0.2" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "grunt": { + "version": "1.0.1", + "bundled": true, + "requires": { + "coffee-script": "~1.10.0", + "dateformat": "~1.0.12", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.3.0", + "glob": "~7.0.0", + "grunt-cli": "~1.2.0", + "grunt-known-options": "~1.1.0", + "grunt-legacy-log": "~1.0.0", + "grunt-legacy-util": "~1.0.0", + "iconv-lite": "~0.4.13", + "js-yaml": "~3.5.2", + "minimatch": "~3.0.0", + "nopt": "~3.0.6", + "path-is-absolute": "~1.0.0", + "rimraf": "~2.2.8" + }, + "dependencies": { + "glob": { + "version": "7.0.6", + "bundled": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "grunt-cli": { + "version": "1.2.0", + "bundled": true, + "requires": { + "findup-sync": "~0.3.0", + "grunt-known-options": "~1.1.0", + "nopt": "~3.0.6", + "resolve": "~1.1.0" + } + } + } + }, + "grunt-contrib-sass": { + "version": "1.0.0", + "bundled": true, + "requires": { + "async": "^0.9.0", + "chalk": "^1.0.0", + "cross-spawn": "^0.2.3", + "dargs": "^4.0.0", + "which": "^1.0.5" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "bundled": true + } + } + }, + "grunt-contrib-watch": { + "version": "1.0.0", + "bundled": true, + "requires": { + "async": "^1.5.0", + "gaze": "^1.0.0", + "lodash": "^3.10.1", + "tiny-lr": "^0.2.1" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "bundled": true + } + } + }, + "grunt-haml2html": { + "version": "0.3.1", + "bundled": true, + "requires": { + "dargs": "~0.1.0" + }, + "dependencies": { + "dargs": { + "version": "0.1.0", + "bundled": true + } + } + }, + "grunt-known-options": { + "version": "1.1.0", + "bundled": true + }, + "grunt-legacy-log": { + "version": "1.0.0", + "bundled": true, + "requires": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~1.0.0", + "hooker": "~0.2.3", + "lodash": "~3.10.1", + "underscore.string": "~3.2.3" + }, + "dependencies": { + "lodash": { + "version": "3.10.1", + "bundled": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "1.0.0", + "bundled": true, + "requires": { + "chalk": "~1.1.1", + "lodash": "~4.3.0" + }, + "dependencies": { + "lodash": { + "version": "4.3.0", + "bundled": true + } + } + }, + "grunt-legacy-util": { + "version": "1.0.0", + "bundled": true, + "requires": { + "async": "~1.5.2", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~4.3.0", + "underscore.string": "~3.2.3", + "which": "~1.2.1" + }, + "dependencies": { + "lodash": { + "version": "4.3.0", + "bundled": true + } + } + }, + "grunt-newer": { + "version": "1.3.0", + "bundled": true, + "requires": { + "async": "^1.5.2", + "rimraf": "^2.5.2" + }, + "dependencies": { + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "^7.0.5" + } + } + } + }, + "has-ansi": { + "version": "2.0.0", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "hooker": { + "version": "0.2.3", + "bundled": true + }, + "hosted-git-info": { + "version": "2.5.0", + "bundled": true + }, + "http-errors": { + "version": "1.3.1", + "bundled": true, + "requires": { + "inherits": "~2.0.1", + "statuses": "1" + } + }, + "http-parser-js": { + "version": "0.4.9", + "bundled": true + }, + "iconv-lite": { + "version": "0.4.19", + "bundled": true + }, + "indent-string": { + "version": "2.1.0", + "bundled": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "is-arrayish": { + "version": "0.2.1", + "bundled": true + }, + "is-builtin-module": { + "version": "1.0.0", + "bundled": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-finite": { + "version": "1.0.2", + "bundled": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-utf8": { + "version": "0.2.1", + "bundled": true + }, + "isexe": { + "version": "2.0.0", + "bundled": true + }, + "js-yaml": { + "version": "3.5.5", + "bundled": true, + "requires": { + "argparse": "^1.0.2", + "esprima": "^2.6.0" + } + }, + "livereload-js": { + "version": "2.2.2", + "bundled": true + }, + "load-json-file": { + "version": "1.1.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.4", + "bundled": true + }, + "loud-rejection": { + "version": "1.6.0", + "bundled": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "2.7.3", + "bundled": true + }, + "map-obj": { + "version": "1.0.1", + "bundled": true + }, + "media-typer": { + "version": "0.3.0", + "bundled": true + }, + "meow": { + "version": "3.7.0", + "bundled": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "mime-db": { + "version": "1.30.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.17", + "bundled": true, + "requires": { + "mime-db": "~1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "nopt": { + "version": "3.0.6", + "bundled": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "bundled": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "on-finished": { + "version": "2.3.0", + "bundled": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1" + } + }, + "parse-json": { + "version": "2.2.0", + "bundled": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "parseurl": { + "version": "1.3.2", + "bundled": true + }, + "path-exists": { + "version": "2.1.0", + "bundled": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "path-type": { + "version": "1.1.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "bundled": true + }, + "pinkie": { + "version": "2.0.4", + "bundled": true + }, + "pinkie-promise": { + "version": "2.0.1", + "bundled": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "qs": { + "version": "5.1.0", + "bundled": true + }, + "raw-body": { + "version": "2.1.7", + "bundled": true, + "requires": { + "bytes": "2.4.0", + "iconv-lite": "0.4.13", + "unpipe": "1.0.0" + }, + "dependencies": { + "bytes": { + "version": "2.4.0", + "bundled": true + }, + "iconv-lite": { + "version": "0.4.13", + "bundled": true + } + } + }, + "read-pkg": { + "version": "1.1.0", + "bundled": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "bundled": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "redent": { + "version": "1.0.0", + "bundled": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "repeating": { + "version": "2.0.1", + "bundled": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "resolve": { + "version": "1.1.7", + "bundled": true + }, + "rimraf": { + "version": "2.2.8", + "bundled": true + }, + "semver": { + "version": "5.4.1", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "spdx-correct": { + "version": "1.0.2", + "bundled": true, + "requires": { + "spdx-license-ids": "^1.0.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "bundled": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "bundled": true + }, + "sprintf-js": { + "version": "1.0.3", + "bundled": true + }, + "statuses": { + "version": "1.4.0", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "bundled": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "bundled": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "bundled": true + }, + "tiny-lr": { + "version": "0.2.1", + "bundled": true, + "requires": { + "body-parser": "~1.14.0", + "debug": "~2.2.0", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.2.0", + "parseurl": "~1.3.0", + "qs": "~5.1.0" + }, + "dependencies": { + "debug": { + "version": "2.2.0", + "bundled": true, + "requires": { + "ms": "0.7.1" + } + }, + "ms": { + "version": "0.7.1", + "bundled": true + } + } + }, + "trim-newlines": { + "version": "1.0.0", + "bundled": true + }, + "type-is": { + "version": "1.6.15", + "bundled": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.15" + } + }, + "underscore.string": { + "version": "3.2.3", + "bundled": true + }, + "unpipe": { + "version": "1.0.0", + "bundled": true + }, + "validate-npm-package-license": { + "version": "3.0.1", + "bundled": true, + "requires": { + "spdx-correct": "~1.0.0", + "spdx-expression-parse": "~1.0.0" + } + }, + "websocket-driver": { + "version": "0.7.0", + "bundled": true, + "requires": { + "http-parser-js": ">=0.4.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.2", + "bundled": true + }, + "which": { + "version": "1.2.14", + "bundled": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } }, "snake-case": { "version": "2.1.0", diff --git a/package.json b/package.json index 0528a49ef..9436a7079 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "mocha": "^5.2.0", "serve-static": "^1.13.2", "sn-models": "0.1.9", - "sn-stylekit": "1.0.15", + "sn-stylekit": "file:~/Desktop/sn/dev/Stylekit", "standard-file-js": "0.3.19" } } From fb6b5fa983d74c5e489684388078082308043fa6 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Thu, 15 Nov 2018 14:41:04 -0600 Subject: [PATCH 06/12] Privs remember me for wip --- .../javascripts/app/controllers/footer.js | 3 +- .../directives/views/privilegesAuthModal.js | 12 +++ .../app/services/passcodeManager.js | 4 +- .../app/services/privilegesManager.js | 81 ++++++++++++++++++- .../privileges-auth-modal.html.haml | 6 ++ 5 files changed, 100 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/app/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js index 47ac00b76..5aeb5b176 100644 --- a/app/assets/javascripts/app/controllers/footer.js +++ b/app/assets/javascripts/app/controllers/footer.js @@ -182,11 +182,12 @@ angular.module('app') if(!room.showRoom) { // About to show, check if has privileges - if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageExtensions)) { privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageExtensions, () => { run(); }); + } else { + run(); } } else { run(); diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js index 99dad564d..072741c34 100644 --- a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js @@ -25,6 +25,17 @@ class PrivilegesAuthModal { 'ngInject'; $scope.authenticationParameters = {}; + $scope.sessionLengthOptions = privilegesManager.getSessionLengthOptions(); + + privilegesManager.getSelectedSessionLength().then((length) => { + $timeout(() => { + $scope.selectedSessionLength = length; + }) + }) + + $scope.selectSessionLength = function(length) { + $scope.selectedSessionLength = length; + } privilegesManager.getPrivileges().then((privileges) => { $timeout(() => { @@ -55,6 +66,7 @@ class PrivilegesAuthModal { privilegesManager.authenticateAction($scope.action, $scope.authenticationParameters).then((result) => { $timeout(() => { if(result.success) { + privilegesManager.setSessionLength($scope.selectedSessionLength); $scope.onSuccess(); $scope.dismiss(); } else { diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js index 4abd4fba0..188a1aee7 100644 --- a/app/assets/javascripts/app/services/passcodeManager.js +++ b/app/assets/javascripts/app/services/passcodeManager.js @@ -89,11 +89,11 @@ class PasscodeManager { } async setAutoLockInterval(interval) { - return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.Fixed); + return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted); } async getAutoLockInterval() { - let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.Fixed); + let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.FixedEncrypted); if(interval) { return JSON.parse(interval); } else { diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index 178034d92..8b77653af 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -1,10 +1,11 @@ class PrivilegesManager { - constructor(passcodeManager, authManager, singletonManager, modelManager, $rootScope, $compile) { + constructor(passcodeManager, authManager, singletonManager, modelManager, storageManager, $rootScope, $compile) { this.passcodeManager = passcodeManager; this.authManager = authManager; this.singletonManager = singletonManager; this.modelManager = modelManager; + this.storageManager = storageManager; this.$rootScope = $rootScope; this.$compile = $compile; @@ -18,6 +19,14 @@ class PrivilegesManager { PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes"; PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges"; + PrivilegesManager.SessionExpiresAtKey = "SessionExpiresAtKey"; + PrivilegesManager.SessionLengthKey = "SessionLengthKey"; + + PrivilegesManager.SessionLengthNone = 0; + PrivilegesManager.SessionLengthFiveMinutes = 5; + PrivilegesManager.SessionLengthOneHour = 3600; + PrivilegesManager.SessionLengthOneWeek = 604800; + this.availableActions = [ PrivilegesManager.ActionManageExtensions, PrivilegesManager.ActionDownloadBackup, @@ -29,6 +38,14 @@ class PrivilegesManager { PrivilegesManager.CredentialAccountPassword, PrivilegesManager.CredentialLocalPasscode ]; + + this.sessionLengths = [ + PrivilegesManager.SessionLengthNone, + PrivilegesManager.SessionLengthFiveMinutes, + PrivilegesManager.SessionLengthOneHour, + PrivilegesManager.SessionLengthOneWeek, + PrivilegesManager.SessionLengthIndefinite + ] } getAvailableActions() { @@ -122,7 +139,7 @@ class PrivilegesManager { metadata[PrivilegesManager.ActionManageExtensions] = { label: "Manage Extensions" - } + }; metadata[PrivilegesManager.ActionDownloadBackup] = { label: "Download Backups" @@ -134,12 +151,70 @@ class PrivilegesManager { metadata[PrivilegesManager.ActionManagePrivileges] = { label: "Manage Privileges" - } + }; return metadata[action]; } + getSessionLengthOptions() { + return [ + { + value: PrivilegesManager.SessionLengthNone, + label: "Don't Remember" + }, + { + value: PrivilegesManager.SessionLengthFiveMinutes, + label: "5 Min" + }, + { + value: PrivilegesManager.SessionLengthOneHour, + label: "1 Hr" + }, + { + value: PrivilegesManager.SessionLengthOneWeek, + label: "1 Week" + } + ] + } + + async setSessionLength(length) { + let addToNow = (seconds) => { + let date = new Date(); + date.setSeconds(date.getSeconds() + seconds); + return date; + } + + let expiresAt = addToNow(length); + + return Promise.all([ + this.storageManager.setItem(PrivilegesManager.SessionExpiresAtKey, JSON.stringify(expiresAt), StorageManager.FixedEncrypted), + this.storageManager.setItem(PrivilegesManager.SessionLengthKey, JSON.stringify(length), StorageManager.FixedEncrypted), + ]) + } + + async getSelectedSessionLength() { + let length = await this.storageManager.getItem(PrivilegesManager.SessionLengthKey, StorageManager.FixedEncrypted); + if(length) { + return JSON.parse(length); + } else { + return PrivilegesManager.SessionLengthNone; + } + } + + async getSessionExpirey() { + let expiresAt = await this.storageManager.getItem(PrivilegesManager.SessionExpiresAtKey, StorageManager.FixedEncrypted); + if(expiresAt) { + return new Date(JSON.parse(expiresAt)); + } else { + return new Date(); + } + } + async actionRequiresPrivilege(action) { + let expiresAt = await this.getSessionExpirey(); + if(expiresAt > new Date()) { + return false; + } return (await this.getPrivileges()).getCredentialsForAction(action).length > 0; } diff --git a/app/assets/templates/directives/privileges-auth-modal.html.haml b/app/assets/templates/directives/privileges-auth-modal.html.haml index 2603614a0..fd9074df3 100644 --- a/app/assets/templates/directives/privileges-auth-modal.html.haml +++ b/app/assets/templates/directives/privileges-auth-modal.html.haml @@ -12,6 +12,12 @@ %p {{promptForCredential(credential)}} %input{"type" => "password", "ng-model" => "authenticationParameters[credential]"} %label.danger{"ng-if" => "isCredentialInFailureState(credential)"} Invalid authentication. Please try again. + .panel-row + .horizontal-group + %h4 Remember for + %a.info{"ng-repeat" => "option in sessionLengthOptions", "ng-click" => "selectSessionLength(option.value)", + "ng-class" => "{'info boxed' : option.value == selectedSessionLength}"} + {{option.label}} .footer .button.info.big.block.bold{"ng-click" => "submit()"} Submit From 16296e7f80ecf33c637eebca3378d1d998022c8a Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Mon, 19 Nov 2018 14:52:02 -0600 Subject: [PATCH 07/12] Privileges wip --- .../directives/views/privilegesAuthModal.js | 9 +++---- .../app/services/privilegesManager.js | 25 +++++++++++++++++-- .../privileges-auth-modal.html.haml | 15 +++++++---- 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js index 072741c34..84452687a 100644 --- a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js @@ -21,7 +21,7 @@ class PrivilegesAuthModal { } } - controller($scope, privilegesManager, $timeout) { + controller($scope, privilegesManager, passcodeManager, authManager, $timeout) { 'ngInject'; $scope.authenticationParameters = {}; @@ -37,11 +37,10 @@ class PrivilegesAuthModal { $scope.selectedSessionLength = length; } - privilegesManager.getPrivileges().then((privileges) => { + privilegesManager.netCredentialsForAction($scope.action).then((credentials) => { $timeout(() => { - $scope.privileges = privileges; - $scope.requiredCredentials = privileges.getCredentialsForAction($scope.action); - }) + $scope.requiredCredentials = credentials; + }); }); $scope.promptForCredential = function(credential) { diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index 8b77653af..460b23fa3 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -78,6 +78,27 @@ class PrivilegesManager { this.currentAuthenticationElement = el; } + async netCredentialsForAction(action) { + let credentials = (await this.getPrivileges()).getCredentialsForAction(action); + let netCredentials = []; + + for(var cred of credentials) { + if(cred == PrivilegesManager.CredentialAccountPassword) { + if(!this.authManager.offline()) { + netCredentials.push(cred); + } else { + console.log("WE ARE OFFLINE"); + } + } else if(cred == PrivilegesManager.CredentialLocalPasscode) { + if(this.passcodeManager.hasPasscode()) { + netCredentials.push(cred); + } + } + } + + return netCredentials; + } + presentPrivilegesManagementModal() { var scope = this.$rootScope.$new(true); var el = this.$compile( "")(scope); @@ -215,7 +236,7 @@ class PrivilegesManager { if(expiresAt > new Date()) { return false; } - return (await this.getPrivileges()).getCredentialsForAction(action).length > 0; + return (await this.netCredentialsForAction(action)).length > 0; } async savePrivileges() { @@ -225,7 +246,7 @@ class PrivilegesManager { } async authenticateAction(action, credentialAuthMapping) { - var requiredCredentials = (await this.getPrivileges()).getCredentialsForAction(action); + var requiredCredentials = (await this.netCredentialsForAction(action)); var successfulCredentials = [], failedCredentials = []; for(let requiredCredential of requiredCredentials) { diff --git a/app/assets/templates/directives/privileges-auth-modal.html.haml b/app/assets/templates/directives/privileges-auth-modal.html.haml index fd9074df3..972a091f6 100644 --- a/app/assets/templates/directives/privileges-auth-modal.html.haml +++ b/app/assets/templates/directives/privileges-auth-modal.html.haml @@ -8,13 +8,18 @@ %a.close-button.info{"ng-click" => "cancel()"} Cancel .content .panel-section - .panel-row{"ng-repeat" => "credential in requiredCredentials"} - %p {{promptForCredential(credential)}} - %input{"type" => "password", "ng-model" => "authenticationParameters[credential]"} - %label.danger{"ng-if" => "isCredentialInFailureState(credential)"} Invalid authentication. Please try again. + %div{"ng-repeat" => "credential in requiredCredentials"} + %p + %strong {{promptForCredential(credential)}} + %div + %input{"type" => "password", "ng-model" => "authenticationParameters[credential]"} + %div + %label.danger{"ng-if" => "isCredentialInFailureState(credential)"} Invalid authentication. Please try again. + .panel-row .panel-row .horizontal-group - %h4 Remember for + %p + %strong Remember for %a.info{"ng-repeat" => "option in sessionLengthOptions", "ng-click" => "selectSessionLength(option.value)", "ng-class" => "{'info boxed' : option.value == selectedSessionLength}"} {{option.label}} From c5d50728c09a9c56444b5d6bfb16cfe4fa210c83 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 20 Nov 2018 13:13:58 -0600 Subject: [PATCH 08/12] Session clearing, more actions --- .../javascripts/app/controllers/editor.js | 31 +++++-- .../app/directives/views/accountMenu.js | 92 ++++++++++++++----- .../app/directives/views/menuRow.js | 1 + .../views/privilegesManagementModal.js | 19 +++- .../app/services/passcodeManager.js | 1 - .../app/services/privilegesManager.js | 26 ++++-- app/assets/stylesheets/app/_modals.scss | 4 + .../templates/directives/menu-row.html.haml | 5 +- .../privileges-management-modal.html.haml | 4 + app/assets/templates/editor.html.haml | 2 +- 10 files changed, 144 insertions(+), 41 deletions(-) diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js index 1edba4c77..56e3d93aa 100644 --- a/app/assets/javascripts/app/controllers/editor.js +++ b/app/assets/javascripts/app/controllers/editor.js @@ -22,7 +22,8 @@ angular.module('app') } } }) - .controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory) { + .controller('EditorCtrl', function ($sce, $timeout, authManager, $rootScope, actionsManager, + syncManager, modelManager, themeManager, componentManager, storageManager, sessionHistory, privilegesManager) { this.spellcheck = true; this.componentManager = componentManager; @@ -382,16 +383,28 @@ angular.module('app') } } - this.deleteNote = function() { - if(this.note.locked) { - alert("This note is locked. If you'd like to delete it, unlock it, and try again."); - return; + this.deleteNote = async function() { + let run = () => { + $timeout(() => { + if(this.note.locked) { + alert("This note is locked. If you'd like to delete it, unlock it, and try again."); + return; + } + + let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note"; + if(confirm(`Are you sure you want to delete ${title}?`)) { + this.remove()(this.note); + this.showMenu = false; + } + }); } - let title = this.note.safeTitle().length ? `'${this.note.title}'` : "this note"; - if(confirm(`Are you sure you want to delete ${title}?`)) { - this.remove()(this.note); - this.showMenu = false; + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDeleteNote)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDeleteNote, () => { + run(); + }); + } else { + run(); } } diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index d7a9c3414..f4df268a3 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -169,8 +169,20 @@ class AccountMenu { authManager.presentPasswordWizard(type); } - $scope.openPrivilegesModal = function() { - privilegesManager.presentPrivilegesManagementModal(); + $scope.openPrivilegesModal = async function() { + let run = () => { + $timeout(() => { + privilegesManager.presentPrivilegesManagementModal(); + }) + } + + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePrivileges)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePrivileges, () => { + run(); + }); + } else { + run(); + } } // Allows indexeddb unencrypted logs to be deleted @@ -387,8 +399,20 @@ class AccountMenu { $scope.reloadAutoLockInterval(); $scope.selectAutoLockInterval = async function(interval) { - await passcodeManager.setAutoLockInterval(interval); - $scope.reloadAutoLockInterval(); + let run = async () => { + await passcodeManager.setAutoLockInterval(interval); + $timeout(() => { + $scope.reloadAutoLockInterval(); + }); + } + + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => { + run(); + }); + } else { + run(); + } } $scope.hasPasscode = function() { @@ -422,27 +446,51 @@ class AccountMenu { }) } - $scope.changePasscodePressed = function() { - $scope.formData.changingPasscode = true; - $scope.addPasscodeClicked(); - $scope.formData.changingPasscode = false; + $scope.changePasscodePressed = async function() { + let run = () => { + $timeout(() => { + $scope.formData.changingPasscode = true; + $scope.addPasscodeClicked(); + $scope.formData.changingPasscode = false; + }) + } + + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => { + run(); + }); + } else { + run(); + } } - $scope.removePasscodePressed = function() { - var signedIn = !authManager.offline(); - var message = "Are you sure you want to remove your local passcode?"; - if(!signedIn) { - message += " This will remove encryption from your local data."; - } - if(confirm(message)) { - passcodeManager.clearPasscode(); + $scope.removePasscodePressed = async function() { + let run = () => { + $timeout(() => { + var signedIn = !authManager.offline(); + var message = "Are you sure you want to remove your local passcode?"; + if(!signedIn) { + message += " This will remove encryption from your local data."; + } + if(confirm(message)) { + passcodeManager.clearPasscode(); - if(authManager.offline()) { - syncManager.markAllItemsDirtyAndSaveOffline(); - // Don't create backup here, as if the user is temporarily removing the passcode to change it, - // we don't want to write unencrypted data to disk. - // $rootScope.$broadcast("major-data-change"); - } + if(authManager.offline()) { + syncManager.markAllItemsDirtyAndSaveOffline(); + // Don't create backup here, as if the user is temporarily removing the passcode to change it, + // we don't want to write unencrypted data to disk. + // $rootScope.$broadcast("major-data-change"); + } + } + }) + } + + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => { + run(); + }); + } else { + run(); } } diff --git a/app/assets/javascripts/app/directives/views/menuRow.js b/app/assets/javascripts/app/directives/views/menuRow.js index 999e64f9f..49fbb5e0a 100644 --- a/app/assets/javascripts/app/directives/views/menuRow.js +++ b/app/assets/javascripts/app/directives/views/menuRow.js @@ -7,6 +7,7 @@ class MenuRow { this.scope = { action: "&", circle: "=", + circleAlign: "=", label: "=", subtitle: "=", hasButton: "=", diff --git a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js index b792d3858..d646cb48c 100644 --- a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js @@ -28,13 +28,30 @@ class PrivilegesManagementModal { } $scope.isCredentialRequiredForAction = function(action, credential) { + if(!$scope.privileges) { + return false; + } return $scope.privileges.isCredentialRequiredForAction(action, credential); } + $scope.clearSession = function() { + privilegesManager.clearSession().then(() => { + $scope.reloadPrivileges(); + }) + } + $scope.reloadPrivileges = async function() { - $scope.privileges = await privilegesManager.getPrivileges(); $scope.availableActions = privilegesManager.getAvailableActions(); $scope.availableCredentials = privilegesManager.getAvailableCredentials(); + let sessionEndDate = await privilegesManager.getSessionExpirey(); + $scope.sessionExpirey = sessionEndDate.toLocaleString(); + $scope.sessionExpired = new Date() >= sessionEndDate; + + privilegesManager.getPrivileges().then((privs) => { + $timeout(() => { + $scope.privileges = privs; + }) + }) } $scope.checkboxValueChanged = function(action, credential) { diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js index 188a1aee7..1aa1f7c5e 100644 --- a/app/assets/javascripts/app/services/passcodeManager.js +++ b/app/assets/javascripts/app/services/passcodeManager.js @@ -2,7 +2,6 @@ class PasscodeManager { constructor(authManager, storageManager) { document.addEventListener('visibilitychange', (e) => { - console.log("visibilitychange", e, document.visibilityState); this.documentVisibilityChanged(document.visibilityState); }); diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index 460b23fa3..daed765ab 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -18,12 +18,14 @@ class PrivilegesManager { PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup"; PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes"; PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges"; + PrivilegesManager.ActionManagePasscode = "ActionManagePasscode"; + PrivilegesManager.ActionDeleteNote = "ActionDeleteNote"; PrivilegesManager.SessionExpiresAtKey = "SessionExpiresAtKey"; PrivilegesManager.SessionLengthKey = "SessionLengthKey"; PrivilegesManager.SessionLengthNone = 0; - PrivilegesManager.SessionLengthFiveMinutes = 5; + PrivilegesManager.SessionLengthFiveMinutes = 300; PrivilegesManager.SessionLengthOneHour = 3600; PrivilegesManager.SessionLengthOneWeek = 604800; @@ -31,7 +33,9 @@ class PrivilegesManager { PrivilegesManager.ActionManageExtensions, PrivilegesManager.ActionDownloadBackup, PrivilegesManager.ActionViewLockedNotes, - PrivilegesManager.ActionManagePrivileges + PrivilegesManager.ActionManagePrivileges, + PrivilegesManager.ActionManagePasscode, + PrivilegesManager.ActionDeleteNote ] this.availableCredentials = [ @@ -86,8 +90,6 @@ class PrivilegesManager { if(cred == PrivilegesManager.CredentialAccountPassword) { if(!this.authManager.offline()) { netCredentials.push(cred); - } else { - console.log("WE ARE OFFLINE"); } } else if(cred == PrivilegesManager.CredentialLocalPasscode) { if(this.passcodeManager.hasPasscode()) { @@ -174,6 +176,14 @@ class PrivilegesManager { label: "Manage Privileges" }; + metadata[PrivilegesManager.ActionManagePasscode] = { + label: "Manage Passcode" + } + + metadata[PrivilegesManager.ActionDeleteNote] = { + label: "Delete Note" + } + return metadata[action]; } @@ -185,11 +195,11 @@ class PrivilegesManager { }, { value: PrivilegesManager.SessionLengthFiveMinutes, - label: "5 Min" + label: "5 Minutes" }, { value: PrivilegesManager.SessionLengthOneHour, - label: "1 Hr" + label: "1 Hour" }, { value: PrivilegesManager.SessionLengthOneWeek, @@ -213,6 +223,10 @@ class PrivilegesManager { ]) } + async clearSession() { + return this.setSessionLength(PrivilegesManager.SessionLengthNone); + } + async getSelectedSessionLength() { let length = await this.storageManager.getItem(PrivilegesManager.SessionLengthKey, StorageManager.FixedEncrypted); if(length) { diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss index 6d6278cbd..074bb4db9 100644 --- a/app/assets/stylesheets/app/_modals.scss +++ b/app/assets/stylesheets/app/_modals.scss @@ -14,6 +14,10 @@ #privileges-modal { width: 700px; + + th { + text-align: left; + } } #password-wizard { diff --git a/app/assets/templates/directives/menu-row.html.haml b/app/assets/templates/directives/menu-row.html.haml index 0cb3cf604..9cdaff83e 100644 --- a/app/assets/templates/directives/menu-row.html.haml +++ b/app/assets/templates/directives/menu-row.html.haml @@ -1,7 +1,7 @@ .row{"ng-attr-title" => "{{desc}}", "ng-click" => "onClick($event)"} .column .left - .column{"ng-if" => "circle"} + .column{"ng-if" => "circle && (!circleAlign || circleAlign == 'left')"} .circle.small{"ng-class" => "circle"} .column{"ng-class" => "{'faded' : faded || disabled}"} .label @@ -13,6 +13,9 @@ %menu-row{"ng-repeat" => "row in subRows", "action" => "row.onClick()", "label" => "row.label", "subtitle" => "row.subtitle", "spinner-class" => "row.spinnerClass"} + .column{"ng-if" => "circle && circleAlign == 'right'"} + .circle.small{"ng-class" => "circle"} + .column{"ng-if" => "hasButton"} .button.info{"ng-click" => "clickButton($event)", "ng-class" => "buttonClass"} {{buttonText}} diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml index 52898a899..1a7ce94d1 100644 --- a/app/assets/templates/directives/privileges-management-modal.html.haml +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -20,6 +20,10 @@ %p {{displayInfoForAction(action)}} %th{"ng-repeat" => "credential in availableCredentials"} %input{"type" => "checkbox", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"} + + .panel-section{"ng-if" => "sessionExpirey && !sessionExpired"} + %p You will not be asked to authenticate until {{sessionExpirey}}. + %a{"ng-click" => "clearSession()"} Clear Session .footer %h2 About Privileges .panel-section.no-bottom-pad diff --git a/app/assets/templates/editor.html.haml b/app/assets/templates/editor.html.haml index d78f5eca9..332fe91f8 100644 --- a/app/assets/templates/editor.html.haml +++ b/app/assets/templates/editor.html.haml @@ -34,7 +34,7 @@ %menu-row{"label" => "ctrl.note.pinned ? 'Unpin' : 'Pin'", "action" => "ctrl.selectedMenuItem(true); ctrl.togglePin()", "desc" => "'Pin or unpin a note from the top of your list'"} %menu-row{"label" => "ctrl.note.archived ? 'Unarchive' : 'Archive'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleArchiveNote()", "desc" => "'Archive or unarchive a note from your Archived system tag'"} %menu-row{"label" => "ctrl.note.locked ? 'Unlock' : 'Lock'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleLockNote()", "desc" => "'Locking notes prevents unintentional editing'"} - %menu-row{"label" => "ctrl.note.content.hidePreview ? 'Unhide Preview' : 'Hide Preview'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()", "desc" => "'Hide or unhide the note preview from the list of notes'"} + %menu-row{"label" => "'Preview'", "circle" => "ctrl.note.content.hidePreview ? 'danger' : 'success'", "circle-align" => "'right'", "action" => "ctrl.selectedMenuItem(true); ctrl.toggleNotePreview()", "desc" => "'Hide or unhide the note preview from the list of notes'"} %menu-row{"label" => "'Delete'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "desc" => "'Delete this note permanently from all your devices'"} .section From 90793938f76291e7a791221f517ee877151d27bc Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Wed, 21 Nov 2018 14:27:04 -0600 Subject: [PATCH 09/12] Privs progress --- .../app/directives/views/accountMenu.js | 63 +++++++++++-------- .../directives/views/privilegesAuthModal.js | 2 +- .../views/privilegesManagementModal.js | 19 +++++- .../app/services/passcodeManager.js | 5 -- .../app/services/privilegesManager.js | 12 ++-- app/assets/stylesheets/app/_modals.scss | 1 + .../directives/account-menu.html.haml | 1 - .../privileges-auth-modal.html.haml | 4 +- .../privileges-management-modal.html.haml | 3 +- 9 files changed, 67 insertions(+), 43 deletions(-) diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index f4df268a3..d984c28f8 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -243,36 +243,49 @@ class AccountMenu { }) } - $scope.importFileSelected = function(files) { - $scope.importData = {}; + $scope.importFileSelected = async function(files) { - var file = files[0]; - var reader = new FileReader(); - reader.onload = function(e) { - try { - var data = JSON.parse(e.target.result); - $timeout(function(){ - if(data.auth_params) { - // request password - $scope.importData.requestPassword = true; - $scope.importData.data = data; + let run = () => { + $timeout(() => { + $scope.importData = {}; - $timeout(() => { - var element = document.getElementById("import-password-request"); - if(element) { - element.scrollIntoView(false); + var file = files[0]; + var reader = new FileReader(); + reader.onload = function(e) { + try { + var data = JSON.parse(e.target.result); + $timeout(function(){ + if(data.auth_params) { + // request password + $scope.importData.requestPassword = true; + $scope.importData.data = data; + + $timeout(() => { + var element = document.getElementById("import-password-request"); + if(element) { + element.scrollIntoView(false); + } + }) + } else { + $scope.performImport(data, null); } }) - } else { - $scope.performImport(data, null); + } catch (e) { + alert("Unable to open file. Ensure it is a proper JSON file and try again."); } - }) - } catch (e) { - alert("Unable to open file. Ensure it is a proper JSON file and try again."); - } + } + + reader.readAsText(file); + }) } - reader.readAsText(file); + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => { + run(); + }); + } else { + run(); + } } $scope.importJSONData = function(data, password, callback) { @@ -335,8 +348,8 @@ class AccountMenu { archiveManager.downloadBackup($scope.archiveFormData.encrypted); } - if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDownloadBackup)) { - privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDownloadBackup, () => { + if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) { + privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => { run(); }); } else { diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js index 84452687a..91816b1b8 100644 --- a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js @@ -39,7 +39,7 @@ class PrivilegesAuthModal { privilegesManager.netCredentialsForAction($scope.action).then((credentials) => { $timeout(() => { - $scope.requiredCredentials = credentials; + $scope.requiredCredentials = credentials.sort(); }); }); diff --git a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js index d646cb48c..c9642b357 100644 --- a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js @@ -14,13 +14,22 @@ class PrivilegesManagementModal { } } - controller($scope, privilegesManager, $timeout) { + controller($scope, privilegesManager, passcodeManager, $timeout) { 'ngInject'; $scope.dummy = {}; + $scope.hasPasscode = passcodeManager.hasPasscode(); + $scope.displayInfoForCredential = function(credential) { - return privilegesManager.displayInfoForCredential(credential).label; + let info = privilegesManager.displayInfoForCredential(credential); + if(credential == PrivilegesManager.CredentialLocalPasscode) { + info["availability"] = $scope.hasPasscode; + } else { + info["availability"] = true; + } + + return info; } $scope.displayInfoForAction = function(action) { @@ -47,6 +56,11 @@ class PrivilegesManagementModal { $scope.sessionExpirey = sessionEndDate.toLocaleString(); $scope.sessionExpired = new Date() >= sessionEndDate; + $scope.credentialDisplayInfo = {}; + for(let cred of $scope.availableCredentials) { + $scope.credentialDisplayInfo[cred] = $scope.displayInfoForCredential(cred); + } + privilegesManager.getPrivileges().then((privs) => { $timeout(() => { $scope.privileges = privs; @@ -55,6 +69,7 @@ class PrivilegesManagementModal { } $scope.checkboxValueChanged = function(action, credential) { + console.log("toggleCredentialForAction", action, credential); $scope.privileges.toggleCredentialForAction(action, credential); privilegesManager.savePrivileges(); } diff --git a/app/assets/javascripts/app/services/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js index 1aa1f7c5e..d203f5aba 100644 --- a/app/assets/javascripts/app/services/passcodeManager.js +++ b/app/assets/javascripts/app/services/passcodeManager.js @@ -13,7 +13,6 @@ class PasscodeManager { const MillisecondsPerSecond = 1000; PasscodeManager.AutoLockIntervalNone = 0; - PasscodeManager.AutoLockIntervalFiveSecs = 5 * MillisecondsPerSecond; PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond; PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond; PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond; @@ -27,10 +26,6 @@ class PasscodeManager { value: PasscodeManager.AutoLockIntervalNone, label: "None" }, - { - value: PasscodeManager.AutoLockIntervalFiveSecs, - label: "5 Secs" - }, { value: PasscodeManager.AutoLockIntervalOneMinute, label: "1 Min" diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index daed765ab..a2869ead4 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -15,7 +15,7 @@ class PrivilegesManager { PrivilegesManager.CredentialLocalPasscode = "CredentialLocalPasscode"; PrivilegesManager.ActionManageExtensions = "ActionManageExtensions"; - PrivilegesManager.ActionDownloadBackup = "ActionDownloadBackup"; + PrivilegesManager.ActionManageBackups = "ActionManageBackups"; PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes"; PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges"; PrivilegesManager.ActionManagePasscode = "ActionManagePasscode"; @@ -31,10 +31,10 @@ class PrivilegesManager { this.availableActions = [ PrivilegesManager.ActionManageExtensions, - PrivilegesManager.ActionDownloadBackup, - PrivilegesManager.ActionViewLockedNotes, + PrivilegesManager.ActionManageBackups, PrivilegesManager.ActionManagePrivileges, PrivilegesManager.ActionManagePasscode, + PrivilegesManager.ActionViewLockedNotes, PrivilegesManager.ActionDeleteNote ] @@ -164,8 +164,8 @@ class PrivilegesManager { label: "Manage Extensions" }; - metadata[PrivilegesManager.ActionDownloadBackup] = { - label: "Download Backups" + metadata[PrivilegesManager.ActionManageBackups] = { + label: "Download/Import Backups" }; metadata[PrivilegesManager.ActionViewLockedNotes] = { @@ -181,7 +181,7 @@ class PrivilegesManager { } metadata[PrivilegesManager.ActionDeleteNote] = { - label: "Delete Note" + label: "Delete Notes" } return metadata[action]; diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss index 074bb4db9..e777e752f 100644 --- a/app/assets/stylesheets/app/_modals.scss +++ b/app/assets/stylesheets/app/_modals.scss @@ -17,6 +17,7 @@ th { text-align: left; + font-weight: normal; } } diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml index ae99c67b7..81ef1ce88 100644 --- a/app/assets/templates/directives/account-menu.html.haml +++ b/app/assets/templates/directives/account-menu.html.haml @@ -133,7 +133,6 @@ .panel-row .horizontal-group %h4 Autolock - .vertical-rule %a.info{"ng-repeat" => "option in passcodeAutoLockOptions", "ng-click" => "selectAutoLockInterval(option.value)", "ng-class" => "{'info boxed' : option.value == selectedAutoLockInterval}"} {{option.label}} diff --git a/app/assets/templates/directives/privileges-auth-modal.html.haml b/app/assets/templates/directives/privileges-auth-modal.html.haml index 972a091f6..3be9f08d5 100644 --- a/app/assets/templates/directives/privileges-auth-modal.html.haml +++ b/app/assets/templates/directives/privileges-auth-modal.html.haml @@ -7,12 +7,12 @@ %h1.title Authentication Required %a.close-button.info{"ng-click" => "cancel()"} Cancel .content - .panel-section + %form.panel-section{"ng-submit" => "submit()"} %div{"ng-repeat" => "credential in requiredCredentials"} %p %strong {{promptForCredential(credential)}} %div - %input{"type" => "password", "ng-model" => "authenticationParameters[credential]"} + %input{"type" => "password", "ng-model" => "authenticationParameters[credential]", "sn-autofocus" => "true", "should-focus" => "$index == 0"} %div %label.danger{"ng-if" => "isCredentialInFailureState(credential)"} Invalid authentication. Please try again. .panel-row diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml index 1a7ce94d1..ee9dac0ae 100644 --- a/app/assets/templates/directives/privileges-management-modal.html.haml +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -13,7 +13,8 @@ %tr %th %th{"ng-repeat" => "cred in availableCredentials"} - {{displayInfoForCredential(cred)}} + %strong {{credentialDisplayInfo[cred].label}} + %p.font-small{"style" => "margin-top: 2px", "ng-show" => "!credentialDisplayInfo[cred].availability"} Not Configured %tbody %tr{"ng-repeat" => "action in availableActions"} %td From 0cab8c29551bee5f8469937e474bd084973c5b0c Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Sun, 25 Nov 2018 18:35:51 -0600 Subject: [PATCH 10/12] Wip --- app/assets/javascripts/app/controllers/home.js | 12 ++++++------ .../app/directives/functional/snEnter.js | 15 +++++++++++++++ .../app/directives/views/accountMenu.js | 2 ++ .../app/directives/views/privilegesAuthModal.js | 16 ++++++++++++++++ .../javascripts/app/services/authManager.js | 1 + .../app/services/privilegesManager.js | 2 ++ .../javascripts/app/services/singletonManager.js | 1 - .../directives/privileges-auth-modal.html.haml | 5 +++-- .../privileges-management-modal.html.haml | 2 +- 9 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/app/directives/functional/snEnter.js diff --git a/app/assets/javascripts/app/controllers/home.js b/app/assets/javascripts/app/controllers/home.js index c83a1c0a4..72e095a93 100644 --- a/app/assets/javascripts/app/controllers/home.js +++ b/app/assets/javascripts/app/controllers/home.js @@ -84,14 +84,14 @@ angular.module('app') syncManager.loadLocalItems().then(() => { $timeout(() => { $scope.allTag.didLoad = true; - $rootScope.$broadcast("initial-data-loaded"); + $rootScope.$broadcast("initial-data-loaded"); // This needs to be processed first before sync is called so that singletonManager observers function properly. + syncManager.sync(); + // refresh every 30s + setInterval(function () { + syncManager.sync(); + }, 30000); }) - syncManager.sync(); - // refresh every 30s - setInterval(function () { - syncManager.sync(); - }, 30000); }); authManager.addEventHandler((event) => { diff --git a/app/assets/javascripts/app/directives/functional/snEnter.js b/app/assets/javascripts/app/directives/functional/snEnter.js new file mode 100644 index 000000000..fe12e0983 --- /dev/null +++ b/app/assets/javascripts/app/directives/functional/snEnter.js @@ -0,0 +1,15 @@ +angular +.module('app') +.directive('snEnter', function() { + return function(scope, element, attrs) { + element.bind("keydown keypress", function(event) { + if(event.which === 13) { + scope.$apply(function(){ + scope.$eval(attrs.snEnter, {'event': event}); + }); + + event.preventDefault(); + } + }); + }; +}); diff --git a/app/assets/javascripts/app/directives/views/accountMenu.js b/app/assets/javascripts/app/directives/views/accountMenu.js index d984c28f8..ae33c16c2 100644 --- a/app/assets/javascripts/app/directives/views/accountMenu.js +++ b/app/assets/javascripts/app/directives/views/accountMenu.js @@ -170,6 +170,8 @@ class AccountMenu { } $scope.openPrivilegesModal = async function() { + $scope.close(); + let run = () => { $timeout(() => { privilegesManager.presentPrivilegesManagementModal(); diff --git a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js index 91816b1b8..c677da92b 100644 --- a/app/assets/javascripts/app/directives/views/privilegesAuthModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js @@ -61,7 +61,23 @@ class PrivilegesAuthModal { }) != null; } + $scope.validate = function() { + var failed = []; + for(var cred of $scope.requiredCredentials) { + var value = $scope.authenticationParameters[cred]; + if(!value || value.length == 0) { + failed.push(cred); + } + } + + $scope.failedCredentials = failed; + return failed.length == 0; + } + $scope.submit = function() { + if(!$scope.validate()) { + return; + } privilegesManager.authenticateAction($scope.action, $scope.authenticationParameters).then((result) => { $timeout(() => { if(result.success) { diff --git a/app/assets/javascripts/app/services/authManager.js b/app/assets/javascripts/app/services/authManager.js index 47e169a68..3e4381e1f 100644 --- a/app/assets/javascripts/app/services/authManager.js +++ b/app/assets/javascripts/app/services/authManager.js @@ -135,6 +135,7 @@ class AuthManager extends SFAuthManager { let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType); this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => { + // console.log("Loaded existing user prefs", resolvedSingleton.uuid); this.userPreferences = resolvedSingleton; this.userPreferencesDidChange(); }, (valueCallback) => { diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index a2869ead4..5e695a030 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -120,6 +120,7 @@ class PrivilegesManager { if(!this.privileges.content.desktopPrivileges) { this.privileges.content.desktopPrivileges = {}; } + console.log("Resolved existing privs", resolvedSingleton.uuid); resolve(resolvedSingleton); }, (valueCallback) => { // Safe to create. Create and return object. @@ -128,6 +129,7 @@ class PrivilegesManager { privs.setDirty(true); this.$rootScope.sync(); valueCallback(privs); + console.log("Creating new privs", privs.uuid); resolve(privs); }); }); diff --git a/app/assets/javascripts/app/services/singletonManager.js b/app/assets/javascripts/app/services/singletonManager.js index dd1624072..c7ca742cc 100644 --- a/app/assets/javascripts/app/services/singletonManager.js +++ b/app/assets/javascripts/app/services/singletonManager.js @@ -104,7 +104,6 @@ class SingletonManager { var singleton = allExtantItemsMatchingPredicate[0]; singletonHandler.singleton = singleton; singletonHandler.resolutionCallback(singleton); - } } } else { diff --git a/app/assets/templates/directives/privileges-auth-modal.html.haml b/app/assets/templates/directives/privileges-auth-modal.html.haml index 3be9f08d5..5e649f502 100644 --- a/app/assets/templates/directives/privileges-auth-modal.html.haml +++ b/app/assets/templates/directives/privileges-auth-modal.html.haml @@ -7,12 +7,13 @@ %h1.title Authentication Required %a.close-button.info{"ng-click" => "cancel()"} Cancel .content - %form.panel-section{"ng-submit" => "submit()"} + .panel-section %div{"ng-repeat" => "credential in requiredCredentials"} %p %strong {{promptForCredential(credential)}} %div - %input{"type" => "password", "ng-model" => "authenticationParameters[credential]", "sn-autofocus" => "true", "should-focus" => "$index == 0"} + %input{"type" => "password", "ng-model" => "authenticationParameters[credential]", + "sn-autofocus" => "true", "should-focus" => "$index == 0", "sn-enter" => "submit()"} %div %label.danger{"ng-if" => "isCredentialInFailureState(credential)"} Invalid authentication. Please try again. .panel-row diff --git a/app/assets/templates/directives/privileges-management-modal.html.haml b/app/assets/templates/directives/privileges-management-modal.html.haml index ee9dac0ae..a8ebc25b0 100644 --- a/app/assets/templates/directives/privileges-management-modal.html.haml +++ b/app/assets/templates/directives/privileges-management-modal.html.haml @@ -20,7 +20,7 @@ %td %p {{displayInfoForAction(action)}} %th{"ng-repeat" => "credential in availableCredentials"} - %input{"type" => "checkbox", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"} + %input{"type" => "checkbox", "ng-disabled" => "!credentialDisplayInfo[credential].availability", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"} .panel-section{"ng-if" => "sessionExpirey && !sessionExpired"} %p You will not be asked to authenticate until {{sessionExpirey}}. From 0c41815601459e4270b8dd96402a8b2a75c14af2 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 27 Nov 2018 12:25:11 -0600 Subject: [PATCH 11/12] Updates --- .../app/directives/views/privilegesManagementModal.js | 6 ++++-- app/assets/javascripts/app/services/privilegesManager.js | 6 +++++- app/assets/templates/directives/account-menu.html.haml | 5 ++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js index c9642b357..bec8691d4 100644 --- a/app/assets/javascripts/app/directives/views/privilegesManagementModal.js +++ b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js @@ -14,17 +14,20 @@ class PrivilegesManagementModal { } } - controller($scope, privilegesManager, passcodeManager, $timeout) { + controller($scope, privilegesManager, passcodeManager, authManager, $timeout) { 'ngInject'; $scope.dummy = {}; $scope.hasPasscode = passcodeManager.hasPasscode(); + $scope.hasAccount = !authManager.offline(); $scope.displayInfoForCredential = function(credential) { let info = privilegesManager.displayInfoForCredential(credential); if(credential == PrivilegesManager.CredentialLocalPasscode) { info["availability"] = $scope.hasPasscode; + } else if(credential == PrivilegesManager.CredentialAccountPassword) { + info["availability"] = $scope.hasAccount; } else { info["availability"] = true; } @@ -69,7 +72,6 @@ class PrivilegesManagementModal { } $scope.checkboxValueChanged = function(action, credential) { - console.log("toggleCredentialForAction", action, credential); $scope.privileges.toggleCredentialForAction(action, credential); privilegesManager.savePrivileges(); } diff --git a/app/assets/javascripts/app/services/privilegesManager.js b/app/assets/javascripts/app/services/privilegesManager.js index 5e695a030..817b48219 100644 --- a/app/assets/javascripts/app/services/privilegesManager.js +++ b/app/assets/javascripts/app/services/privilegesManager.js @@ -30,9 +30,9 @@ class PrivilegesManager { PrivilegesManager.SessionLengthOneWeek = 604800; this.availableActions = [ + PrivilegesManager.ActionManagePrivileges, PrivilegesManager.ActionManageExtensions, PrivilegesManager.ActionManageBackups, - PrivilegesManager.ActionManagePrivileges, PrivilegesManager.ActionManagePasscode, PrivilegesManager.ActionViewLockedNotes, PrivilegesManager.ActionDeleteNote @@ -61,6 +61,10 @@ class PrivilegesManager { } presentPrivilegesModal(action, onSuccess, onCancel) { + if(this.authenticationInProgress()) { + onCancel && onCancel(); + return; + } let customSuccess = () => { onSuccess && onSuccess(); diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml index 81ef1ce88..5eb144c6d 100644 --- a/app/assets/templates/directives/account-menu.html.haml +++ b/app/assets/templates/directives/account-menu.html.haml @@ -87,7 +87,7 @@ %a.panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"} Change Password - %a.panel-row.condensed{"ng-click" => "openPrivilegesModal('')"} + %a.panel-row.condensed{"ng-show" => "user", "ng-click" => "openPrivilegesModal('')"} Manage Privileges %a.panel-row.justify-left.condensed.success{"ng-if" => "securityUpdateAvailable", "ng-click" => "openPasswordWizard('upgrade-security')"} .inline.circle.small.success.mr-8 @@ -114,6 +114,7 @@ %p Adding a passcode is not supported in temporary sessions. Please sign out, then sign back in with the "Stay signed in" option checked. %form{"ng-if" => "formData.showPasscodeForm", "ng-submit" => "submitPasscodeForm()"} + .panel-row %input.form-control{:type => 'password', "ng-model" => "formData.passcode", "placeholder" => "Passcode", "sn-autofocus" => "true", "should-focus" => "true"} %input.form-control{:type => 'password', "ng-model" => "formData.confirmPasscode", "placeholder" => "Confirm Passcode"} .button-group.stretch.panel-row.form-submit @@ -138,6 +139,8 @@ {{option.label}} %p The autolock timer begins when the window or tab loses focus. + %a.panel-row.condensed{"ng-show" => "!user", "ng-click" => "openPrivilegesModal('')"} + Manage Privileges .panel-section{"ng-if" => "!importData.loading"} %h3.title Data Backups From 140b59e79de6f0e6f91b34eb74f7d96b1ccafa98 Mon Sep 17 00:00:00 2001 From: Mo Bitar Date: Tue, 27 Nov 2018 12:30:37 -0600 Subject: [PATCH 12/12] 2.4.0-beta1 --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ffdf0d56e..f38c4d64f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "2.3.17", + "version": "2.4.0-beta1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -7728,7 +7728,7 @@ "dev": true }, "sn-stylekit": { - "version": "file:../Stylekit", + "version": "1.1.6", "dev": true, "dependencies": { "abbrev": { diff --git a/package.json b/package.json index c04e3d2d4..d80b04969 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "2.3.18", + "version": "2.4.0-beta1", "license": "AGPL-3.0-or-later", "repository": { "type": "git", @@ -44,7 +44,7 @@ "mocha": "^5.2.0", "serve-static": "^1.13.2", "sn-models": "0.1.9", - "sn-stylekit": "file:~/Desktop/sn/dev/Stylekit", + "sn-stylekit": "1.1.6", "standard-file-js": "0.3.19" } }