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

View File

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

View File

@@ -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) => {

View File

@@ -41,7 +41,6 @@ class 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 = {};
@@ -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();
}
}

View File

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

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

View File

@@ -7,6 +7,7 @@ class MenuRow {
this.scope = {
action: "&",
circle: "=",
circleAlign: "=",
label: "=",
subtitle: "=",
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() {
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) => {

View File

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

View File

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

View File

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

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];
singletonHandler.singleton = singleton;
singletonHandler.resolutionCallback(singleton);
}
}
} else {

View File

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

View File

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

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 {
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;
}
}

View File

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

View File

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

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
.sk-panel
.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.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

View File

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

View File

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

View File

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

2
package-lock.json generated
View File

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

View File

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