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