diff --git a/app/assets/javascripts/app/controllers/editor.js b/app/assets/javascripts/app/controllers/editor.js
index c1a4416a3..ed97fe6a8 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;
@@ -386,16 +387,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/controllers/footer.js b/app/assets/javascripts/app/controllers/footer.js
index d408d1be4..5aeb5b176 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;
@@ -172,7 +173,31 @@ angular.module('app')
}
}
- this.selectRoom = function(room) {
- room.showRoom = !room.showRoom;
+ this.selectRoom = async function(room) {
+ let run = () => {
+ $timeout(() => {
+ room.showRoom = !room.showRoom;
+ })
+ }
+
+ 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();
+ }
+ }
+
+ 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..72e095a93 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());
@@ -83,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/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/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/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/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 d24684515..ae33c16c2 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};
@@ -38,7 +38,6 @@ class AccountMenu {
}
$scope.canAddPasscode = !authManager.isEphemeralSession();
-
$scope.syncStatus = syncManager.syncStatus;
$scope.submitMfaForm = function() {
@@ -167,10 +166,27 @@ class AccountMenu {
$scope.openPasswordWizard = function(type) {
// Close the account menu
$scope.close();
-
authManager.presentPasswordWizard(type);
}
+ $scope.openPrivilegesModal = async function() {
+ $scope.close();
+
+ 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
// clearAllModels will remove data from backing store, but not from working memory
// See: https://github.com/standardnotes/desktop/issues/131
@@ -229,36 +245,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) {
@@ -316,8 +345,18 @@ class AccountMenu {
Export
*/
- $scope.downloadDataArchive = function() {
- archiveManager.downloadBackup($scope.archiveFormData.encrypted);
+ $scope.downloadDataArchive = async function() {
+ let run = () => {
+ archiveManager.downloadBackup($scope.archiveFormData.encrypted);
+ }
+
+ if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManageBackups)) {
+ privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManageBackups, () => {
+ run();
+ });
+ } else {
+ run();
+ }
}
/*
@@ -362,6 +401,35 @@ class AccountMenu {
Passcode Lock
*/
+ $scope.passcodeAutoLockOptions = passcodeManager.getAutoLockIntervalOptions();
+
+ $scope.reloadAutoLockInterval = function() {
+ passcodeManager.getAutoLockInterval().then((interval) => {
+ $timeout(() => {
+ $scope.selectedAutoLockInterval = interval;
+ })
+ })
+ }
+
+ $scope.reloadAutoLockInterval();
+
+ $scope.selectAutoLockInterval = async function(interval) {
+ 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() {
return passcodeManager.hasPasscode();
}
@@ -377,10 +445,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();
@@ -393,27 +461,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/privilegesAuthModal.js b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js
new file mode 100644
index 000000000..c677da92b
--- /dev/null
+++ b/app/assets/javascripts/app/directives/views/privilegesAuthModal.js
@@ -0,0 +1,97 @@
+/*
+ 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, passcodeManager, authManager, $timeout) {
+ 'ngInject';
+
+ $scope.authenticationParameters = {};
+ $scope.sessionLengthOptions = privilegesManager.getSessionLengthOptions();
+
+ privilegesManager.getSelectedSessionLength().then((length) => {
+ $timeout(() => {
+ $scope.selectedSessionLength = length;
+ })
+ })
+
+ $scope.selectSessionLength = function(length) {
+ $scope.selectedSessionLength = length;
+ }
+
+ privilegesManager.netCredentialsForAction($scope.action).then((credentials) => {
+ $timeout(() => {
+ $scope.requiredCredentials = credentials.sort();
+ });
+ });
+
+ $scope.promptForCredential = function(credential) {
+ return privilegesManager.displayInfoForCredential(credential).prompt;
+ }
+
+ $scope.cancel = function() {
+ $scope.dismiss();
+ $scope.onCancel && $scope.onCancel();
+ }
+
+ $scope.isCredentialInFailureState = function(credential) {
+ if(!$scope.failedCredentials) {
+ return false;
+ }
+ return $scope.failedCredentials.find((candidate) => {
+ return candidate == credential;
+ }) != 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) {
+ privilegesManager.setSessionLength($scope.selectedSessionLength);
+ $scope.onSuccess();
+ $scope.dismiss();
+ } else {
+ $scope.failedCredentials = result.failedCredentials;
+ }
+ })
+ })
+ }
+
+ }
+}
+
+angular.module('app').directive('privilegesAuthModal', () => new PrivilegesAuthModal);
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..bec8691d4
--- /dev/null
+++ b/app/assets/javascripts/app/directives/views/privilegesManagementModal.js
@@ -0,0 +1,88 @@
+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, 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;
+ }
+
+ return info;
+ }
+
+ $scope.displayInfoForAction = function(action) {
+ return privilegesManager.displayInfoForAction(action).label;
+ }
+
+ $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.availableActions = privilegesManager.getAvailableActions();
+ $scope.availableCredentials = privilegesManager.getAvailableCredentials();
+ let sessionEndDate = await privilegesManager.getSessionExpirey();
+ $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;
+ })
+ })
+ }
+
+ $scope.checkboxValueChanged = function(action, credential) {
+ $scope.privileges.toggleCredentialForAction(action, credential);
+ privilegesManager.savePrivileges();
+ }
+
+ $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/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/authManager.js b/app/assets/javascripts/app/services/authManager.js
index 6787d3709..3e4381e1f 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;
@@ -128,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/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/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/passcodeManager.js b/app/assets/javascripts/app/services/passcodeManager.js
index 47c9e763b..d203f5aba 100644
--- a/app/assets/javascripts/app/services/passcodeManager.js
+++ b/app/assets/javascripts/app/services/passcodeManager.js
@@ -1,100 +1,186 @@
-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', (e) => {
+ 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.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() {
+ var interval = await this.getAutoLockInterval();
+ if(interval == PasscodeManager.AutoLockIntervalNone) {
+ return;
+ }
+
+ this.lockTimeout = setTimeout(() => {
+ this.lockApplication();
+ }, interval);
+ }
+
+ cancelAutoLockTimer() {
+ clearTimeout(this.lockTimeout);
+ }
+
+ lockApplication() {
+ window.location.reload();
+ this.cancelAutoLockTimer();
+ }
+
+ isLocked() {
+ return this._locked;
+ }
+
+ hasPasscode() {
+ return this._hasPasscode;
+ }
+
+ keys() {
+ return this._keys;
+ }
+
+ async setAutoLockInterval(interval) {
+ return this.storageManager.setItem(PasscodeManager.AutoLockIntervalKey, JSON.stringify(interval), StorageManager.FixedEncrypted);
+ }
+
+ async getAutoLockInterval() {
+ let interval = await this.storageManager.getItem(PasscodeManager.AutoLockIntervalKey, StorageManager.FixedEncrypted);
+ if(interval) {
+ return JSON.parse(interval);
+ } else {
+ return PasscodeManager.AutoLockIntervalNone;
+ }
+ }
+
+ 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
new file mode 100644
index 000000000..97d489338
--- /dev/null
+++ b/app/assets/javascripts/app/services/privilegesManager.js
@@ -0,0 +1,307 @@
+class PrivilegesManager {
+
+ 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;
+
+ this.loadPrivileges();
+
+ PrivilegesManager.CredentialAccountPassword = "CredentialAccountPassword";
+ PrivilegesManager.CredentialLocalPasscode = "CredentialLocalPasscode";
+
+ PrivilegesManager.ActionManageExtensions = "ActionManageExtensions";
+ PrivilegesManager.ActionManageBackups = "ActionManageBackups";
+ PrivilegesManager.ActionViewLockedNotes = "ActionViewLockedNotes";
+ PrivilegesManager.ActionManagePrivileges = "ActionManagePrivileges";
+ PrivilegesManager.ActionManagePasscode = "ActionManagePasscode";
+ PrivilegesManager.ActionDeleteNote = "ActionDeleteNote";
+
+ PrivilegesManager.SessionExpiresAtKey = "SessionExpiresAtKey";
+ PrivilegesManager.SessionLengthKey = "SessionLengthKey";
+
+ PrivilegesManager.SessionLengthNone = 0;
+ PrivilegesManager.SessionLengthFiveMinutes = 300;
+ PrivilegesManager.SessionLengthOneHour = 3600;
+ PrivilegesManager.SessionLengthOneWeek = 604800;
+
+ this.availableActions = [
+ PrivilegesManager.ActionManagePrivileges,
+ PrivilegesManager.ActionManageExtensions,
+ PrivilegesManager.ActionManageBackups,
+ PrivilegesManager.ActionManagePasscode,
+ PrivilegesManager.ActionViewLockedNotes,
+ PrivilegesManager.ActionDeleteNote
+ ]
+
+ this.availableCredentials = [
+ PrivilegesManager.CredentialAccountPassword,
+ PrivilegesManager.CredentialLocalPasscode
+ ];
+
+ this.sessionLengths = [
+ PrivilegesManager.SessionLengthNone,
+ PrivilegesManager.SessionLengthFiveMinutes,
+ PrivilegesManager.SessionLengthOneHour,
+ PrivilegesManager.SessionLengthOneWeek,
+ PrivilegesManager.SessionLengthIndefinite
+ ]
+ }
+
+ getAvailableActions() {
+ return this.availableActions;
+ }
+
+ getAvailableCredentials() {
+ return this.availableCredentials;
+ }
+
+ presentPrivilegesModal(action, onSuccess, onCancel) {
+ if(this.authenticationInProgress()) {
+ onCancel && onCancel();
+ return;
+ }
+
+ let customSuccess = () => {
+ onSuccess && onSuccess();
+ this.currentAuthenticationElement = null;
+ }
+
+ let customCancel = () => {
+ onCancel && 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;
+ }
+
+ 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 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);
+ angular.element(document.body).append(el);
+ }
+
+ authenticationInProgress() {
+ return this.currentAuthenticationElement != null;
+ }
+
+ 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 = {};
+ }
+ console.log("Resolved existing privs", resolvedSingleton.uuid);
+ resolve(resolvedSingleton);
+ }, (valueCallback) => {
+ // Safe to create. Create and return object.
+ var privs = new SNPrivileges({content_type: prefsContentType});
+ this.modelManager.addItem(privs);
+ privs.setDirty(true);
+ this.$rootScope.sync();
+ valueCallback(privs);
+ console.log("Creating new privs", privs.uuid);
+ resolve(privs);
+ });
+ });
+ }
+
+ async getPrivileges() {
+ if(this.privileges) {
+ return this.privileges;
+ } else {
+ return this.loadPrivileges();
+ }
+ }
+
+ 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.ActionManageBackups] = {
+ label: "Download/Import Backups"
+ };
+
+ metadata[PrivilegesManager.ActionViewLockedNotes] = {
+ label: "View Locked Notes"
+ };
+
+ metadata[PrivilegesManager.ActionManagePrivileges] = {
+ label: "Manage Privileges"
+ };
+
+ metadata[PrivilegesManager.ActionManagePasscode] = {
+ label: "Manage Passcode"
+ }
+
+ metadata[PrivilegesManager.ActionDeleteNote] = {
+ label: "Delete Notes"
+ }
+
+ return metadata[action];
+ }
+
+ getSessionLengthOptions() {
+ return [
+ {
+ value: PrivilegesManager.SessionLengthNone,
+ label: "Don't Remember"
+ },
+ {
+ value: PrivilegesManager.SessionLengthFiveMinutes,
+ label: "5 Minutes"
+ },
+ {
+ value: PrivilegesManager.SessionLengthOneHour,
+ label: "1 Hour"
+ },
+ {
+ 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 clearSession() {
+ return this.setSessionLength(PrivilegesManager.SessionLengthNone);
+ }
+
+ 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.netCredentialsForAction(action)).length > 0;
+ }
+
+ async savePrivileges() {
+ let privs = await this.getPrivileges();
+ privs.setDirty(true);
+ this.$rootScope.sync();
+ }
+
+ async authenticateAction(action, credentialAuthMapping) {
+ var requiredCredentials = (await this.netCredentialsForAction(action));
+ var successfulCredentials = [], failedCredentials = [];
+
+ for(let requiredCredential of requiredCredentials) {
+ var passesAuth = await this._verifyAuthenticationParameters(requiredCredential, credentialAuthMapping[requiredCredential]);
+ if(passesAuth) {
+ successfulCredentials.push(requiredCredential);
+ } else {
+ failedCredentials.push(requiredCredential);
+ }
+ }
+
+ return {
+ success: failedCredentials.length == 0,
+ successfulCredentials: successfulCredentials,
+ failedCredentials: failedCredentials
+ }
+ }
+
+ async _verifyAuthenticationParameters(credential, value) {
+
+ let verifyAccountPassword = async (password) => {
+ return this.authManager.verifyAccountPassword(password);
+ }
+
+ let verifyLocalPasscode = async (passcode) => {
+ return this.passcodeManager.verifyPasscode(passcode);
+ }
+
+ if(credential == PrivilegesManager.CredentialAccountPassword) {
+ return verifyAccountPassword(value);
+ } else if(credential == PrivilegesManager.CredentialLocalPasscode) {
+ return verifyLocalPasscode(value);
+ }
+ }
+
+}
+
+angular.module('app').service('privilegesManager', PrivilegesManager);
diff --git a/app/assets/javascripts/app/services/singletonManager.js b/app/assets/javascripts/app/services/singletonManager.js
index 8638cf36f..3df8cbbdd 100644
--- a/app/assets/javascripts/app/services/singletonManager.js
+++ b/app/assets/javascripts/app/services/singletonManager.js
@@ -108,7 +108,6 @@ class SingletonManager {
var singleton = allExtantItemsMatchingPredicate[0];
singletonHandler.singleton = singleton;
singletonHandler.resolutionCallback(singleton);
-
}
}
} else {
diff --git a/app/assets/stylesheets/app/_editor.scss b/app/assets/stylesheets/app/_editor.scss
index 65eb89e1f..aac1a7994 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: var(--sn-stylekit-background-color);
}
-.locked {
- opacity: 0.8;
-}
-
#editor-title-bar {
width: 100%;
diff --git a/app/assets/stylesheets/app/_lock-screen.scss b/app/assets/stylesheets/app/_lock-screen.scss
index 5e135881e..f5faed28e 100644
--- a/app/assets/stylesheets/app/_lock-screen.scss
+++ b/app/assets/stylesheets/app/_lock-screen.scss
@@ -14,8 +14,8 @@
z-index: $z-index-lock-screen;
background-color: var(--sn-stylekit-contrast-background-color);
color: var(--sn-stylekit-foreground-color);
- font-size: 16px;
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
@@ -29,6 +29,7 @@
.sk-panel {
width: 315px;
flex-grow: 0;
+ border-radius: 0;
.sk-panel-header {
justify-content: center;
@@ -36,9 +37,6 @@
}
#passcode-reset {
- margin-top: 18px;
text-align: center;
- width: 100%;
- font-size: 13px;
}
}
diff --git a/app/assets/stylesheets/app/_modals.scss b/app/assets/stylesheets/app/_modals.scss
index bb35bbef6..09ac067f2 100644
--- a/app/assets/stylesheets/app/_modals.scss
+++ b/app/assets/stylesheets/app/_modals.scss
@@ -11,6 +11,42 @@
}
}
+#privileges-modal {
+ width: 700px;
+
+ table {
+ margin-bottom: 12px;
+ width: 100%;
+ overflow: auto;
+ border-collapse: collapse;
+ border-spacing: 0px;
+ border-color: var(--sn-stylekit-contrast-border-color);
+ background-color: var(--sn-stylekit-background-color);
+ color: var(--sn-stylekit-contrast-foreground-color);
+
+ th, td {
+ padding: 6px 13px;
+ border: 1px solid var(--sn-stylekit-contrast-border-color);
+ }
+
+ tr:nth-child(2n) {
+ background-color: var(--sn-stylekit-contrast-background-color);
+ }
+ }
+
+ th {
+ text-align: center;
+ font-weight: normal;
+ }
+
+ .priv-header {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+}
+
#password-wizard {
font-size: 16px;
}
@@ -147,9 +183,10 @@
// Not sure yet if totally required.
// Update: The extensions manager doesn't display correctly without it
// flex-grow: 1 should fix that.
-
+
flex-grow: 1;
width: 100%;
height: 100%;
+ background-color: transparent;
}
}
diff --git a/app/assets/templates/directives/account-menu.html.haml b/app/assets/templates/directives/account-menu.html.haml
index 9cff2a4b0..436ef6a74 100644
--- a/app/assets/templates/directives/account-menu.html.haml
+++ b/app/assets/templates/directives/account-menu.html.haml
@@ -27,9 +27,6 @@
%a.sk-panel-row{"ng-click" => "formData.showAdvanced = !formData.showAdvanced"}
Advanced Options
- .sk-notification.neutral{"ng-if" => "formData.showRegister"}
- .sk-notification-title No Password Reset.
- .sk-notification-text Because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.
.sk-notification.unpadded.contrast.advanced-options.sk-panel-row{"ng-if" => "formData.showAdvanced"}
.sk-panel-column.stretch
@@ -47,12 +44,16 @@
%button.sk-button.info.featured{"type" => "submit", "ng-disabled" => "formData.authenticating"}
.sk-label {{formData.showLogin ? "Sign In" : "Register"}}
+ .sk-notification.neutral{"ng-if" => "formData.showRegister"}
+ .sk-notification-title No Password Reset.
+ .sk-notification-text Because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.
+
.sk-panel-row
%label
%input{"type" => "checkbox", "ng-model" => "formData.ephemeral", "ng-true-value" => "false", "ng-false-value" => "true"}
Stay signed in
- .sk-panel-row
%label{"ng-if" => "notesAndTagsCount() > 0"}
+ .sk-panel-row
%input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"}
Merge local data ({{notesAndTagsCount()}} notes and tags)
@@ -88,7 +89,10 @@
.sk-panel-row
- %a.sk-a.info.sk-panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"} Change Password
+ %a.sk-a.info.sk-panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"}
+ Change Password
+ %a.sk-a.info.sk-panel-row.condensed{"ng-show" => "user", "ng-click" => "openPrivilegesModal('')"}
+ Manage Privileges
%a.sk-panel-row.justify-left.condensed.success{"ng-if" => "securityUpdateAvailable", "ng-click" => "openPasswordWizard('upgrade-security')"}
.inline.sk-circle.small.success.mr-8
.inline Security Update Available
@@ -123,19 +127,31 @@
%a.neutral.sk-a.sk-panel-row{"ng-click" => "formData.showPasscodeForm = false"} Cancel
%div{"ng-if" => "hasPasscode() && !formData.showPasscodeForm"}
- .sk-panel-row
- %p.sk-p
- Passcode lock is enabled.
- %span{"ng-if" => "isDesktopApplication()"} Your passcode will be required on new sessions after app quit.
- .sk-panel-row.justify-left
- .sk-horizontal-group
- %a.sk-a.info{"ng-click" => "changePasscodePressed()"} Change Passcode
- %a.sk-a.danger{"ng-click" => "removePasscodePressed()"} Remove Passcode
+ .sk-p
+ Passcode lock is enabled.
+ %span{"ng-if" => "isDesktopApplication()"} Your passcode will be required on new sessions after app quit.
+ .sk-notification.contrast
+ .sk-notification-title Options
+ .sk-notification-text
+ .sk-panel-row
+ .sk-horizontal-group
+ .sk-h4.sk-bold Autolock
+ %a.sk-a.info{"ng-repeat" => "option in passcodeAutoLockOptions", "ng-click" => "selectAutoLockInterval(option.value)",
+ "ng-class" => "{'info boxed' : option.value == selectedAutoLockInterval}"}
+ {{option.label}}
+ .sk-p The autolock timer begins when the window or tab loses focus.
+ .sk-panel-row
+ %a.sk-a.info.sk-panel-row.condensed{"ng-show" => "!user", "ng-click" => "openPrivilegesModal('')"} Manage Privileges
+ %a.sk-a.info.sk-panel-row.condensed{"ng-click" => "changePasscodePressed()"} Change Passcode
+ %a.sk-a.danger.sk-panel-row.condensed{"ng-click" => "removePasscodePressed()"} Remove Passcode
.sk-panel-section{"ng-if" => "!importData.loading"}
.sk-panel-section-title Data Backups
+ .sk-p
+ Download a backup of all your data.
+ .sk-panel-row
%form.sk-panel-form.sk-panel-row{"ng-if" => "encryptedBackupsAvailable()"}
.sk-input-group
%label
@@ -151,7 +167,7 @@
%label.sk-button.info
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
- .sk-label Import From Backup
+ .sk-label Import Backup
%span{"ng-if" => "isDesktopApplication()"} Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.
diff --git a/app/assets/templates/directives/menu-row.html.haml b/app/assets/templates/directives/menu-row.html.haml
index 009c0d236..ca3ffc399 100644
--- a/app/assets/templates/directives/menu-row.html.haml
+++ b/app/assets/templates/directives/menu-row.html.haml
@@ -1,7 +1,7 @@
.sk-menu-panel-row.row{"ng-attr-title" => "{{desc}}", "ng-click" => "onClick($event)"}
.sk-menu-panel-column
.left
- .sk-menu-panel-column{"ng-if" => "circle"}
+ .sk-menu-panel-column{"ng-if" => "circle && (!circleAlign || circleAlign == 'left')"}
.sk-circle.small{"ng-class" => "circle"}
.sk-menu-panel-column{"ng-class" => "{'faded' : faded || disabled}"}
.sk-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"}
+ .sk-menu-panel-column{"ng-if" => "circle && circleAlign == 'right'"}
+ .sk-circle.small{"ng-class" => "circle"}
+
.sk-menu-panel-column{"ng-if" => "hasButton"}
.sk-button{"ng-click" => "clickButton($event)", "ng-class" => "buttonClass"}
.sk-label {{buttonText}}
diff --git a/app/assets/templates/directives/permissions-modal.html.haml b/app/assets/templates/directives/permissions-modal.html.haml
index 9c797a243..9628eb9ad 100644
--- a/app/assets/templates/directives/permissions-modal.html.haml
+++ b/app/assets/templates/directives/permissions-modal.html.haml
@@ -1,6 +1,6 @@
-.background{"ng-click" => "deny()"}
+.sk-modal-background{"ng-click" => "deny()"}
-.content#permissions-modal
+.sk-modal-content#permissions-modal
.sn-component
.sk-panel
.sk-panel-header
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..c48f6a9d2
--- /dev/null
+++ b/app/assets/templates/directives/privileges-auth-modal.html.haml
@@ -0,0 +1,29 @@
+.sk-modal-background{"ng-click" => "cancel()"}
+
+.sk-modal-content#privileges-modal
+ .sn-component
+ .sk-panel
+ .sk-panel-header
+ .sk-panel-header-title Authentication Required
+ %a.close-button.info{"ng-click" => "cancel()"} Cancel
+ .sk-panel-content
+ .sk-panel-section
+ %div{"ng-repeat" => "credential in requiredCredentials"}
+ .sk-p.sk-bold.sk-panel-row
+ %strong {{promptForCredential(credential)}}
+ .sk-panel-row
+ %input.sk-input.contrast{"type" => "password", "ng-model" => "authenticationParameters[credential]",
+ "sn-autofocus" => "true", "should-focus" => "$index == 0", "sn-enter" => "submit()"}
+ .sk-panel-row
+ %label.sk-label.danger{"ng-if" => "isCredentialInFailureState(credential)"} Invalid authentication. Please try again.
+ .sk-panel-row
+ .sk-panel-row
+ .sk-horizontal-group
+ .sk-p.sk-bold Remember For
+ %a.sk-a.info{"ng-repeat" => "option in sessionLengthOptions", "ng-click" => "selectSessionLength(option.value)",
+ "ng-class" => "{'info boxed' : option.value == selectedSessionLength}"}
+ {{option.label}}
+
+ .sk-panel-footer.extra-padding
+ .sk-button.info.big.block.bold{"ng-click" => "submit()"}
+ .sk-label Submit
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..03a4bfb11
--- /dev/null
+++ b/app/assets/templates/directives/privileges-management-modal.html.haml
@@ -0,0 +1,40 @@
+.sk-modal-background{"ng-click" => "cancel()"}
+
+.sk-modal-content#privileges-modal
+ .sn-component
+ .sk-panel
+ .sk-panel-header
+ .sk-panel-header-title Manage Privileges
+ %a.sk-a.close-button.info{"ng-click" => "cancel()"} Done
+ .sk-panel-content
+ .sk-panel-section
+ %table.sk-table
+ %thead
+ %tr
+ %th
+ %th{"ng-repeat" => "cred in availableCredentials"}
+ .priv-header
+ %strong {{credentialDisplayInfo[cred].label}}
+ .sk-p.font-small{"style" => "margin-top: 2px", "ng-show" => "!credentialDisplayInfo[cred].availability"} Not Configured
+ %tbody
+ %tr{"ng-repeat" => "action in availableActions"}
+ %td
+ .sk-p {{displayInfoForAction(action)}}
+ %th{"ng-repeat" => "credential in availableCredentials"}
+ %input{"type" => "checkbox", "ng-disabled" => "!credentialDisplayInfo[credential].availability", "ng-checked" => "isCredentialRequiredForAction(action, credential)", "ng-click" => "checkboxValueChanged(action, credential)"}
+
+ .sk-panel-section{"ng-if" => "sessionExpirey && !sessionExpired"}
+ .sk-p You will not be asked to authenticate until {{sessionExpirey}}.
+ %a.sk-a {"ng-click" => "clearSession()"} Clear Session
+ .sk-panel-footer
+ .sk-h2 About Privileges
+ .sk-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/app/assets/templates/editor.html.haml b/app/assets/templates/editor.html.haml
index dfea92eed..ec5c34da7 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'"}
.sk-menu-panel-section
diff --git a/app/assets/templates/footer.html.haml b/app/assets/templates/footer.html.haml
index 41ac65bb7..40717084e 100644
--- a/app/assets/templates/footer.html.haml
+++ b/app/assets/templates/footer.html.haml
@@ -1,7 +1,7 @@
.sn-component
#footer-bar.sk-app-bar.no-edges
.left
- .sk-app-bar-item{"ng-click" => "ctrl.accountMenuPressed()", "click-outside" => "ctrl.showAccountMenu = false;", "is-open" => "ctrl.showAccountMenu"}
+ .sk-app-bar-item{"ng-click" => "ctrl.accountMenuPressed()", "click-outside" => "ctrl.clickOutsideAccountMenu()", "is-open" => "ctrl.showAccountMenu"}
.sk-app-bar-item-column
.sk-circle.small{"ng-class" => "ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'neutral')"}
.sk-app-bar-item-column
@@ -39,6 +39,6 @@
.sk-app-bar-item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"}
.sk-label Refresh
- .sk-app-bar-item#lock-item{"ng-if" => "ctrl.hasPasscode()"}
+ .sk-app-bar-item#lock-item{"ng-if" => "ctrl.hasPasscode()", "title" => "Locks application and wipes unencrypted data from memory."}
.sk-label
%i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"}
diff --git a/app/assets/templates/lock-screen.html.haml b/app/assets/templates/lock-screen.html.haml
index 890b495b2..222e6b704 100644
--- a/app/assets/templates/lock-screen.html.haml
+++ b/app/assets/templates/lock-screen.html.haml
@@ -13,11 +13,12 @@
%button.sk-button.info{"type" => "submit"}
.sk-label Unlock
- #passcode-reset
- %a.sk-a.neutral{"ng-if" => "!formData.showRecovery", "ng-click" => "forgotPasscode()"} Forgot Passcode?
+ .sk-panel-footer
+ #passcode-reset
+ %a.sk-a.neutral{"ng-if" => "!formData.showRecovery", "ng-click" => "forgotPasscode()"} Forgot?
- %div{"ng-if" => "formData.showRecovery"}
- %p
- If you forgot your local passcode, your only option is to clear all your local data from this device
- and sign back in to your account.
- %a.sk-a.danger{"ng-click" => "beginDeleteData()"} Delete Local Data
+ %div{"ng-if" => "formData.showRecovery"}
+ .sk-p
+ If you forgot your local passcode, your only option is to clear all your local data from this device
+ and sign back in to your account.
+ %a.sk-a.danger{"ng-click" => "beginDeleteData()"} Delete Local Data
diff --git a/app/assets/templates/notes.html.haml b/app/assets/templates/notes.html.haml
index e566e45ea..7e82d7719 100644
--- a/app/assets/templates/notes.html.haml
+++ b/app/assets/templates/notes.html.haml
@@ -58,9 +58,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}}
diff --git a/package-lock.json b/package-lock.json
index 31193042a..5bf46c70b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "standard-notes-web",
- "version": "2.3.18",
+ "version": "2.4.0-beta1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index c533efa89..12eb97359 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",