Merge privileges

This commit is contained in:
Mo Bitar
2018-12-11 12:52:15 -06:00
32 changed files with 1115 additions and 210 deletions

View File

@@ -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.spellcheck = true;
this.componentManager = componentManager; this.componentManager = componentManager;
@@ -386,16 +387,28 @@ angular.module('app')
} }
} }
this.deleteNote = function() { this.deleteNote = async function() {
if(this.note.locked) { let run = () => {
alert("This note is locked. If you'd like to delete it, unlock it, and try again."); $timeout(() => {
return; 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(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionDeleteNote)) {
if(confirm(`Are you sure you want to delete ${title}?`)) { privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionDeleteNote, () => {
this.remove()(this.note); run();
this.showMenu = false; });
} else {
run();
} }
} }

View File

@@ -23,7 +23,8 @@ angular.module('app')
} }
}) })
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager, .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) => { authManager.checkForSecurityUpdate().then((available) => {
this.securityUpdateAvailable = available; this.securityUpdateAvailable = available;
@@ -172,7 +173,31 @@ angular.module('app')
} }
} }
this.selectRoom = function(room) { this.selectRoom = async function(room) {
room.showRoom = !room.showRoom; 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;
} }
}); });

View File

@@ -1,6 +1,7 @@
angular.module('app') angular.module('app')
.controller('HomeCtrl', function ($scope, $location, $rootScope, $timeout, modelManager, .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()); storageManager.initialize(passcodeManager.hasPasscode(), authManager.isEphemeralSession());
@@ -83,14 +84,14 @@ angular.module('app')
syncManager.loadLocalItems().then(() => { syncManager.loadLocalItems().then(() => {
$timeout(() => { $timeout(() => {
$scope.allTag.didLoad = true; $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) => { authManager.addEventHandler((event) => {

View File

@@ -41,7 +41,6 @@ class LockScreen {
}) })
} }
} }
} }
angular.module('app').directive('lockScreen', () => new LockScreen); angular.module('app').directive('lockScreen', () => new LockScreen);

View File

@@ -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 = {}; this.panelController = {};
@@ -198,19 +199,31 @@ angular.module('app')
} }
} }
this.selectNote = function(note, viaClick = false) { this.selectNote = async function(note, viaClick = false) {
if(!note) { if(!note) {
this.createNewNote(); this.createNewNote();
return; return;
} }
this.selectedNote = note; let run = () => {
note.conflict_of = null; // clear conflict $timeout(() => {
this.selectionMade()(note); this.selectedNote = note;
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0); note.conflict_of = null; // clear conflict
this.selectionMade()(note);
this.selectedIndex = Math.max(this.visibleNotes().indexOf(note), 0);
if(viaClick && this.isFiltering()) { if(viaClick && this.isFiltering()) {
desktopManager.searchText(this.noteFilter.text); desktopManager.searchText(this.noteFilter.text);
}
})
}
if(note.locked && await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionViewLockedNotes)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionViewLockedNotes, () => {
run();
});
} else {
run();
} }
} }

View File

@@ -2,9 +2,6 @@ angular.module('app').directive('infiniteScroll', [
'$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) { '$rootScope', '$window', '$timeout', function($rootScope, $window, $timeout) {
return { return {
link: function(scope, elem, attrs) { link: function(scope, elem, attrs) {
// elem.css('overflow-x', 'hidden');
// elem.css('height', 'inherit');
var offset = parseInt(attrs.threshold) || 0; var offset = parseInt(attrs.threshold) || 0;
var e = elem[0] var e = elem[0]

View File

@@ -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();
}
});
};
});

View File

@@ -10,7 +10,7 @@ class AccountMenu {
} }
controller($scope, $rootScope, authManager, modelManager, syncManager, storageManager, dbManager, passcodeManager, controller($scope, $rootScope, authManager, modelManager, syncManager, storageManager, dbManager, passcodeManager,
$timeout, $compile, archiveManager) { $timeout, $compile, archiveManager, privilegesManager) {
'ngInject'; 'ngInject';
$scope.formData = {mergeLocal: true, ephemeral: false}; $scope.formData = {mergeLocal: true, ephemeral: false};
@@ -38,7 +38,6 @@ class AccountMenu {
} }
$scope.canAddPasscode = !authManager.isEphemeralSession(); $scope.canAddPasscode = !authManager.isEphemeralSession();
$scope.syncStatus = syncManager.syncStatus; $scope.syncStatus = syncManager.syncStatus;
$scope.submitMfaForm = function() { $scope.submitMfaForm = function() {
@@ -167,10 +166,27 @@ class AccountMenu {
$scope.openPasswordWizard = function(type) { $scope.openPasswordWizard = function(type) {
// Close the account menu // Close the account menu
$scope.close(); $scope.close();
authManager.presentPasswordWizard(type); 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 // Allows indexeddb unencrypted logs to be deleted
// clearAllModels will remove data from backing store, but not from working memory // clearAllModels will remove data from backing store, but not from working memory
// See: https://github.com/standardnotes/desktop/issues/131 // See: https://github.com/standardnotes/desktop/issues/131
@@ -229,36 +245,49 @@ class AccountMenu {
}) })
} }
$scope.importFileSelected = function(files) { $scope.importFileSelected = async function(files) {
$scope.importData = {};
var file = files[0]; let run = () => {
var reader = new FileReader(); $timeout(() => {
reader.onload = function(e) { $scope.importData = {};
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 file = files[0];
var element = document.getElementById("import-password-request"); var reader = new FileReader();
if(element) { reader.onload = function(e) {
element.scrollIntoView(false); 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 { } catch (e) {
$scope.performImport(data, null); 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) { $scope.importJSONData = function(data, password, callback) {
@@ -316,8 +345,18 @@ class AccountMenu {
Export Export
*/ */
$scope.downloadDataArchive = function() { $scope.downloadDataArchive = async function() {
archiveManager.downloadBackup($scope.archiveFormData.encrypted); 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 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() { $scope.hasPasscode = function() {
return passcodeManager.hasPasscode(); return passcodeManager.hasPasscode();
} }
@@ -377,10 +445,10 @@ class AccountMenu {
return; 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, () => { fn(passcode, () => {
$timeout(function(){ $timeout(() => {
$scope.formData.showPasscodeForm = false; $scope.formData.showPasscodeForm = false;
var offline = authManager.offline(); var offline = authManager.offline();
@@ -393,27 +461,51 @@ class AccountMenu {
}) })
} }
$scope.changePasscodePressed = function() { $scope.changePasscodePressed = async function() {
$scope.formData.changingPasscode = true; let run = () => {
$scope.addPasscodeClicked(); $timeout(() => {
$scope.formData.changingPasscode = false; $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() { $scope.removePasscodePressed = async function() {
var signedIn = !authManager.offline(); let run = () => {
var message = "Are you sure you want to remove your local passcode?"; $timeout(() => {
if(!signedIn) { var signedIn = !authManager.offline();
message += " This will remove encryption from your local data."; var message = "Are you sure you want to remove your local passcode?";
} if(!signedIn) {
if(confirm(message)) { message += " This will remove encryption from your local data.";
passcodeManager.clearPasscode(); }
if(confirm(message)) {
passcodeManager.clearPasscode();
if(authManager.offline()) { if(authManager.offline()) {
syncManager.markAllItemsDirtyAndSaveOffline(); syncManager.markAllItemsDirtyAndSaveOffline();
// Don't create backup here, as if the user is temporarily removing the passcode to change it, // 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. // we don't want to write unencrypted data to disk.
// $rootScope.$broadcast("major-data-change"); // $rootScope.$broadcast("major-data-change");
} }
}
})
}
if(await privilegesManager.actionRequiresPrivilege(PrivilegesManager.ActionManagePasscode)) {
privilegesManager.presentPrivilegesModal(PrivilegesManager.ActionManagePasscode, () => {
run();
});
} else {
run();
} }
} }

View File

@@ -7,6 +7,7 @@ class MenuRow {
this.scope = { this.scope = {
action: "&", action: "&",
circle: "=", circle: "=",
circleAlign: "=",
label: "=", label: "=",
subtitle: "=", subtitle: "=",
hasButton: "=", hasButton: "=",

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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() { async checkForSecurityUpdate() {
if(this.offline()) { if(this.offline()) {
return false; return false;
@@ -128,6 +135,7 @@ class AuthManager extends SFAuthManager {
let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType); let contentTypePredicate = new SFPredicate("content_type", "=", prefsContentType);
this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => { this.singletonManager.registerSingleton([contentTypePredicate], (resolvedSingleton) => {
// console.log("Loaded existing user prefs", resolvedSingleton.uuid);
this.userPreferences = resolvedSingleton; this.userPreferences = resolvedSingleton;
this.userPreferencesDidChange(); this.userPreferencesDidChange();
}, (valueCallback) => { }, (valueCallback) => {

View File

@@ -5,7 +5,7 @@ class HttpManager extends SFHttpManager {
super($timeout); super($timeout);
this.setJWTRequestHandler(async () => { this.setJWTRequestHandler(async () => {
return storageManager.getItem("jwt");; return storageManager.getItem("jwt");
}) })
} }
} }

View File

@@ -7,7 +7,8 @@ SFModelManager.ContentTypeClassMapping = {
"SN|Theme" : SNTheme, "SN|Theme" : SNTheme,
"SN|Component" : SNComponent, "SN|Component" : SNComponent,
"SF|Extension" : SNServerExtension, "SF|Extension" : SNServerExtension,
"SF|MFA" : SNMfa "SF|MFA" : SNMfa,
"SN|Privileges" : SNPrivileges
}; };
SFItem.AppDomain = "org.standardnotes.sn"; SFItem.AppDomain = "org.standardnotes.sn";

View File

@@ -1,100 +1,186 @@
angular.module('app') class PasscodeManager {
.provider('passcodeManager', function () {
this.$get = function($rootScope, $timeout, modelManager, dbManager, authManager, storageManager) { constructor(authManager, storageManager) {
return new PasscodeManager($rootScope, $timeout, modelManager, dbManager, 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._locked = this._hasPasscode;
this.isLocked = function() { const MillisecondsPerSecond = 1000;
return this._locked; PasscodeManager.AutoLockIntervalNone = 0;
} PasscodeManager.AutoLockIntervalOneMinute = 60 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalFiveMinutes = 300 * MillisecondsPerSecond;
PasscodeManager.AutoLockIntervalOneHour = 3600 * MillisecondsPerSecond;
this.hasPasscode = function() { PasscodeManager.AutoLockIntervalKey = "AutoLockIntervalKey";
return this._hasPasscode; }
}
this.keys = function() { getAutoLockIntervalOptions() {
return this._keys; return [
} {
value: PasscodeManager.AutoLockIntervalNone,
this.passcodeAuthParams = function() { label: "None"
var authParams = JSON.parse(storageManager.getItemSync("offlineParams", StorageManager.Fixed)); },
if(authParams && !authParams.version) { {
var keys = this.keys(); value: PasscodeManager.AutoLockIntervalOneMinute,
if(keys && keys.ak) { label: "1 Min"
// 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 { value: PasscodeManager.AutoLockIntervalFiveMinutes,
authParams.version = "001"; label: "5 Min"
} },
{
value: PasscodeManager.AutoLockIntervalOneHour,
label: "1 Hr"
} }
return authParams; ]
} }
this.unlock = function(passcode, callback) { documentVisibilityChanged(visbility) {
var params = this.passcodeAuthParams(); let visible = document.visibilityState == "visible";
SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => { if(!visible) {
if(keys.pw !== params.hash) { this.beginAutoLockTimer();
callback(false); } else {
return; this.cancelAutoLockTimer();
}
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();
} }
} }
});
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);

View File

@@ -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( "<privileges-auth-modal action='action' on-success='onSuccess' on-cancel='onCancel' class='sk-modal'></privileges-auth-modal>" )(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( "<privileges-management-modal class='sk-modal'></privileges-management-modal>")(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);

View File

@@ -108,7 +108,6 @@ class SingletonManager {
var singleton = allExtantItemsMatchingPredicate[0]; var singleton = allExtantItemsMatchingPredicate[0];
singletonHandler.singleton = singleton; singletonHandler.singleton = singleton;
singletonHandler.resolutionCallback(singleton); singletonHandler.resolutionCallback(singleton);
} }
} }
} else { } else {

View File

@@ -1,4 +1,11 @@
$heading-height: 75px; $heading-height: 75px;
#editor-column {
.locked {
opacity: 0.8;
}
}
.editor { .editor {
flex: 1 50%; flex: 1 50%;
display: flex; display: flex;
@@ -7,10 +14,6 @@ $heading-height: 75px;
background-color: var(--sn-stylekit-background-color); background-color: var(--sn-stylekit-background-color);
} }
.locked {
opacity: 0.8;
}
#editor-title-bar { #editor-title-bar {
width: 100%; width: 100%;

View File

@@ -14,8 +14,8 @@
z-index: $z-index-lock-screen; z-index: $z-index-lock-screen;
background-color: var(--sn-stylekit-contrast-background-color); background-color: var(--sn-stylekit-contrast-background-color);
color: var(--sn-stylekit-foreground-color); color: var(--sn-stylekit-foreground-color);
font-size: 16px;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -29,6 +29,7 @@
.sk-panel { .sk-panel {
width: 315px; width: 315px;
flex-grow: 0; flex-grow: 0;
border-radius: 0;
.sk-panel-header { .sk-panel-header {
justify-content: center; justify-content: center;
@@ -36,9 +37,6 @@
} }
#passcode-reset { #passcode-reset {
margin-top: 18px;
text-align: center; text-align: center;
width: 100%;
font-size: 13px;
} }
} }

View File

@@ -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 { #password-wizard {
font-size: 16px; font-size: 16px;
} }
@@ -147,9 +183,10 @@
// Not sure yet if totally required. // Not sure yet if totally required.
// Update: The extensions manager doesn't display correctly without it // Update: The extensions manager doesn't display correctly without it
// flex-grow: 1 should fix that. // flex-grow: 1 should fix that.
flex-grow: 1; flex-grow: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: transparent;
} }
} }

View File

@@ -27,9 +27,6 @@
%a.sk-panel-row{"ng-click" => "formData.showAdvanced = !formData.showAdvanced"} %a.sk-panel-row{"ng-click" => "formData.showAdvanced = !formData.showAdvanced"}
Advanced Options 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-notification.unpadded.contrast.advanced-options.sk-panel-row{"ng-if" => "formData.showAdvanced"}
.sk-panel-column.stretch .sk-panel-column.stretch
@@ -47,12 +44,16 @@
%button.sk-button.info.featured{"type" => "submit", "ng-disabled" => "formData.authenticating"} %button.sk-button.info.featured{"type" => "submit", "ng-disabled" => "formData.authenticating"}
.sk-label {{formData.showLogin ? "Sign In" : "Register"}} .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 .sk-panel-row
%label %label
%input{"type" => "checkbox", "ng-model" => "formData.ephemeral", "ng-true-value" => "false", "ng-false-value" => "true"} %input{"type" => "checkbox", "ng-model" => "formData.ephemeral", "ng-true-value" => "false", "ng-false-value" => "true"}
Stay signed in Stay signed in
.sk-panel-row
%label{"ng-if" => "notesAndTagsCount() > 0"} %label{"ng-if" => "notesAndTagsCount() > 0"}
.sk-panel-row
%input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"} %input{"type" => "checkbox", "ng-model" => "formData.mergeLocal", "ng-bind" => "true", "ng-change" => "mergeLocalChanged()"}
Merge local data ({{notesAndTagsCount()}} notes and tags) Merge local data ({{notesAndTagsCount()}} notes and tags)
@@ -88,7 +89,10 @@
.sk-panel-row .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')"} %a.sk-panel-row.justify-left.condensed.success{"ng-if" => "securityUpdateAvailable", "ng-click" => "openPasswordWizard('upgrade-security')"}
.inline.sk-circle.small.success.mr-8 .inline.sk-circle.small.success.mr-8
.inline Security Update Available .inline Security Update Available
@@ -123,19 +127,31 @@
%a.neutral.sk-a.sk-panel-row{"ng-click" => "formData.showPasscodeForm = false"} Cancel %a.neutral.sk-a.sk-panel-row{"ng-click" => "formData.showPasscodeForm = false"} Cancel
%div{"ng-if" => "hasPasscode() && !formData.showPasscodeForm"} %div{"ng-if" => "hasPasscode() && !formData.showPasscodeForm"}
.sk-panel-row .sk-p
%p.sk-p Passcode lock is enabled.
Passcode lock is enabled. %span{"ng-if" => "isDesktopApplication()"} Your passcode will be required on new sessions after app quit.
%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-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{"ng-if" => "!importData.loading"}
.sk-panel-section-title Data Backups .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()"} %form.sk-panel-form.sk-panel-row{"ng-if" => "encryptedBackupsAvailable()"}
.sk-input-group .sk-input-group
%label %label
@@ -151,7 +167,7 @@
%label.sk-button.info %label.sk-button.info
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"} %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. %span{"ng-if" => "isDesktopApplication()"} Backups are automatically created on desktop and can be managed via the "Backups" top-level menu.

View File

@@ -1,7 +1,7 @@
.sk-menu-panel-row.row{"ng-attr-title" => "{{desc}}", "ng-click" => "onClick($event)"} .sk-menu-panel-row.row{"ng-attr-title" => "{{desc}}", "ng-click" => "onClick($event)"}
.sk-menu-panel-column .sk-menu-panel-column
.left .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-circle.small{"ng-class" => "circle"}
.sk-menu-panel-column{"ng-class" => "{'faded' : faded || disabled}"} .sk-menu-panel-column{"ng-class" => "{'faded' : faded || disabled}"}
.sk-label .sk-label
@@ -13,6 +13,9 @@
%menu-row{"ng-repeat" => "row in subRows", "action" => "row.onClick()", %menu-row{"ng-repeat" => "row in subRows", "action" => "row.onClick()",
"label" => "row.label", "subtitle" => "row.subtitle", "spinner-class" => "row.spinnerClass"} "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-menu-panel-column{"ng-if" => "hasButton"}
.sk-button{"ng-click" => "clickButton($event)", "ng-class" => "buttonClass"} .sk-button{"ng-click" => "clickButton($event)", "ng-class" => "buttonClass"}
.sk-label {{buttonText}} .sk-label {{buttonText}}

View File

@@ -1,6 +1,6 @@
.background{"ng-click" => "deny()"} .sk-modal-background{"ng-click" => "deny()"}
.content#permissions-modal .sk-modal-content#permissions-modal
.sn-component .sn-component
.sk-panel .sk-panel
.sk-panel-header .sk-panel-header

View File

@@ -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

View File

@@ -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.

View File

@@ -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.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.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.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'"} %menu-row{"label" => "'Delete'", "action" => "ctrl.selectedMenuItem(); ctrl.deleteNote()", "desc" => "'Delete this note permanently from all your devices'"}
.sk-menu-panel-section .sk-menu-panel-section

View File

@@ -1,7 +1,7 @@
.sn-component .sn-component
#footer-bar.sk-app-bar.no-edges #footer-bar.sk-app-bar.no-edges
.left .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-app-bar-item-column
.sk-circle.small{"ng-class" => "ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'neutral')"} .sk-circle.small{"ng-class" => "ctrl.error ? 'danger' : (ctrl.getUser() ? 'info' : 'neutral')"}
.sk-app-bar-item-column .sk-app-bar-item-column
@@ -39,6 +39,6 @@
.sk-app-bar-item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} .sk-app-bar-item{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"}
.sk-label Refresh .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 .sk-label
%i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"} %i.icon.ion-locked#footer-lock-icon{"ng-if" => "ctrl.hasPasscode()", "ng-click" => "ctrl.lockApp()"}

View File

@@ -13,11 +13,12 @@
%button.sk-button.info{"type" => "submit"} %button.sk-button.info{"type" => "submit"}
.sk-label Unlock .sk-label Unlock
#passcode-reset .sk-panel-footer
%a.sk-a.neutral{"ng-if" => "!formData.showRecovery", "ng-click" => "forgotPasscode()"} Forgot Passcode? #passcode-reset
%a.sk-a.neutral{"ng-if" => "!formData.showRecovery", "ng-click" => "forgotPasscode()"} Forgot?
%div{"ng-if" => "formData.showRecovery"} %div{"ng-if" => "formData.showRecovery"}
%p .sk-p
If you forgot your local passcode, your only option is to clear all your local data from this device 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. and sign back in to your account.
%a.sk-a.danger{"ng-click" => "beginDeleteData()"} Delete Local Data %a.sk-a.danger{"ng-click" => "beginDeleteData()"} Delete Local Data

View File

@@ -58,9 +58,11 @@
.faded {{note.savedTagsString || note.tagsString()}} .faded {{note.savedTagsString || note.tagsString()}}
.name{"ng-if" => "note.title"} .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.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"} .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}} .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}} .default-preview{"ng-if" => "!note.content.preview_html && !note.content.preview_plain"} {{note.text}}

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "standard-notes-web", "name": "standard-notes-web",
"version": "2.3.18", "version": "2.4.0-beta1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "standard-notes-web", "name": "standard-notes-web",
"version": "2.3.18", "version": "2.4.0-beta1",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"repository": { "repository": {
"type": "git", "type": "git",