8
.babelrc
8
.babelrc
@@ -1,3 +1,9 @@
|
||||
{
|
||||
"presets": ["env"]
|
||||
"presets": ["env"],
|
||||
"plugins": [
|
||||
["transform-runtime", {
|
||||
"polyfill": false,
|
||||
"regenerator": true
|
||||
}]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ module.exports = function(grunt) {
|
||||
|
||||
lib: {
|
||||
src: [
|
||||
'node_modules/standard-file-js/dist/regenerator.js',
|
||||
'node_modules/standard-file-js/dist/sfjs.js',
|
||||
'vendor/assets/bower_components/angular/angular.js',
|
||||
'vendor/assets/javascripts/lodash/lodash.custom.min.js'
|
||||
@@ -160,6 +161,9 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-babel');
|
||||
grunt.loadNpmTasks('grunt-browserify');
|
||||
|
||||
// grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat:app',
|
||||
// 'concat:lib', 'concat:dist', 'concat:css', 'babel', 'browserify', 'uglify']);
|
||||
|
||||
grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat:app', 'babel', 'browserify',
|
||||
'concat:lib', 'concat:dist', 'ngAnnotate', 'concat:css', 'uglify']);
|
||||
};
|
||||
|
||||
@@ -229,15 +229,11 @@ angular.module('app')
|
||||
if(success) {
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
this.saveError = false;
|
||||
this.syncTakingTooLong = false;
|
||||
this.showAllChangesSavedStatus();
|
||||
}.bind(this), 200)
|
||||
} else {
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
this.saveError = true;
|
||||
this.syncTakingTooLong = false;
|
||||
this.showErrorStatus();
|
||||
}.bind(this), 200)
|
||||
}
|
||||
@@ -277,6 +273,9 @@ angular.module('app')
|
||||
}
|
||||
|
||||
this.showAllChangesSavedStatus = function() {
|
||||
this.saveError = false;
|
||||
this.syncTakingTooLong = false;
|
||||
|
||||
var status = "All changes saved";
|
||||
if(authManager.offline()) {
|
||||
status += " (offline)";
|
||||
@@ -285,7 +284,9 @@ angular.module('app')
|
||||
}
|
||||
|
||||
this.showErrorStatus = function() {
|
||||
this.noteStatus = $sce.trustAsHtml("Error syncing<br>(changes saved offline)")
|
||||
this.saveError = true;
|
||||
this.syncTakingTooLong = false;
|
||||
this.noteStatus = $sce.trustAsHtml("<span class='error bold'>Sync Unreachable</span><br>All changes saved offline")
|
||||
}
|
||||
|
||||
this.contentChanged = function() {
|
||||
@@ -325,19 +326,16 @@ angular.module('app')
|
||||
|
||||
this.togglePin = function() {
|
||||
this.note.setAppDataItem("pinned", !this.note.pinned);
|
||||
this.note.setDirty(true);
|
||||
this.changesMade();
|
||||
}
|
||||
|
||||
this.toggleLockNote = function() {
|
||||
this.note.setAppDataItem("locked", !this.note.locked);
|
||||
this.note.setDirty(true);
|
||||
this.changesMade();
|
||||
}
|
||||
|
||||
this.toggleArchiveNote = function() {
|
||||
this.note.setAppDataItem("archived", !this.note.archived);
|
||||
this.note.setDirty(true);
|
||||
this.changesMade(true);
|
||||
$rootScope.$broadcast("noteArchived");
|
||||
}
|
||||
|
||||
@@ -25,142 +25,149 @@ angular.module('app')
|
||||
.controller('FooterCtrl', function ($rootScope, authManager, modelManager, $timeout, dbManager,
|
||||
syncManager, storageManager, passcodeManager, componentManager, singletonManager, nativeExtManager) {
|
||||
|
||||
$rootScope.$on("reload-ext-data", () => {
|
||||
if(this.reloadInProgress) { return; }
|
||||
this.reloadInProgress = true;
|
||||
this.securityUpdateAvailable = authManager.checkForSecurityUpdate();
|
||||
$rootScope.$on("security-update-status-changed", () => {
|
||||
this.securityUpdateAvailable = authManager.securityUpdateAvailable;
|
||||
})
|
||||
|
||||
// A reload occurs when the extensions manager window is opened. We can close it after a delay
|
||||
let extWindow = this.rooms.find((room) => {return room.package_info.identifier == nativeExtManager.extensionsManagerIdentifier});
|
||||
if(!extWindow) {
|
||||
return;
|
||||
this.openSecurityUpdate = function() {
|
||||
authManager.presentPasswordWizard("upgrade-security");
|
||||
}
|
||||
|
||||
this.selectRoom(extWindow);
|
||||
$rootScope.$on("reload-ext-data", () => {
|
||||
if(this.reloadInProgress) { return; }
|
||||
this.reloadInProgress = true;
|
||||
|
||||
$timeout(() => {
|
||||
this.selectRoom(extWindow);
|
||||
this.reloadInProgress = false;
|
||||
$rootScope.$broadcast("ext-reload-complete");
|
||||
}, 2000)
|
||||
});
|
||||
|
||||
this.getUser = function() {
|
||||
return authManager.user;
|
||||
}
|
||||
|
||||
this.updateOfflineStatus = function() {
|
||||
this.offline = authManager.offline();
|
||||
}
|
||||
this.updateOfflineStatus();
|
||||
|
||||
if(this.offline && !passcodeManager.hasPasscode()) {
|
||||
this.showAccountMenu = true;
|
||||
}
|
||||
|
||||
this.findErrors = function() {
|
||||
this.error = syncManager.syncStatus.error;
|
||||
}
|
||||
this.findErrors();
|
||||
|
||||
this.onAuthSuccess = function() {
|
||||
this.showAccountMenu = false;
|
||||
}.bind(this)
|
||||
|
||||
this.accountMenuPressed = function() {
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
this.closeAllRooms();
|
||||
}
|
||||
|
||||
this.closeAccountMenu = () => {
|
||||
this.showAccountMenu = false;
|
||||
}
|
||||
|
||||
this.hasPasscode = function() {
|
||||
return passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
this.lockApp = function() {
|
||||
$rootScope.lockApplication();
|
||||
}
|
||||
|
||||
this.refreshData = function() {
|
||||
this.isRefreshing = true;
|
||||
syncManager.sync((response) => {
|
||||
$timeout(function(){
|
||||
this.isRefreshing = false;
|
||||
}.bind(this), 200)
|
||||
if(response && response.error) {
|
||||
alert("There was an error syncing. Please try again. If all else fails, log out and log back in.");
|
||||
} else {
|
||||
this.syncUpdated();
|
||||
// A reload occurs when the extensions manager window is opened. We can close it after a delay
|
||||
let extWindow = this.rooms.find((room) => {return room.package_info.identifier == nativeExtManager.extensionsManagerIdentifier});
|
||||
if(!extWindow) {
|
||||
return;
|
||||
}
|
||||
}, {force: true}, "refreshData");
|
||||
}
|
||||
|
||||
this.syncUpdated = function() {
|
||||
this.lastSyncDate = new Date();
|
||||
}
|
||||
this.selectRoom(extWindow);
|
||||
|
||||
$rootScope.$on("new-update-available", function(version){
|
||||
$timeout(function(){
|
||||
// timeout calls apply() which is needed
|
||||
this.onNewUpdateAvailable();
|
||||
}.bind(this))
|
||||
}.bind(this))
|
||||
$timeout(() => {
|
||||
this.selectRoom(extWindow);
|
||||
this.reloadInProgress = false;
|
||||
$rootScope.$broadcast("ext-reload-complete");
|
||||
}, 2000)
|
||||
});
|
||||
|
||||
this.onNewUpdateAvailable = function() {
|
||||
this.newUpdateAvailable = true;
|
||||
}
|
||||
|
||||
this.clickedNewUpdateAnnouncement = function() {
|
||||
this.newUpdateAvailable = false;
|
||||
alert("A new update is ready to install. Please use the top-level 'Updates' menu to manage installation.")
|
||||
}
|
||||
|
||||
|
||||
/* Rooms */
|
||||
|
||||
this.componentManager = componentManager;
|
||||
this.rooms = [];
|
||||
|
||||
modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
||||
var incomingRooms = allItems.filter((candidate) => {return candidate.area == "rooms"});
|
||||
this.rooms = _.uniq(this.rooms.concat(incomingRooms)).filter((candidate) => {return !candidate.deleted});
|
||||
});
|
||||
|
||||
componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => {
|
||||
// RIP: There used to be code here that checked if component.active was true, and if so, displayed the component.
|
||||
// However, we no longer want to persist active state for footer extensions. If you open Extensions on one computer,
|
||||
// it shouldn't open on another computer. Active state should only be persisted for persistent extensions, like Folders.
|
||||
}, actionHandler: (component, action, data) => {
|
||||
if(action == "set-size") {
|
||||
component.setLastSize(data);
|
||||
this.getUser = function() {
|
||||
return authManager.user;
|
||||
}
|
||||
}, focusHandler: (component, focused) => {
|
||||
if(component.isEditor() && focused) {
|
||||
|
||||
this.updateOfflineStatus = function() {
|
||||
this.offline = authManager.offline();
|
||||
}
|
||||
this.updateOfflineStatus();
|
||||
|
||||
if(this.offline && !passcodeManager.hasPasscode()) {
|
||||
this.showAccountMenu = true;
|
||||
}
|
||||
|
||||
this.findErrors = function() {
|
||||
this.error = syncManager.syncStatus.error;
|
||||
}
|
||||
this.findErrors();
|
||||
|
||||
this.onAuthSuccess = function() {
|
||||
this.showAccountMenu = false;
|
||||
}.bind(this)
|
||||
|
||||
this.accountMenuPressed = function() {
|
||||
this.showAccountMenu = !this.showAccountMenu;
|
||||
this.closeAllRooms();
|
||||
}
|
||||
|
||||
this.closeAccountMenu = () => {
|
||||
this.showAccountMenu = false;
|
||||
}
|
||||
|
||||
this.hasPasscode = function() {
|
||||
return passcodeManager.hasPasscode();
|
||||
}
|
||||
|
||||
this.lockApp = function() {
|
||||
$rootScope.lockApplication();
|
||||
}
|
||||
|
||||
this.refreshData = function() {
|
||||
this.isRefreshing = true;
|
||||
syncManager.sync((response) => {
|
||||
$timeout(function(){
|
||||
this.isRefreshing = false;
|
||||
}.bind(this), 200)
|
||||
if(response && response.error) {
|
||||
alert("There was an error syncing. Please try again. If all else fails, log out and log back in.");
|
||||
} else {
|
||||
this.syncUpdated();
|
||||
}
|
||||
}, {force: true}, "refreshData");
|
||||
}
|
||||
|
||||
this.syncUpdated = function() {
|
||||
this.lastSyncDate = new Date();
|
||||
}
|
||||
|
||||
$rootScope.$on("new-update-available", function(version){
|
||||
$timeout(function(){
|
||||
// timeout calls apply() which is needed
|
||||
this.onNewUpdateAvailable();
|
||||
}.bind(this))
|
||||
}.bind(this))
|
||||
|
||||
this.onNewUpdateAvailable = function() {
|
||||
this.newUpdateAvailable = true;
|
||||
}
|
||||
|
||||
this.clickedNewUpdateAnnouncement = function() {
|
||||
this.newUpdateAvailable = false;
|
||||
alert("A new update is ready to install. Please use the top-level 'Updates' menu to manage installation.")
|
||||
}
|
||||
|
||||
|
||||
/* Rooms */
|
||||
|
||||
this.componentManager = componentManager;
|
||||
this.rooms = [];
|
||||
|
||||
modelManager.addItemSyncObserver("room-bar", "SN|Component", (allItems, validItems, deletedItems, source) => {
|
||||
var incomingRooms = allItems.filter((candidate) => {return candidate.area == "rooms"});
|
||||
this.rooms = _.uniq(this.rooms.concat(incomingRooms)).filter((candidate) => {return !candidate.deleted});
|
||||
});
|
||||
|
||||
componentManager.registerHandler({identifier: "roomBar", areas: ["rooms", "modal"], activationHandler: (component) => {
|
||||
// RIP: There used to be code here that checked if component.active was true, and if so, displayed the component.
|
||||
// However, we no longer want to persist active state for footer extensions. If you open Extensions on one computer,
|
||||
// it shouldn't open on another computer. Active state should only be persisted for persistent extensions, like Folders.
|
||||
}, actionHandler: (component, action, data) => {
|
||||
if(action == "set-size") {
|
||||
component.setLastSize(data);
|
||||
}
|
||||
}, focusHandler: (component, focused) => {
|
||||
if(component.isEditor() && focused) {
|
||||
this.closeAllRooms();
|
||||
this.closeAccountMenu();
|
||||
}
|
||||
}});
|
||||
|
||||
$rootScope.$on("editorFocused", () => {
|
||||
this.closeAllRooms();
|
||||
this.closeAccountMenu();
|
||||
}
|
||||
}});
|
||||
})
|
||||
|
||||
$rootScope.$on("editorFocused", () => {
|
||||
this.closeAllRooms();
|
||||
this.closeAccountMenu();
|
||||
})
|
||||
|
||||
this.onRoomDismiss = function(room) {
|
||||
room.showRoom = false;
|
||||
}
|
||||
|
||||
this.closeAllRooms = function() {
|
||||
for(var room of this.rooms) {
|
||||
this.onRoomDismiss = function(room) {
|
||||
room.showRoom = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.selectRoom = function(room) {
|
||||
room.showRoom = !room.showRoom;
|
||||
}
|
||||
|
||||
this.closeAllRooms = function() {
|
||||
for(var room of this.rooms) {
|
||||
room.showRoom = false;
|
||||
}
|
||||
}
|
||||
|
||||
this.selectRoom = function(room) {
|
||||
room.showRoom = !room.showRoom;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -36,7 +36,8 @@ angular.module('app')
|
||||
|
||||
function load() {
|
||||
// pass keys to storageManager to decrypt storage
|
||||
storageManager.setKeys(passcodeManager.keys());
|
||||
// Update: Wait, why? passcodeManager already handles this.
|
||||
// storageManager.setKeys(passcodeManager.keys());
|
||||
|
||||
openDatabase();
|
||||
// Retrieve local data and begin sycing timer
|
||||
@@ -285,7 +286,7 @@ angular.module('app')
|
||||
})
|
||||
}
|
||||
} else {
|
||||
authManager.login(server, email, pw, false, {}, function(response){
|
||||
authManager.login(server, email, pw, false, false, {}, function(response){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -41,7 +41,14 @@ angular.module('app')
|
||||
|
||||
this.loadPreferences = function() {
|
||||
let prevSortValue = this.sortBy;
|
||||
|
||||
this.sortBy = authManager.getUserPrefValue("sortBy", "created_at");
|
||||
|
||||
if(this.sortBy == "updated_at") {
|
||||
// use client_updated_at instead
|
||||
this.sortBy = "client_updated_at";
|
||||
}
|
||||
|
||||
if(prevSortValue && prevSortValue != this.sortBy) {
|
||||
$timeout(() => {
|
||||
this.selectFirstNote();
|
||||
@@ -120,7 +127,7 @@ angular.module('app')
|
||||
var base = "";
|
||||
if(this.sortBy == "created_at") {
|
||||
base += " Date Added";
|
||||
} else if(this.sortBy == "updated_at") {
|
||||
} else if(this.sortBy == "client_updated_at") {
|
||||
base += " Date Modifed";
|
||||
} else if(this.sortBy == "title") {
|
||||
base += " Title";
|
||||
@@ -274,7 +281,7 @@ angular.module('app')
|
||||
}
|
||||
|
||||
this.selectedSortByUpdated = function() {
|
||||
this.setSortBy("updated_at");
|
||||
this.setSortBy("client_updated_at");
|
||||
this.sortDescending = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,14 @@ class AccountMenu {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager, $timeout, storageManager) {
|
||||
controller($scope, $rootScope, authManager, modelManager, syncManager, dbManager, passcodeManager,
|
||||
$timeout, storageManager, $compile, archiveManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {mergeLocal: true, url: syncManager.serverURL, ephemeral: false};
|
||||
$scope.user = authManager.user;
|
||||
$scope.server = syncManager.serverURL;
|
||||
$scope.securityUpdateAvailable = authManager.checkForSecurityUpdate();
|
||||
|
||||
$scope.close = function() {
|
||||
$timeout(() => {
|
||||
@@ -29,63 +31,6 @@ class AccountMenu {
|
||||
$scope.canAddPasscode = !authManager.isEphemeralSession();
|
||||
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
$scope.newPasswordData = {};
|
||||
|
||||
$scope.showPasswordChangeForm = function() {
|
||||
$scope.newPasswordData.showForm = true;
|
||||
}
|
||||
|
||||
$scope.submitPasswordChange = function() {
|
||||
|
||||
let newPass = $scope.newPasswordData.newPassword;
|
||||
|
||||
if(!newPass || newPass.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(newPass != $scope.newPasswordData.newPasswordConfirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var email = $scope.user.email;
|
||||
if(!email) {
|
||||
alert("We don't have your email stored. Please log out then log back in to fix this issue.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.newPasswordData.status = "Generating New Keys...";
|
||||
$scope.newPasswordData.showForm = false;
|
||||
|
||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||
syncManager.sync(function(response){
|
||||
authManager.changePassword(email, newPass, function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error changing your password. Please try again.");
|
||||
$scope.newPasswordData.status = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// re-encrypt all items
|
||||
$scope.newPasswordData.status = "Re-encrypting all items with your new key...";
|
||||
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync(function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.")
|
||||
return;
|
||||
}
|
||||
$scope.newPasswordData.status = "Successfully changed password and re-encrypted all items.";
|
||||
$timeout(function(){
|
||||
alert("Your password has been changed, and your items successfully re-encrypted and synced. You must sign out of all other signed in applications and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
});
|
||||
})
|
||||
}, null, "submitPasswordChange")
|
||||
}
|
||||
|
||||
$scope.submitMfaForm = function() {
|
||||
var params = {};
|
||||
@@ -113,10 +58,13 @@ class AccountMenu {
|
||||
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
$timeout(function(){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, $scope.formData.ephemeral, extraParams,
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password,
|
||||
$scope.formData.ephemeral, $scope.formData.strictSignin, extraParams,
|
||||
(response) => {
|
||||
if(!response || response.error) {
|
||||
|
||||
syncManager.unlockSyncing();
|
||||
|
||||
$scope.formData.status = null;
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
|
||||
@@ -133,8 +81,8 @@ class AccountMenu {
|
||||
$timeout(() => {
|
||||
$scope.formData.showLogin = true;
|
||||
$scope.formData.mfa = null;
|
||||
if(error.message) { alert(error.message); }
|
||||
})
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,6 +152,13 @@ class AccountMenu {
|
||||
}
|
||||
}
|
||||
|
||||
$scope.openPasswordWizard = function(type) {
|
||||
// Close the account menu
|
||||
$scope.close();
|
||||
|
||||
authManager.presentPasswordWizard(type);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -253,7 +208,7 @@ class AccountMenu {
|
||||
var message = `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`;
|
||||
alert(message);
|
||||
} else {
|
||||
alert("Your data was successfully imported.")
|
||||
alert("Your data has been successfully imported.")
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
@@ -291,9 +246,9 @@ class AccountMenu {
|
||||
var onDataReady = function(errorCount) {
|
||||
var items = modelManager.mapResponseItemsToLocalModels(data.items, ModelManager.MappingSourceFileImport);
|
||||
items.forEach(function(item){
|
||||
item.setDirty(true);
|
||||
item.setDirty(true, true);
|
||||
item.deleted = false;
|
||||
item.markAllReferencesDirty();
|
||||
item.markAllReferencesDirty(true);
|
||||
|
||||
// We don't want to activate any components during import process in case of exceptions
|
||||
// breaking up the import proccess
|
||||
@@ -308,26 +263,28 @@ class AccountMenu {
|
||||
}.bind(this)
|
||||
|
||||
if(data.auth_params) {
|
||||
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: password}, data.auth_params), function(keys){
|
||||
SFJS.crypto.computeEncryptionKeysForUser(password, data.auth_params).then((keys) => {
|
||||
try {
|
||||
SFItemTransformer.decryptMultipleItems(data.items, keys, false); /* throws = false as we don't want to interrupt all decryption if just one fails */
|
||||
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
||||
data.items.forEach(function(item){
|
||||
item.enc_item_key = null;
|
||||
item.auth_hash = null;
|
||||
});
|
||||
SFJS.itemTransformer.decryptMultipleItems(data.items, keys, false) /* throws = false as we don't want to interrupt all decryption if just one fails */
|
||||
.then(() => {
|
||||
// delete items enc_item_key since the user's actually key will do the encrypting once its passed off
|
||||
data.items.forEach(function(item){
|
||||
item.enc_item_key = null;
|
||||
item.auth_hash = null;
|
||||
});
|
||||
|
||||
var errorCount = 0;
|
||||
// Don't import items that didn't decrypt properly
|
||||
data.items = data.items.filter(function(item){
|
||||
if(item.errorDecrypting) {
|
||||
errorCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
var errorCount = 0;
|
||||
// Don't import items that didn't decrypt properly
|
||||
data.items = data.items.filter(function(item){
|
||||
if(item.errorDecrypting) {
|
||||
errorCount++;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
onDataReady(errorCount);
|
||||
})
|
||||
|
||||
onDataReady(errorCount);
|
||||
}
|
||||
catch (e) {
|
||||
console.log("Error decrypting", e);
|
||||
@@ -335,7 +292,7 @@ class AccountMenu {
|
||||
callback(null);
|
||||
return;
|
||||
}
|
||||
}.bind(this));
|
||||
});
|
||||
} else {
|
||||
onDataReady();
|
||||
}
|
||||
@@ -345,166 +302,10 @@ class AccountMenu {
|
||||
Export
|
||||
*/
|
||||
|
||||
function loadZip(callback) {
|
||||
if(window.zip) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = "/assets/zip/zip.js";
|
||||
scriptTag.async = false;
|
||||
var headTag = document.getElementsByTagName('head')[0];
|
||||
headTag.appendChild(scriptTag);
|
||||
scriptTag.onload = function() {
|
||||
zip.workerScriptsPath = "assets/zip/";
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadZippedNotes(notes) {
|
||||
loadZip(function(){
|
||||
|
||||
zip.createWriter(new zip.BlobWriter("application/zip"), function(zipWriter) {
|
||||
|
||||
var index = 0;
|
||||
function nextFile() {
|
||||
var note = notes[index];
|
||||
var blob = new Blob([note.text], {type: 'text/plain'});
|
||||
zipWriter.add(`${note.title}-${note.uuid}.txt`, new zip.BlobReader(blob), function() {
|
||||
index++;
|
||||
if(index < notes.length) {
|
||||
nextFile();
|
||||
} else {
|
||||
zipWriter.close(function(blob) {
|
||||
downloadData(blob, `Notes Txt Archive - ${new Date()}.zip`)
|
||||
zipWriter = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nextFile();
|
||||
}, onerror);
|
||||
})
|
||||
}
|
||||
|
||||
var textFile = null;
|
||||
|
||||
function hrefForData(data) {
|
||||
// If we are replacing a previously generated file we need to
|
||||
// manually revoke the object URL to avoid memory leaks.
|
||||
if (textFile !== null) {
|
||||
window.URL.revokeObjectURL(textFile);
|
||||
}
|
||||
|
||||
textFile = window.URL.createObjectURL(data);
|
||||
|
||||
// returns a URL you can use as a href
|
||||
return textFile;
|
||||
}
|
||||
|
||||
function downloadData(data, fileName) {
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', fileName);
|
||||
link.href = hrefForData(data);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
$scope.downloadDataArchive = function() {
|
||||
// download in Standard File format
|
||||
var keys, authParams, protocolVersion;
|
||||
if($scope.archiveFormData.encrypted) {
|
||||
if(authManager.offline() && passcodeManager.hasPasscode()) {
|
||||
keys = passcodeManager.keys();
|
||||
authParams = passcodeManager.passcodeAuthParams();
|
||||
protocolVersion = authParams.version;
|
||||
} else {
|
||||
keys = authManager.keys();
|
||||
authParams = authManager.getAuthParams();
|
||||
protocolVersion = authManager.protocolVersion();
|
||||
}
|
||||
}
|
||||
var data = $scope.itemsData(keys, authParams, protocolVersion);
|
||||
downloadData(data, `SN Archive - ${new Date()}.txt`);
|
||||
|
||||
// download as zipped plain text files
|
||||
if(!keys) {
|
||||
var notes = modelManager.allItemsMatchingTypes(["Note"]);
|
||||
downloadZippedNotes(notes);
|
||||
}
|
||||
archiveManager.downloadBackup($scope.archiveFormData.encrypted);
|
||||
}
|
||||
|
||||
$scope.itemsData = function(keys, authParams, protocolVersion) {
|
||||
let data = modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
|
||||
let blobData = new Blob([data], {type: 'text/json'});
|
||||
return blobData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Advanced
|
||||
|
||||
$scope.reencryptPressed = function() {
|
||||
if(!confirm("Are you sure you want to re-encrypt and sync all your items? This is useful when updates are made to our encryption specification. You should have been instructed to come here from our website.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!confirm("It is highly recommended that you download a backup of your data before proceeding. Press cancel to go back. Note that this procedure can take some time, depending on the number of items you have. Do not close the app during process.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync(function(response){
|
||||
if(response.error) {
|
||||
alert("There was an error re-encrypting your items. You should try syncing again. If all else fails, you should restore your notes from backup.")
|
||||
return;
|
||||
}
|
||||
|
||||
$timeout(function(){
|
||||
alert("Your items have been successfully re-encrypted and synced. You must sign out of all other signed in applications (mobile, desktop, web) and sign in again, or else you may corrupt your data.")
|
||||
$scope.newPasswordData = {};
|
||||
}, 1000)
|
||||
}, null, "reencryptPressed");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 002 Update
|
||||
|
||||
$scope.securityUpdateAvailable = function() {
|
||||
var keys = authManager.keys()
|
||||
return keys && !keys.ak;
|
||||
}
|
||||
|
||||
$scope.clickedSecurityUpdate = function() {
|
||||
if(!$scope.securityUpdateData) {
|
||||
$scope.securityUpdateData = {};
|
||||
}
|
||||
$scope.securityUpdateData.showForm = true;
|
||||
}
|
||||
|
||||
$scope.submitSecurityUpdateForm = function() {
|
||||
$scope.securityUpdateData.processing = true;
|
||||
var authParams = authManager.getAuthParams();
|
||||
|
||||
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: $scope.securityUpdateData.password}, authParams), function(keys){
|
||||
if(keys.mk !== authManager.keys().mk) {
|
||||
alert("Invalid password. Please try again.");
|
||||
$timeout(function(){
|
||||
$scope.securityUpdateData.processing = false;
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.saveKeys(keys);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Encryption Status
|
||||
*/
|
||||
|
||||
@@ -71,7 +71,7 @@ class ActionsMenu {
|
||||
this.executeAction(subaction, extension, parentAction);
|
||||
$event.stopPropagation();
|
||||
},
|
||||
title: subaction.label,
|
||||
label: subaction.label,
|
||||
subtitle: subaction.desc,
|
||||
spinnerClass: subaction.running ? 'info' : null
|
||||
}
|
||||
|
||||
38
app/assets/javascripts/app/directives/views/inputModal.js
Normal file
38
app/assets/javascripts/app/directives/views/inputModal.js
Normal file
@@ -0,0 +1,38 @@
|
||||
class InputModal {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/input-modal.html";
|
||||
this.scope = {
|
||||
type: "=",
|
||||
title: "=",
|
||||
message: "=",
|
||||
placeholder: "=",
|
||||
callback: "&"
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
$scope.el = el;
|
||||
}
|
||||
|
||||
controller($scope, modelManager, archiveManager, authManager, syncManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {};
|
||||
|
||||
$scope.dismiss = function() {
|
||||
$scope.el.remove();
|
||||
$scope.$destroy();
|
||||
}
|
||||
|
||||
$scope.submit = function() {
|
||||
$scope.callback()($scope.formData.input);
|
||||
$scope.dismiss();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app').directive('inputModal', () => new InputModal);
|
||||
@@ -6,7 +6,7 @@ class MenuRow {
|
||||
this.templateUrl = "directives/menu-row.html";
|
||||
this.scope = {
|
||||
circle: "=",
|
||||
title: "=",
|
||||
label: "=",
|
||||
subtite: "=",
|
||||
hasButton: "=",
|
||||
buttonText: "=",
|
||||
@@ -14,7 +14,8 @@ class MenuRow {
|
||||
buttonAction: "&",
|
||||
spinnerClass: "=",
|
||||
subRows: "=",
|
||||
faded: "="
|
||||
faded: "=",
|
||||
desc: "="
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
227
app/assets/javascripts/app/directives/views/passwordWizard.js
Normal file
227
app/assets/javascripts/app/directives/views/passwordWizard.js
Normal file
@@ -0,0 +1,227 @@
|
||||
class PasswordWizard {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "directives/password-wizard.html";
|
||||
this.scope = {
|
||||
type: "="
|
||||
};
|
||||
}
|
||||
|
||||
link($scope, el, attrs) {
|
||||
$scope.el = el;
|
||||
}
|
||||
|
||||
controller($scope, modelManager, archiveManager, authManager, syncManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.dismiss = function() {
|
||||
$scope.el.remove();
|
||||
$scope.$destroy();
|
||||
}
|
||||
|
||||
$scope.syncStatus = syncManager.syncStatus;
|
||||
$scope.formData = {};
|
||||
|
||||
const IntroStep = 0;
|
||||
const BackupStep = 1;
|
||||
const SignoutStep = 2;
|
||||
const PasswordStep = 3;
|
||||
const SyncStep = 4;
|
||||
const FinishStep = 5;
|
||||
|
||||
let DefaultContinueTitle = "Continue";
|
||||
$scope.continueTitle = DefaultContinueTitle;
|
||||
|
||||
$scope.step = IntroStep;
|
||||
|
||||
$scope.titleForStep = function(step) {
|
||||
switch (step) {
|
||||
case BackupStep:
|
||||
return "Download a backup of your data";
|
||||
case SignoutStep:
|
||||
return "Sign out of all your devices";
|
||||
case PasswordStep:
|
||||
return $scope.changePassword ? "Password information" : "Enter your current password";
|
||||
case SyncStep:
|
||||
return "Encrypt and sync data with new keys";
|
||||
case FinishStep:
|
||||
return "Sign back in to your devices";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.configure = function() {
|
||||
if($scope.type == "change-pw") {
|
||||
$scope.title = "Change Password";
|
||||
$scope.changePassword = true;
|
||||
} else if($scope.type == "upgrade-security") {
|
||||
$scope.title = "Security Update";
|
||||
$scope.securityUpdate = true;
|
||||
}
|
||||
}();
|
||||
|
||||
$scope.continue = function() {
|
||||
|
||||
if($scope.step == FinishStep) {
|
||||
$scope.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
let next = () => {
|
||||
$scope.step += 1;
|
||||
$scope.initializeStep($scope.step);
|
||||
}
|
||||
|
||||
var preprocessor = $scope.preprocessorForStep($scope.step);
|
||||
if(preprocessor) {
|
||||
preprocessor(() => {
|
||||
next();
|
||||
})
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.downloadBackup = function(encrypted) {
|
||||
archiveManager.downloadBackup(encrypted);
|
||||
}
|
||||
|
||||
$scope.preprocessorForStep = function(step) {
|
||||
if(step == PasswordStep) {
|
||||
return (callback) => {
|
||||
$scope.showSpinner = true;
|
||||
$scope.continueTitle = "Generating Keys...";
|
||||
$timeout(() => {
|
||||
$scope.validateCurrentPassword((success) => {
|
||||
$scope.showSpinner = false;
|
||||
$scope.continueTitle = DefaultContinueTitle;
|
||||
if(success) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let FailedSyncMessage = "There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.";
|
||||
|
||||
$scope.initializeStep = function(step) {
|
||||
if(step == SyncStep) {
|
||||
$scope.lockContinue = true;
|
||||
$scope.formData.status = "Processing encryption keys...";
|
||||
$scope.formData.processing = true;
|
||||
|
||||
$scope.processPasswordChange((passwordSuccess) => {
|
||||
$scope.formData.statusError = !passwordSuccess;
|
||||
$scope.formData.processing = passwordSuccess;
|
||||
|
||||
if(passwordSuccess) {
|
||||
$scope.formData.status = "Encrypting and syncing data with new keys...";
|
||||
|
||||
$scope.resyncData((syncSuccess) => {
|
||||
$scope.formData.statusError = !syncSuccess;
|
||||
$scope.formData.processing = !syncSuccess;
|
||||
if(syncSuccess) {
|
||||
$scope.lockContinue = false;
|
||||
|
||||
if($scope.changePassword) {
|
||||
$scope.formData.status = "Successfully changed password and synced all items.";
|
||||
} else if($scope.securityUpdate) {
|
||||
$scope.formData.status = "Successfully performed security update and synced all items.";
|
||||
}
|
||||
} else {
|
||||
$scope.formData.status = FailedSyncMessage;
|
||||
}
|
||||
})
|
||||
} else {
|
||||
$scope.formData.status = "Unable to process your password. Please try again.";
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
else if(step == FinishStep) {
|
||||
$scope.continueTitle = "Finish";
|
||||
}
|
||||
}
|
||||
|
||||
$scope.validateCurrentPassword = function(callback) {
|
||||
let currentPassword = $scope.formData.currentPassword;
|
||||
let newPass = $scope.securityUpdate ? currentPassword : $scope.formData.newPassword;
|
||||
|
||||
if($scope.changePassword) {
|
||||
if(!newPass || newPass.length == 0) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if(newPass != $scope.formData.newPasswordConfirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
$scope.formData.status = null;
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(!authManager.user.email) {
|
||||
alert("We don't have your email stored. Please log out then log back in to fix this issue.");
|
||||
$scope.formData.status = null;
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure value for current password matches what's saved
|
||||
let authParams = authManager.getAuthParams();
|
||||
let password = $scope.formData.currentPassword;
|
||||
SFJS.crypto.computeEncryptionKeysForUser(password, authParams).then((keys) => {
|
||||
let success = keys.mk === authManager.keys().mk;
|
||||
if(success) {
|
||||
this.currentServerPw = keys.pw;
|
||||
} else {
|
||||
alert("The current password you entered is not correct. Please try again.");
|
||||
}
|
||||
$timeout(() => callback(success));
|
||||
});
|
||||
}
|
||||
|
||||
$scope.resyncData = function(callback) {
|
||||
modelManager.setAllItemsDirty();
|
||||
syncManager.sync((response) => {
|
||||
if(response.error) {
|
||||
alert(FailedSyncMessage)
|
||||
$timeout(() => callback(false));
|
||||
} else {
|
||||
$timeout(() => callback(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$scope.processPasswordChange = function(callback) {
|
||||
let newUserPassword = $scope.securityUpdate ? $scope.formData.currentPassword : $scope.formData.newPassword;
|
||||
|
||||
let currentServerPw = this.currentServerPw;
|
||||
|
||||
SFJS.crypto.generateInitialKeysAndAuthParamsForUser(authManager.user.email, newUserPassword).then((results) => {
|
||||
let newKeys = results.keys;
|
||||
let newAuthParams = results.authParams;
|
||||
|
||||
// perform a sync beforehand to pull in any last minutes changes before we change the encryption key (and thus cant decrypt new changes)
|
||||
syncManager.sync((response) => {
|
||||
authManager.changePassword(currentServerPw, newKeys, newAuthParams, (response) => {
|
||||
if(response.error) {
|
||||
alert(response.error.message ? response.error.message : "There was an error changing your password. Please try again.");
|
||||
$timeout(() => callback(false));
|
||||
} else {
|
||||
$timeout(() => callback(true));
|
||||
}
|
||||
})
|
||||
}, null, "submitPasswordChange")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').directive('passwordWizard', () => new PasswordWizard);
|
||||
@@ -9,7 +9,7 @@ class Item {
|
||||
this.observers = [];
|
||||
|
||||
if(!this.uuid) {
|
||||
this.uuid = SFJS.crypto.generateUUID();
|
||||
this.uuid = SFJS.crypto.generateUUIDSync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ class Item {
|
||||
this.updated_at = new Date();
|
||||
}
|
||||
|
||||
// Allows the getter to be re-invoked
|
||||
this._client_updated_at = null;
|
||||
|
||||
if(json.content) {
|
||||
this.mapContentToLocalProperties(this.contentObject);
|
||||
} else if(json.deleted == true) {
|
||||
@@ -68,7 +71,7 @@ class Item {
|
||||
// Subclasses can override
|
||||
}
|
||||
|
||||
setDirty(dirty) {
|
||||
setDirty(dirty, dontUpdateClientDate) {
|
||||
this.dirty = dirty;
|
||||
|
||||
// Allows the syncManager to check if an item has been marked dirty after a sync has been started
|
||||
@@ -81,14 +84,22 @@ class Item {
|
||||
this.dirtyCount = 0;
|
||||
}
|
||||
|
||||
if(dirty && !dontUpdateClientDate) {
|
||||
// Set the client modified date to now if marking the item as dirty
|
||||
this.client_updated_at = new Date();
|
||||
} else if(!this.hasRawClientUpdatedAtValue()) {
|
||||
// copy updated_at
|
||||
this.client_updated_at = new Date(this.updated_at);
|
||||
}
|
||||
|
||||
if(dirty) {
|
||||
this.notifyObserversOfChange();
|
||||
}
|
||||
}
|
||||
|
||||
markAllReferencesDirty() {
|
||||
markAllReferencesDirty(dontUpdateClientDate) {
|
||||
this.allReferencedObjects().forEach(function(reference){
|
||||
reference.setDirty(true);
|
||||
reference.setDirty(true, dontUpdateClientDate);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -214,6 +225,28 @@ class Item {
|
||||
return this.getAppDataItem("locked");
|
||||
}
|
||||
|
||||
hasRawClientUpdatedAtValue() {
|
||||
return this.getAppDataItem("client_updated_at") != null;
|
||||
}
|
||||
|
||||
get client_updated_at() {
|
||||
if(!this._client_updated_at) {
|
||||
var saved = this.getAppDataItem("client_updated_at");
|
||||
if(saved) {
|
||||
this._client_updated_at = new Date(saved);
|
||||
} else {
|
||||
this._client_updated_at = new Date(this.updated_at);
|
||||
}
|
||||
}
|
||||
return this._client_updated_at;
|
||||
}
|
||||
|
||||
set client_updated_at(date) {
|
||||
this._client_updated_at = date;
|
||||
|
||||
this.setAppDataItem("client_updated_at", date);
|
||||
}
|
||||
|
||||
/*
|
||||
During sync conflicts, when determing whether to create a duplicate for an item, we can omit keys that have no
|
||||
meaningful weight and can be ignored. For example, if one component has active = true and another component has active = false,
|
||||
@@ -245,7 +278,7 @@ class Item {
|
||||
}
|
||||
|
||||
updatedAtString() {
|
||||
return this.dateToLocalizedString(this.updated_at);
|
||||
return this.dateToLocalizedString(this.client_updated_at);
|
||||
}
|
||||
|
||||
dateToLocalizedString(date) {
|
||||
|
||||
@@ -3,43 +3,44 @@ class ItemParams {
|
||||
constructor(item, keys, version) {
|
||||
this.item = item;
|
||||
this.keys = keys;
|
||||
this.version = version || "002";
|
||||
this.version = version || SFJS.version();
|
||||
}
|
||||
|
||||
paramsForExportFile(includeDeleted) {
|
||||
async paramsForExportFile(includeDeleted) {
|
||||
this.additionalFields = ["updated_at"];
|
||||
this.forExportFile = true;
|
||||
if(includeDeleted) {
|
||||
return this.__params();
|
||||
} else {
|
||||
return _.omit(this.__params(), ["deleted"]);
|
||||
var result = await this.__params();
|
||||
return _.omit(result, ["deleted"]);
|
||||
}
|
||||
}
|
||||
|
||||
paramsForExtension() {
|
||||
async paramsForExtension() {
|
||||
return this.paramsForExportFile();
|
||||
}
|
||||
|
||||
paramsForLocalStorage() {
|
||||
async paramsForLocalStorage() {
|
||||
this.additionalFields = ["updated_at", "dirty", "errorDecrypting"];
|
||||
this.forExportFile = true;
|
||||
return this.__params();
|
||||
}
|
||||
|
||||
paramsForSync() {
|
||||
async paramsForSync() {
|
||||
return this.__params();
|
||||
}
|
||||
|
||||
__params() {
|
||||
async __params() {
|
||||
|
||||
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
|
||||
|
||||
var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at};
|
||||
if(!this.item.errorDecrypting) {
|
||||
// Items should always be encrypted for export files. Only respect item.doNotEncrypt for remote sync params;
|
||||
// Items should always be encrypted for export files. Only respect item.doNotEncrypt for remote sync params.
|
||||
var doNotEncrypt = this.item.doNotEncrypt() && !this.forExportFile;
|
||||
if(this.keys && !doNotEncrypt) {
|
||||
var encryptedParams = SFItemTransformer.encryptItem(this.item, this.keys, this.version);
|
||||
var encryptedParams = await SFJS.itemTransformer.encryptItem(this.item, this.keys, this.version);
|
||||
_.merge(params, encryptedParams);
|
||||
|
||||
if(this.version !== "001") {
|
||||
@@ -47,7 +48,7 @@ class ItemParams {
|
||||
}
|
||||
}
|
||||
else {
|
||||
params.content = this.forExportFile ? this.item.createContentJSONFromProperties() : "000" + SFJS.crypto.base64(JSON.stringify(this.item.createContentJSONFromProperties()));
|
||||
params.content = this.forExportFile ? this.item.createContentJSONFromProperties() : "000" + await SFJS.crypto.base64(JSON.stringify(this.item.createContentJSONFromProperties()));
|
||||
if(!this.forExportFile) {
|
||||
params.enc_item_key = null;
|
||||
params.auth_hash = null;
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
class ActionsManager {
|
||||
|
||||
constructor(httpManager, modelManager, authManager, syncManager) {
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.syncManager = syncManager;
|
||||
constructor(httpManager, modelManager, authManager, syncManager, $rootScope, $compile, $timeout) {
|
||||
this.httpManager = httpManager;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.syncManager = syncManager;
|
||||
this.$rootScope = $rootScope;
|
||||
this.$compile = $compile;
|
||||
this.$timeout = $timeout;
|
||||
|
||||
// Used when decrypting old items with new keys. This array is only kept in memory.
|
||||
this.previousPasswords = [];
|
||||
}
|
||||
|
||||
get extensions() {
|
||||
@@ -46,47 +52,92 @@ class ActionsManager {
|
||||
}
|
||||
}
|
||||
|
||||
executeAction(action, extension, item, callback) {
|
||||
async executeAction(action, extension, item, callback) {
|
||||
|
||||
var customCallback = function(response) {
|
||||
var customCallback = (response) => {
|
||||
action.running = false;
|
||||
callback(response);
|
||||
this.$timeout(() => {
|
||||
callback(response);
|
||||
})
|
||||
}
|
||||
|
||||
action.running = true;
|
||||
|
||||
let decrypted = action.access_type == "decrypted";
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
var triedPasswords = [];
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
action.error = false;
|
||||
var items = response.items || [response.item];
|
||||
SFItemTransformer.decryptMultipleItems(items, this.authManager.keys());
|
||||
items = this.modelManager.mapResponseItemsToLocalModels(items, ModelManager.MappingSourceRemoteActionRetrieved);
|
||||
for(var item of items) {
|
||||
item.setDirty(true);
|
||||
let handleResponseDecryption = async (response, keys, merge) => {
|
||||
var item = response.item;
|
||||
|
||||
await SFJS.itemTransformer.decryptItem(item, keys);
|
||||
|
||||
if(!item.errorDecrypting) {
|
||||
if(merge) {
|
||||
var items = this.modelManager.mapResponseItemsToLocalModels([item], ModelManager.MappingSourceRemoteActionRetrieved);
|
||||
for(var mappedItem of items) {
|
||||
mappedItem.setDirty(true);
|
||||
}
|
||||
this.syncManager.sync(null);
|
||||
customCallback({items: items});
|
||||
}.bind(this), function(response){
|
||||
customCallback({item: item});
|
||||
} else {
|
||||
item = this.modelManager.createItem(item, true /* Dont notify observers */);
|
||||
customCallback({item: item});
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
// Error decrypting
|
||||
if(!response.auth_params) {
|
||||
// In some cases revisions were missing auth params. Instruct the user to email us to get this remedied.
|
||||
alert("We were unable to decrypt this revision using your current keys, and this revision is missing metadata that would allow us to try different keys to decrypt it. This can likely be fixed with some manual intervention. Please email hello@standardnotes.org for assistance.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Try previous passwords
|
||||
for(let passwordCandidate of this.previousPasswords) {
|
||||
if(triedPasswords.includes(passwordCandidate)) {
|
||||
continue;
|
||||
}
|
||||
triedPasswords.push(passwordCandidate);
|
||||
|
||||
var keyResults = await SFJS.crypto.computeEncryptionKeysForUser(passwordCandidate, response.auth_params);
|
||||
if(!keyResults) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var success = await handleResponseDecryption(response, keyResults, merge);
|
||||
if(success) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.presentPasswordModal((password) => {
|
||||
this.previousPasswords.push(password);
|
||||
handleResponseDecryption(response, keys, merge);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (action.verb) {
|
||||
case "get": {
|
||||
this.httpManager.getAbsolute(action.url, {}, (response) => {
|
||||
action.error = false;
|
||||
handleResponseDecryption(response, this.authManager.keys(), true);
|
||||
}, (response) => {
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "render": {
|
||||
|
||||
this.httpManager.getAbsolute(action.url, {}, function(response){
|
||||
this.httpManager.getAbsolute(action.url, {}, (response) => {
|
||||
action.error = false;
|
||||
SFItemTransformer.decryptItem(response.item, this.authManager.keys());
|
||||
var item = this.modelManager.createItem(response.item, true /* Dont notify observers */);
|
||||
customCallback({item: item});
|
||||
|
||||
}.bind(this), function(response){
|
||||
handleResponseDecryption(response, this.authManager.keys(), false);
|
||||
}, (response) => {
|
||||
action.error = true;
|
||||
customCallback(null);
|
||||
})
|
||||
@@ -102,22 +153,15 @@ class ActionsManager {
|
||||
}
|
||||
|
||||
case "post": {
|
||||
var params = {};
|
||||
this.outgoingParamsForItem(item, extension, decrypted).then((itemParams) => {
|
||||
var params = {
|
||||
items: [itemParams] // Wrap it in an array
|
||||
}
|
||||
|
||||
if(action.all) {
|
||||
var items = this.modelManager.allItemsMatchingTypes(action.content_types);
|
||||
params.items = items.map(function(item){
|
||||
var params = this.outgoingParamsForItem(item, extension, decrypted);
|
||||
return params;
|
||||
}.bind(this))
|
||||
|
||||
} else {
|
||||
params.items = [this.outgoingParamsForItem(item, extension, decrypted)];
|
||||
}
|
||||
|
||||
this.performPost(action, extension, params, function(response){
|
||||
customCallback(response);
|
||||
});
|
||||
this.performPost(action, extension, params, function(response){
|
||||
customCallback(response);
|
||||
});
|
||||
})
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -130,7 +174,7 @@ class ActionsManager {
|
||||
action.lastExecuted = new Date();
|
||||
}
|
||||
|
||||
outgoingParamsForItem(item, extension, decrypted = false) {
|
||||
async outgoingParamsForItem(item, extension, decrypted = false) {
|
||||
var keys = this.authManager.keys();
|
||||
if(decrypted) {
|
||||
keys = null;
|
||||
@@ -154,6 +198,17 @@ class ActionsManager {
|
||||
})
|
||||
}
|
||||
|
||||
presentPasswordModal(callback) {
|
||||
|
||||
var scope = this.$rootScope.$new(true);
|
||||
scope.type = "password";
|
||||
scope.title = "Decryption Assistance";
|
||||
scope.message = "Unable to decrypt this item with your current keys. Please enter your account password at the time of this revision.";
|
||||
scope.callback = callback;
|
||||
var el = this.$compile( "<input-modal type='type' message='message' title='title' callback='callback'></input-modal>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('actionsManager', ActionsManager);
|
||||
|
||||
120
app/assets/javascripts/app/services/archiveManager.js
Normal file
120
app/assets/javascripts/app/services/archiveManager.js
Normal file
@@ -0,0 +1,120 @@
|
||||
class ArchiveManager {
|
||||
|
||||
constructor(passcodeManager, authManager, modelManager) {
|
||||
this.passcodeManager = passcodeManager;
|
||||
this.authManager = authManager;
|
||||
this.modelManager = modelManager;
|
||||
}
|
||||
|
||||
/*
|
||||
Public
|
||||
*/
|
||||
|
||||
downloadBackup(encrypted) {
|
||||
// download in Standard File format
|
||||
var keys, authParams, protocolVersion;
|
||||
if(encrypted) {
|
||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||
keys = this.passcodeManager.keys();
|
||||
authParams = this.passcodeManager.passcodeAuthParams();
|
||||
protocolVersion = authParams.version;
|
||||
} else {
|
||||
keys = this.authManager.keys();
|
||||
authParams = this.authManager.getAuthParams();
|
||||
protocolVersion = this.authManager.protocolVersion();
|
||||
}
|
||||
}
|
||||
this.__itemsData(keys, authParams, protocolVersion).then((data) => {
|
||||
this.__downloadData(data, `SN Archive - ${new Date()}.txt`);
|
||||
|
||||
// download as zipped plain text files
|
||||
if(!keys) {
|
||||
var notes = this.modelManager.allItemsMatchingTypes(["Note"]);
|
||||
this.__downloadZippedNotes(notes);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
Private
|
||||
*/
|
||||
|
||||
async __itemsData(keys, authParams, protocolVersion) {
|
||||
let data = await this.modelManager.getAllItemsJSONData(keys, authParams, protocolVersion);
|
||||
let blobData = new Blob([data], {type: 'text/json'});
|
||||
return blobData;
|
||||
}
|
||||
|
||||
__loadZip(callback) {
|
||||
if(window.zip) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = "/assets/zip/zip.js";
|
||||
scriptTag.async = false;
|
||||
var headTag = document.getElementsByTagName('head')[0];
|
||||
headTag.appendChild(scriptTag);
|
||||
scriptTag.onload = function() {
|
||||
zip.workerScriptsPath = "assets/zip/";
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
__downloadZippedNotes(notes) {
|
||||
this.__loadZip(() => {
|
||||
zip.createWriter(new zip.BlobWriter("application/zip"), (zipWriter) => {
|
||||
var index = 0;
|
||||
|
||||
let nextFile = () => {
|
||||
var note = notes[index];
|
||||
var blob = new Blob([note.text], {type: 'text/plain'});
|
||||
|
||||
var title = note.title.replace("/", "").replace("\\", "");
|
||||
|
||||
zipWriter.add(`${title}-${note.uuid}.txt`, new zip.BlobReader(blob), () => {
|
||||
index++;
|
||||
if(index < notes.length) {
|
||||
nextFile();
|
||||
} else {
|
||||
zipWriter.close((blob) => {
|
||||
this.__downloadData(blob, `Notes Txt Archive - ${new Date()}.zip`)
|
||||
zipWriter = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
nextFile();
|
||||
}, onerror);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
__hrefForData(data) {
|
||||
// If we are replacing a previously generated file we need to
|
||||
// manually revoke the object URL to avoid memory leaks.
|
||||
if (this.textFile !== null) {
|
||||
window.URL.revokeObjectURL(this.textFile);
|
||||
}
|
||||
|
||||
this.textFile = window.URL.createObjectURL(data);
|
||||
|
||||
// returns a URL you can use as a href
|
||||
return this.textFile;
|
||||
}
|
||||
|
||||
__downloadData(data, fileName) {
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', fileName);
|
||||
link.href = this.__hrefForData(data);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
angular.module('app').service('archiveManager', ArchiveManager);
|
||||
@@ -7,11 +7,11 @@ angular.module('app')
|
||||
return domain;
|
||||
}
|
||||
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager);
|
||||
this.$get = function($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager, $compile) {
|
||||
return new AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager, $compile);
|
||||
}
|
||||
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager) {
|
||||
function AuthManager($rootScope, $timeout, httpManager, modelManager, dbManager, storageManager, singletonManager, $compile) {
|
||||
|
||||
this.loadInitialData = function() {
|
||||
var userData = storageManager.getItem("user");
|
||||
@@ -77,21 +77,15 @@ angular.module('app')
|
||||
|
||||
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.
|
||||
return "002";
|
||||
} else {
|
||||
return "001";
|
||||
}
|
||||
}
|
||||
|
||||
this.costMinimumForVersion = function(version) {
|
||||
// all current versions have a min of 3000
|
||||
// future versions will increase this
|
||||
return 3000;
|
||||
}
|
||||
|
||||
this.isProtocolVersionSupported = function(version) {
|
||||
var supportedVersions = ["001", "002"];
|
||||
return supportedVersions.includes(version);
|
||||
return SFJS.supportedVersions().includes(version);
|
||||
}
|
||||
|
||||
this.getAuthParamsForEmail = function(url, email, extraParams, callback) {
|
||||
@@ -107,16 +101,11 @@ angular.module('app')
|
||||
})
|
||||
}
|
||||
|
||||
this.supportsPasswordDerivationCost = function(cost) {
|
||||
// some passwords are created on platforms with stronger pbkdf2 capabilities, like iOS,
|
||||
// which accidentally used 60,000 iterations (now adjusted), which CryptoJS can't handle here (WebCrypto can however).
|
||||
// if user has high password cost and is using browser that doesn't support WebCrypto,
|
||||
// we want to tell them that they can't login with this browser.
|
||||
return SFJS.crypto.supportsPasswordDerivationCost(cost);
|
||||
}
|
||||
this.login = function(url, email, password, ephemeral, strictSignin, extraParams, callback) {
|
||||
this.getAuthParamsForEmail(url, email, extraParams, (authParams) => {
|
||||
|
||||
this.login = function(url, email, password, ephemeral, extraParams, callback) {
|
||||
this.getAuthParamsForEmail(url, email, extraParams, function(authParams){
|
||||
// SF3 requires a unique identifier in the auth params
|
||||
authParams.identifier = email;
|
||||
|
||||
if(authParams.error) {
|
||||
callback(authParams);
|
||||
@@ -129,12 +118,27 @@ angular.module('app')
|
||||
}
|
||||
|
||||
if(!this.isProtocolVersionSupported(authParams.version)) {
|
||||
let message = "The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.org/help/security-update for more information.";
|
||||
var message;
|
||||
if(SFJS.isVersionNewerThanLibraryVersion(authParams.version)) {
|
||||
// The user has a new account type, but is signing in to an older client.
|
||||
message = "This version of the application does not support your newer account type. Please upgrade to the latest version of Standard Notes to sign in.";
|
||||
} else {
|
||||
// The user has a very old account type, which is no longer supported by this client
|
||||
message = "The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.org/help/security for more information.";
|
||||
}
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
if(!this.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
||||
if(SFJS.isProtocolVersionOutdated(authParams.version)) {
|
||||
let message = `The encryption version for your account, ${authParams.version}, is outdated and requires upgrade. You may proceed with login, but are advised to follow prompts for Security Updates once inside. Please visit standardnotes.org/help/security for more information.\n\nClick 'OK' to proceed with login.`
|
||||
if(!confirm(message)) {
|
||||
callback({error: {}});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(!SFJS.supportsPasswordDerivationCost(authParams.pw_cost)) {
|
||||
let message = "Your account was created on a platform with higher security capabilities than this browser supports. " +
|
||||
"If we attempted to generate your login keys here, it would take hours. " +
|
||||
"Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to log in."
|
||||
@@ -142,32 +146,42 @@ angular.module('app')
|
||||
return;
|
||||
}
|
||||
|
||||
var minimum = this.costMinimumForVersion(authParams.version);
|
||||
var minimum = SFJS.costMinimumForVersion(authParams.version);
|
||||
if(authParams.pw_cost < minimum) {
|
||||
let message = "Unable to login due to insecure password parameters. Please visit standardnotes.org/help/password-upgrade for more information.";
|
||||
let message = "Unable to login due to insecure password parameters. Please visit standardnotes.org/help/security for more information.";
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
|
||||
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: password}, authParams), function(keys){
|
||||
if(strictSignin) {
|
||||
// Refuse sign in if authParams.version is anything but the latest version
|
||||
var latestVersion = SFJS.version();
|
||||
if(authParams.version !== latestVersion) {
|
||||
let message = `Strict sign in refused server sign in parameters. The latest security version is ${latestVersion}, but your account is reported to have version ${authParams.version}. If you'd like to proceed with sign in anyway, please disable strict sign in and try again.`;
|
||||
callback({error: {message: message}});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SFJS.crypto.computeEncryptionKeysForUser(password, authParams).then((keys) => {
|
||||
var requestUrl = url + "/auth/sign_in";
|
||||
var params = _.merge({password: keys.pw, email: email}, extraParams);
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, (response) => {
|
||||
this.setEphemeral(ephemeral);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
this.checkForSecurityUpdate();
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
$timeout(() => callback(response));
|
||||
}, (response) => {
|
||||
console.error("Error logging in", response);
|
||||
if(typeof response !== 'object') {
|
||||
response = {error: {message: "A server error occurred while trying to sign in. Please try again."}};
|
||||
}
|
||||
callback(response);
|
||||
})
|
||||
$timeout(() => callback(response));
|
||||
});
|
||||
|
||||
}.bind(this));
|
||||
}.bind(this))
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
this.handleAuthResponse = function(response, email, url, authParams, keys) {
|
||||
@@ -191,88 +205,75 @@ angular.module('app')
|
||||
|
||||
this.saveKeys = function(keys) {
|
||||
this._keys = keys;
|
||||
// Doesn't need to be saved.
|
||||
// pw doesn't need to be saved.
|
||||
// storageManager.setItem("pw", keys.pw);
|
||||
storageManager.setItem("mk", keys.mk);
|
||||
storageManager.setItem("ak", keys.ak);
|
||||
}
|
||||
|
||||
this.register = function(url, email, password, ephemeral, callback) {
|
||||
SFJS.crypto.generateInitialEncryptionKeysForUser({password: password, email: email}, function(keys, authParams){
|
||||
SFJS.crypto.generateInitialKeysAndAuthParamsForUser(email, password).then((results) => {
|
||||
let keys = results.keys;
|
||||
let authParams = results.authParams;
|
||||
|
||||
var requestUrl = url + "/auth";
|
||||
var params = _.merge({password: keys.pw, email: email}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response){
|
||||
httpManager.postAbsolute(requestUrl, params, (response) => {
|
||||
this.setEphemeral(ephemeral);
|
||||
this.handleAuthResponse(response, email, url, authParams, keys);
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
}, (response) => {
|
||||
console.error("Registration error", response);
|
||||
if(typeof response !== 'object') {
|
||||
response = {error: {message: "A server error occurred while trying to register. Please try again."}};
|
||||
}
|
||||
callback(response);
|
||||
}.bind(this))
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
this.changePassword = function(email, new_password, callback) {
|
||||
SFJS.crypto.generateInitialEncryptionKeysForUser({password: new_password, email: email}, function(keys, authParams){
|
||||
var requestUrl = storageManager.getItem("server") + "/auth/change_pw";
|
||||
var params = _.merge({new_password: keys.pw}, authParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||
this.handleAuthResponse(response, email, null, authParams, keys);
|
||||
callback(response);
|
||||
}.bind(this), function(response){
|
||||
var error = response;
|
||||
if(!error) {
|
||||
error = {message: "Something went wrong while changing your password. Your password was not changed. Please try again."}
|
||||
}
|
||||
console.error("Change pw error", response);
|
||||
callback({error: error});
|
||||
})
|
||||
}.bind(this))
|
||||
});
|
||||
}
|
||||
|
||||
this.updateAuthParams = function(authParams, callback) {
|
||||
var requestUrl = storageManager.getItem("server") + "/auth/update";
|
||||
var params = authParams;
|
||||
httpManager.postAbsolute(requestUrl, params, function(response) {
|
||||
storageManager.setItem("auth_params", JSON.stringify(authParams));
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
}.bind(this), function(response){
|
||||
var error = response;
|
||||
console.error("Update error:", response);
|
||||
if(callback) {
|
||||
callback({error: error});
|
||||
this.changePassword = function(current_server_pw, newKeys, newAuthParams, callback) {
|
||||
let email = this.user.email;
|
||||
let newServerPw = newKeys.pw;
|
||||
|
||||
var requestUrl = storageManager.getItem("server") + "/auth/change_pw";
|
||||
var params = _.merge({new_password: newServerPw, current_password: current_server_pw}, newAuthParams);
|
||||
|
||||
httpManager.postAbsolute(requestUrl, params, (response) => {
|
||||
this.handleAuthResponse(response, email, null, newAuthParams, newKeys);
|
||||
callback(response);
|
||||
|
||||
// Allows security update status to be changed if neccessary
|
||||
this.checkForSecurityUpdate();
|
||||
}, (response) => {
|
||||
if(typeof response !== 'object') {
|
||||
response = {error: {message: "Something went wrong while changing your password. Your password was not changed. Please try again."}}
|
||||
}
|
||||
callback(response);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
this.checkForSecurityUpdate = function() {
|
||||
if(this.offline()) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if(this.protocolVersion() === "001") {
|
||||
if(this.keys().ak) {
|
||||
// upgrade to 002
|
||||
var authParams = this.getAuthParams();
|
||||
authParams.version = "002";
|
||||
this.updateAuthParams(authParams, function(response){
|
||||
if(!response.error) {
|
||||
// let rest of UI load first
|
||||
$timeout(function(){
|
||||
alert("Your encryption version has been updated. To take full advantage of this update, please resync all your items by clicking Account -> Advanced -> Re-encrypt All Items.")
|
||||
}, 750);
|
||||
}
|
||||
});
|
||||
}
|
||||
let latest = SFJS.version();
|
||||
let updateAvailable = this.protocolVersion() !== latest;
|
||||
if(updateAvailable !== this.securityUpdateAvailable) {
|
||||
this.securityUpdateAvailable = updateAvailable;
|
||||
$rootScope.$broadcast("security-update-status-changed");
|
||||
}
|
||||
|
||||
return this.securityUpdateAvailable;
|
||||
}
|
||||
|
||||
this.presentPasswordWizard = function(type) {
|
||||
var scope = $rootScope.$new(true);
|
||||
scope.type = type;
|
||||
var el = $compile( "<password-wizard type='type'></password-wizard>" )(scope);
|
||||
angular.element(document.body).append(el);
|
||||
}
|
||||
|
||||
this.staticifyObject = function(object) {
|
||||
|
||||
@@ -399,7 +399,9 @@ class ComponentManager {
|
||||
for(let handler of this.handlersForArea(component.area)) {
|
||||
if(handler.contextRequestHandler) {
|
||||
var itemInContext = handler.contextRequestHandler(component);
|
||||
this.sendContextItemInReply(component, itemInContext, message);
|
||||
if(itemInContext) {
|
||||
this.sendContextItemInReply(component, itemInContext, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
@@ -717,7 +719,7 @@ class ComponentManager {
|
||||
console.log("Web|componentManager|registerComponentWindow", component);
|
||||
}
|
||||
component.window = componentWindow;
|
||||
component.sessionKey = SFJS.crypto.generateUUID();
|
||||
component.sessionKey = SFJS.crypto.generateUUIDSync();
|
||||
this.sendMessageToComponent(component, {
|
||||
action: "component-registered",
|
||||
sessionKey: component.sessionKey,
|
||||
|
||||
@@ -32,8 +32,11 @@ class DesktopManager {
|
||||
return this.applicationDataPath;
|
||||
}
|
||||
|
||||
/* Sending a component in its raw state is really slow for the desktop app */
|
||||
convertComponentForTransmission(component) {
|
||||
/*
|
||||
Sending a component in its raw state is really slow for the desktop app
|
||||
Keys are not passed into ItemParams, so the result is not encrypted
|
||||
*/
|
||||
async convertComponentForTransmission(component) {
|
||||
return new ItemParams(component).paramsForExportFile(true);
|
||||
}
|
||||
|
||||
@@ -41,14 +44,15 @@ class DesktopManager {
|
||||
syncComponentsInstallation(components) {
|
||||
if(!this.isDesktop) return;
|
||||
|
||||
var data = components.map((component) => {
|
||||
Promise.all(components.map((component) => {
|
||||
return this.convertComponentForTransmission(component);
|
||||
})).then((data) => {
|
||||
this.installationSyncHandler(data);
|
||||
})
|
||||
this.installationSyncHandler(data);
|
||||
}
|
||||
|
||||
installComponent(component) {
|
||||
this.installComponentHandler(this.convertComponentForTransmission(component));
|
||||
async installComponent(component) {
|
||||
this.installComponentHandler(await this.convertComponentForTransmission(component));
|
||||
}
|
||||
|
||||
registerUpdateObserver(callback) {
|
||||
@@ -125,7 +129,7 @@ class DesktopManager {
|
||||
}
|
||||
}
|
||||
|
||||
desktop_requestBackupFile() {
|
||||
desktop_requestBackupFile(callback) {
|
||||
var keys, authParams, protocolVersion;
|
||||
if(this.authManager.offline() && this.passcodeManager.hasPasscode()) {
|
||||
keys = this.passcodeManager.keys();
|
||||
@@ -137,13 +141,14 @@ class DesktopManager {
|
||||
protocolVersion = this.authManager.protocolVersion();
|
||||
}
|
||||
|
||||
let data = this.modelManager.getAllItemsJSONData(
|
||||
this.modelManager.getAllItemsJSONData(
|
||||
keys,
|
||||
authParams,
|
||||
protocolVersion,
|
||||
true /* return null on empty */
|
||||
);
|
||||
return data;
|
||||
).then((data) => {
|
||||
callback(data);
|
||||
})
|
||||
}
|
||||
|
||||
desktop_setMajorDataChangeHandler(handler) {
|
||||
|
||||
@@ -59,7 +59,7 @@ class ModelManager {
|
||||
|
||||
var newItem = this.createItem(item);
|
||||
|
||||
newItem.uuid = SFJS.crypto.generateUUID();
|
||||
newItem.uuid = SFJS.crypto.generateUUIDSync();
|
||||
|
||||
// Update uuids of relationships
|
||||
newItem.informReferencesOfUUIDChange(item.uuid, newItem.uuid);
|
||||
@@ -167,7 +167,7 @@ class ModelManager {
|
||||
if(json_obj.deleted == true || unknownContentType) {
|
||||
if(json_obj.deleted && json_obj.dirty) {
|
||||
// Item was marked as deleted but not yet synced
|
||||
// We need to create this item as usual, but just not add it to indivudal arrays
|
||||
// We need to create this item as usual, but just not add it to individual arrays
|
||||
// i.e add to this.items but not this.notes (so that it can be retrieved with getDirtyItems)
|
||||
isDirtyItemPendingDelete = true;
|
||||
} else {
|
||||
@@ -380,8 +380,11 @@ class ModelManager {
|
||||
}
|
||||
|
||||
getDirtyItems() {
|
||||
// Items that have errorDecrypting should never be synced back up to the server
|
||||
return this.items.filter(function(item){return item.dirty == true && !item.dummy && !item.errorDecrypting})
|
||||
return this.items.filter((item) => {
|
||||
// An item that has an error decrypting can be synced only if it is being deleted.
|
||||
// Otherwise, we don't want to send corrupt content up to the server.
|
||||
return item.dirty == true && !item.dummy && (!item.errorDecrypting || item.deleted);
|
||||
})
|
||||
}
|
||||
|
||||
clearDirtyItems(items) {
|
||||
@@ -414,13 +417,13 @@ class ModelManager {
|
||||
}
|
||||
|
||||
/* Used when changing encryption key */
|
||||
setAllItemsDirty() {
|
||||
setAllItemsDirty(dontUpdateClientDates = true) {
|
||||
var relevantItems = this.allItems.filter(function(item){
|
||||
return _.includes(this.acceptableContentTypes, item.content_type);
|
||||
}.bind(this));
|
||||
|
||||
for(var item of relevantItems) {
|
||||
item.setDirty(true);
|
||||
item.setDirty(true, dontUpdateClientDates);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,24 +462,25 @@ class ModelManager {
|
||||
Archives
|
||||
*/
|
||||
|
||||
getAllItemsJSONData(keys, authParams, protocolVersion, returnNullIfEmpty) {
|
||||
var items = _.map(this.allItems, (item) => {
|
||||
async getAllItemsJSONData(keys, authParams, protocolVersion, returnNullIfEmpty) {
|
||||
return Promise.all(this.allItems.map((item) => {
|
||||
var itemParams = new ItemParams(item, keys, protocolVersion);
|
||||
return itemParams.paramsForExportFile();
|
||||
});
|
||||
})).then((items) => {
|
||||
if(returnNullIfEmpty && items.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(returnNullIfEmpty && items.length == 0) {
|
||||
return null;
|
||||
}
|
||||
var data = {items: items}
|
||||
|
||||
var data = {items: items}
|
||||
if(keys) {
|
||||
// auth params are only needed when encrypted with a standard file key
|
||||
data["auth_params"] = authParams;
|
||||
}
|
||||
|
||||
if(keys) {
|
||||
// auth params are only needed when encrypted with a standard file key
|
||||
data["auth_params"] = authParams;
|
||||
}
|
||||
return JSON.stringify(data, null, 2 /* pretty print */);
|
||||
})
|
||||
|
||||
return JSON.stringify(data, null, 2 /* pretty print */);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -26,38 +26,47 @@ angular.module('app')
|
||||
return JSON.parse(storageManager.getItem("offlineParams", StorageManager.Fixed));
|
||||
}
|
||||
|
||||
this.protocolVersion = function() {
|
||||
return this._authParams && this._authParams.version;
|
||||
}
|
||||
|
||||
this.unlock = function(passcode, callback) {
|
||||
var params = this.passcodeAuthParams();
|
||||
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, params), function(keys){
|
||||
SFJS.crypto.computeEncryptionKeysForUser(passcode, params).then((keys) => {
|
||||
if(keys.pw !== params.hash) {
|
||||
callback(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._keys = keys;
|
||||
this.decryptLocalStorage(keys);
|
||||
this._locked = false;
|
||||
callback(true);
|
||||
}.bind(this));
|
||||
this._authParams = params;
|
||||
this.decryptLocalStorage(keys, params).then(() => {
|
||||
this._locked = false;
|
||||
callback(true);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
this.setPasscode = (passcode, callback) => {
|
||||
var cost = SFJS.crypto.defaultPasswordGenerationCost();
|
||||
var salt = SFJS.crypto.generateRandomKey(512);
|
||||
var defaultParams = {pw_cost: cost, pw_salt: salt, version: "002"};
|
||||
var uuid = SFJS.crypto.generateUUIDSync();
|
||||
|
||||
SFJS.crypto.computeEncryptionKeysForUser(_.merge({password: passcode}, defaultParams), function(keys) {
|
||||
defaultParams.hash = keys.pw;
|
||||
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);
|
||||
this.encryptLocalStorage(keys, authParams);
|
||||
|
||||
|
||||
// After it's cleared, it's safe to write to it
|
||||
storageManager.setItem("offlineParams", JSON.stringify(defaultParams), StorageManager.Fixed);
|
||||
storageManager.setItem("offlineParams", JSON.stringify(authParams), StorageManager.Fixed);
|
||||
callback(true);
|
||||
}.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
this.changePasscode = (newPasscode, callback) => {
|
||||
@@ -71,16 +80,16 @@ angular.module('app')
|
||||
this._hasPasscode = false;
|
||||
}
|
||||
|
||||
this.encryptLocalStorage = function(keys) {
|
||||
storageManager.setKeys(keys);
|
||||
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 = function(keys) {
|
||||
storageManager.setKeys(keys);
|
||||
storageManager.decryptStorage();
|
||||
this.decryptLocalStorage = async function(keys, authParams) {
|
||||
storageManager.setKeys(keys, authParams);
|
||||
return storageManager.decryptStorage();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -142,8 +142,9 @@ class StorageManager {
|
||||
return hash;
|
||||
}
|
||||
|
||||
setKeys(keys) {
|
||||
setKeys(keys, authParams) {
|
||||
this.encryptedStorageKeys = keys;
|
||||
this.encryptedStorageAuthParams = authParams;
|
||||
}
|
||||
|
||||
writeEncryptedStorageToDisk() {
|
||||
@@ -152,13 +153,15 @@ class StorageManager {
|
||||
encryptedStorage.storage = this.storageAsHash();
|
||||
|
||||
// Save new encrypted storage in Fixed storage
|
||||
var params = new ItemParams(encryptedStorage, this.encryptedStorageKeys);
|
||||
this.setItem("encryptedStorage", JSON.stringify(params.paramsForSync()), StorageManager.Fixed);
|
||||
var params = new ItemParams(encryptedStorage, this.encryptedStorageKeys, this.encryptedStorageAuthParams.version);
|
||||
params.paramsForSync().then((syncParams) => {
|
||||
this.setItem("encryptedStorage", JSON.stringify(syncParams), StorageManager.Fixed);
|
||||
})
|
||||
}
|
||||
|
||||
decryptStorage() {
|
||||
async decryptStorage() {
|
||||
var stored = JSON.parse(this.getItem("encryptedStorage", StorageManager.Fixed));
|
||||
SFItemTransformer.decryptItem(stored, this.encryptedStorageKeys);
|
||||
await SFJS.itemTransformer.decryptItem(stored, this.encryptedStorageKeys);
|
||||
var encryptedStorage = new EncryptedStorage(stored);
|
||||
|
||||
for(var key of Object.keys(encryptedStorage.storage)) {
|
||||
|
||||
@@ -26,27 +26,51 @@ class SyncManager {
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
// Use null to use the latest protocol version if offline
|
||||
var version = this.authManager.offline() ? null : this.authManager.protocolVersion();
|
||||
|
||||
var version = this.authManager.offline() ? this.passcodeManager.protocolVersion() : this.authManager.protocolVersion();
|
||||
var keys = this.authManager.offline() ? this.passcodeManager.keys() : this.authManager.keys();
|
||||
var params = items.map(function(item) {
|
||||
|
||||
Promise.all(items.map(async (item) => {
|
||||
var itemParams = new ItemParams(item, keys, version);
|
||||
itemParams = itemParams.paramsForLocalStorage();
|
||||
itemParams = await itemParams.paramsForLocalStorage();
|
||||
if(offlineOnly) {
|
||||
delete itemParams.dirty;
|
||||
}
|
||||
return itemParams;
|
||||
}.bind(this));
|
||||
|
||||
this.storageManager.saveModels(params, callback);
|
||||
})).then((params) => {
|
||||
this.storageManager.saveModels(params, callback);
|
||||
})
|
||||
}
|
||||
|
||||
loadLocalItems(callback) {
|
||||
var params = this.storageManager.getAllModels(function(items){
|
||||
var items = this.handleItemsResponse(items, null, ModelManager.MappingSourceLocalRetrieved);
|
||||
Item.sortItemsByDate(items);
|
||||
callback(items);
|
||||
}.bind(this))
|
||||
async loadLocalItems(callback) {
|
||||
this.storageManager.getAllModels((items) => {
|
||||
// break it up into chunks to make interface more responsive for large item counts
|
||||
let total = items.length;
|
||||
let iteration = 50;
|
||||
var current = 0;
|
||||
var processed = [];
|
||||
|
||||
var completion = () => {
|
||||
Item.sortItemsByDate(processed);
|
||||
callback(processed);
|
||||
}
|
||||
|
||||
var decryptNext = async () => {
|
||||
var subitems = items.slice(current, current + iteration);
|
||||
var processedSubitems = await this.handleItemsResponse(subitems, null, ModelManager.MappingSourceLocalRetrieved);
|
||||
processed.push(processedSubitems);
|
||||
|
||||
current += subitems.length;
|
||||
|
||||
if(current < total) {
|
||||
this.$timeout(() => { decryptNext(); });
|
||||
} else {
|
||||
completion();
|
||||
}
|
||||
}
|
||||
|
||||
decryptNext();
|
||||
})
|
||||
}
|
||||
|
||||
syncOffline(items, callback) {
|
||||
@@ -54,7 +78,7 @@ class SyncManager {
|
||||
for(var item of items) {
|
||||
item.updated_at = new Date();
|
||||
}
|
||||
this.writeItemsToLocalStorage(items, true, function(responseItems){
|
||||
this.writeItemsToLocalStorage(items, true, (responseItems) => {
|
||||
// delete anything needing to be deleted
|
||||
for(var item of items) {
|
||||
if(item.deleted) {
|
||||
@@ -70,7 +94,7 @@ class SyncManager {
|
||||
if(callback) {
|
||||
callback({success: true});
|
||||
}
|
||||
}.bind(this))
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
@@ -201,7 +225,7 @@ class SyncManager {
|
||||
this.syncLocked = false;
|
||||
}
|
||||
|
||||
sync(callback, options = {}, source) {
|
||||
async sync(callback, options = {}, source) {
|
||||
|
||||
if(this.syncLocked) {
|
||||
console.log("Sync Locked, Returning;");
|
||||
@@ -264,6 +288,12 @@ class SyncManager {
|
||||
this.syncStatus.current = 0;
|
||||
}
|
||||
|
||||
// If items are marked as dirty during a long running sync request, total isn't updated
|
||||
// This happens mostly in the case of large imports and sync conflicts where duplicated items are created
|
||||
if(this.syncStatus.current > this.syncStatus.total) {
|
||||
this.syncStatus.total = this.syncStatus.current;
|
||||
}
|
||||
|
||||
// when doing a sync request that returns items greater than the limit, and thus subsequent syncs are required,
|
||||
// we want to keep track of all retreived items, then save to local storage only once all items have been retrieved,
|
||||
// so that relationships remain intact
|
||||
@@ -281,11 +311,14 @@ class SyncManager {
|
||||
|
||||
var params = {};
|
||||
params.limit = 150;
|
||||
params.items = _.map(subItems, function(item){
|
||||
|
||||
await Promise.all(subItems.map((item) => {
|
||||
var itemParams = new ItemParams(item, keys, version);
|
||||
itemParams.additionalFields = options.additionalFields;
|
||||
return itemParams.paramsForSync();
|
||||
}.bind(this));
|
||||
})).then((itemsParams) => {
|
||||
params.items = itemsParams;
|
||||
})
|
||||
|
||||
for(var item of subItems) {
|
||||
// Reset dirty counter to 0, since we're about to sync it.
|
||||
@@ -300,7 +333,7 @@ class SyncManager {
|
||||
this.stopCheckingIfSyncIsTakingTooLong();
|
||||
}.bind(this);
|
||||
|
||||
var onSyncSuccess = function(response) {
|
||||
var onSyncSuccess = async function(response) {
|
||||
// Check to make sure any subItem hasn't been marked as dirty again while a sync was ongoing
|
||||
var itemsToClearAsDirty = [];
|
||||
for(var item of subItems) {
|
||||
@@ -325,8 +358,7 @@ class SyncManager {
|
||||
|
||||
// Map retrieved items to local data
|
||||
// Note that deleted items will not be returned
|
||||
var retrieved
|
||||
= this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved);
|
||||
var retrieved = await this.handleItemsResponse(response.retrieved_items, null, ModelManager.MappingSourceRemoteRetrieved);
|
||||
|
||||
// Append items to master list of retrieved items for this ongoing sync operation
|
||||
this.allRetreivedItems = this.allRetreivedItems.concat(retrieved);
|
||||
@@ -337,8 +369,7 @@ class SyncManager {
|
||||
var omitFields = ["content", "auth_hash"];
|
||||
|
||||
// Map saved items to local data
|
||||
var saved =
|
||||
this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved);
|
||||
var saved = await this.handleItemsResponse(response.saved_items, omitFields, ModelManager.MappingSourceRemoteSaved);
|
||||
|
||||
// Append items to master list of saved items for this ongoing sync operation
|
||||
this.allSavedItems = this.allSavedItems.concat(saved);
|
||||
@@ -380,7 +411,7 @@ class SyncManager {
|
||||
) {
|
||||
this.$rootScope.$broadcast("major-data-change");
|
||||
}
|
||||
|
||||
|
||||
this.callQueuedCallbacksAndCurrent(callback, response);
|
||||
this.$rootScope.$broadcast("sync:completed", {retrievedItems: this.allRetreivedItems, savedItems: this.allSavedItems});
|
||||
|
||||
@@ -418,9 +449,9 @@ class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
handleItemsResponse(responseItems, omitFields, source) {
|
||||
async handleItemsResponse(responseItems, omitFields, source) {
|
||||
var keys = this.authManager.keys() || this.passcodeManager.keys();
|
||||
SFItemTransformer.decryptMultipleItems(responseItems, keys);
|
||||
await SFJS.itemTransformer.decryptMultipleItems(responseItems, keys);
|
||||
var items = this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields, source);
|
||||
|
||||
// During the decryption process, items may be marked as "errorDecrypting". If so, we want to be sure
|
||||
@@ -446,7 +477,7 @@ class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
handleUnsavedItemsResponse(unsaved) {
|
||||
async handleUnsavedItemsResponse(unsaved) {
|
||||
if(unsaved.length == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -454,7 +485,7 @@ class SyncManager {
|
||||
console.log("Handle unsaved", unsaved);
|
||||
|
||||
var i = 0;
|
||||
var handleNext = () => {
|
||||
var handleNext = async () => {
|
||||
if(i >= unsaved.length) {
|
||||
// Handled all items
|
||||
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||
@@ -463,7 +494,7 @@ class SyncManager {
|
||||
|
||||
var mapping = unsaved[i];
|
||||
var itemResponse = mapping.item;
|
||||
SFItemTransformer.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||
await SFJS.itemTransformer.decryptMultipleItems([itemResponse], this.authManager.keys());
|
||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||
|
||||
if(!item) {
|
||||
|
||||
@@ -61,6 +61,10 @@ $heading-height: 75px;
|
||||
margin-top: 4px;
|
||||
text-align: right;
|
||||
color: rgba(black, 0.23);
|
||||
|
||||
.error {
|
||||
color: #f6a200;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-tags {
|
||||
|
||||
@@ -87,12 +87,15 @@ $footer-height: 32px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
width: 8px;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
background-color: rgb(224, 224, 224);
|
||||
background-color: rgba(black, 0.05);
|
||||
opacity: 0;
|
||||
border: 1px dashed rgba($blue-color, 0.15);
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
|
||||
&.left {
|
||||
left: 0;
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
#password-wizard {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: white;
|
||||
}
|
||||
@@ -44,6 +48,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.auto-height {
|
||||
> .content {
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
> .content {
|
||||
width: 700px;
|
||||
@@ -51,6 +61,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.small {
|
||||
> .content {
|
||||
width: 700px;
|
||||
height: 344px;
|
||||
}
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
@@ -64,6 +81,10 @@
|
||||
padding: 0;
|
||||
padding-bottom: 0;
|
||||
min-width: 300px;
|
||||
|
||||
-webkit-box-shadow: 0px 2px 35px 0px rgba(0,0,0,0.19);
|
||||
-moz-box-shadow: 0px 2px 35px 0px rgba(0,0,0,0.19);
|
||||
box-shadow: 0px 2px 35px 0px rgba(0,0,0,0.19);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
.sn-component {
|
||||
|
||||
.notification {
|
||||
&.unpadded {
|
||||
padding: 0;
|
||||
padding-bottom: 0 !important;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.padded-row {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.bordered-row {
|
||||
border-bottom: 1px solid rgba(black, 0.1);
|
||||
border-top: 1px solid rgba(black, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
.panel {
|
||||
|
||||
@@ -70,6 +70,14 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
.mr-5 {
|
||||
margin-right: 5px !important;
|
||||
}
|
||||
|
||||
.mr-8 {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
|
||||
.faded {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -82,6 +90,10 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
@@ -103,10 +115,10 @@ $screen-md-max: ($screen-lg-min - 1) !default;
|
||||
font-weight: normal !important;
|
||||
}
|
||||
|
||||
.small {
|
||||
.small-text {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.medium {
|
||||
.medium-text {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
@@ -31,10 +31,18 @@
|
||||
.notification.info{"ng-if" => "formData.showRegister"}
|
||||
%h2.title No Password Reset.
|
||||
.text Because your notes are encrypted using your password, Standard Notes does not have a password reset option. You cannot forget your password.
|
||||
.advanced-options.panel-row{"ng-if" => "formData.showAdvanced"}
|
||||
|
||||
.notification.unpadded.default.advanced-options.panel-row{"ng-if" => "formData.showAdvanced"}
|
||||
.panel-column.stretch
|
||||
%label Sync Server Domain
|
||||
%input.form-control.mt-5{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
|
||||
%h4.title.panel-row.padded-row Advanced Options
|
||||
%div.bordered-row.padded-row
|
||||
%label Sync Server Domain
|
||||
%input.form-control.mt-5{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
|
||||
%label.padded-row{"ng-if" => "formData.showLogin"}
|
||||
%input{"type" => "checkbox", "ng-model" => "formData.strictSignin"}
|
||||
Use strict sign in
|
||||
%span
|
||||
%a{"href" => "https://standardnotes.org/help/security", "target" => "_blank"} (Learn more)
|
||||
|
||||
.button-group.stretch.panel-row.form-submit
|
||||
%button.button.info.featured{"type" => "submit"}
|
||||
@@ -60,6 +68,11 @@
|
||||
|
||||
%div{"ng-if" => "!formData.showLogin && !formData.showRegister && !formData.mfa"}
|
||||
.panel-section{"ng-if" => "user"}
|
||||
.notification.danger{"ng-if" => "syncStatus.error"}
|
||||
%h2.title Sync Unreachable
|
||||
.text Hmm...we can't seem to sync your account. The reason: {{syncStatus.error.message}}
|
||||
%p
|
||||
%a{"href" => "https://standardnotes.org/help", "target" => "_blank"} Need help?
|
||||
.panel-row
|
||||
%h2.title.wrap {{user.email}}
|
||||
.horizontal-group{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
|
||||
@@ -68,55 +81,14 @@
|
||||
{{"Syncing" + (syncStatus.total > 0 ? ":" : "")}}
|
||||
%span{"ng-if" => "syncStatus.total > 0"} {{syncStatus.current}}/{{syncStatus.total}}
|
||||
|
||||
.subtitle.danger.panel-row{"ng-if" => "syncStatus.error"} Error syncing: {{syncStatus.error.message}}
|
||||
|
||||
.subtitle.subtle.normal {{server}}
|
||||
|
||||
.panel-row
|
||||
|
||||
%a.panel-row.condensed{"ng-click" => "newPasswordData.changePassword = !newPasswordData.changePassword"} Change Password
|
||||
.notification.warning{"ng-if" => "newPasswordData.changePassword"}
|
||||
%h1.title Change Password
|
||||
.text
|
||||
%p Since your encryption key is based on your password, changing your password requires all your notes and tags to be re-encrypted using your new key.
|
||||
%p If you have thousands of items, this can take several minutes — you must keep the application window open during this process.
|
||||
%p After changing your password, you must log out of all other applications currently signed in to your account.
|
||||
%p.bold It is highly recommended you download a backup of your data before proceeding.
|
||||
.panel-row{"ng-if" => "!newPasswordData.status"}
|
||||
.horizontal-group{"ng-if" => "!newPasswordData.showForm"}
|
||||
%a.red{"ng-click" => "showPasswordChangeForm()"} Continue
|
||||
%a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
|
||||
.panel-row{"ng-if" => "newPasswordData.showForm"}
|
||||
%form.panel-form.stretch
|
||||
%input{:type => 'password', "ng-model" => "newPasswordData.newPassword", "placeholder" => "Enter new password"}
|
||||
%input{:type => 'password', "ng-model" => "newPasswordData.newPasswordConfirmation", "placeholder" => "Confirm new password"}
|
||||
.button-group.stretch.panel-row.form-submit
|
||||
.button.info{"type" => "submit", "ng-click" => "submitPasswordChange()"}
|
||||
.label Submit
|
||||
%a{"ng-click" => "newPasswordData.changePassword = false; newPasswordData.showForm = false"} Cancel
|
||||
|
||||
%p.italic.mt-10{"ng-if" => "newPasswordData.status"} {{newPasswordData.status}}
|
||||
|
||||
|
||||
|
||||
%a.panel-row.condensed{"ng-click" => "showAdvanced = !showAdvanced"} Advanced
|
||||
%div{"ng-if" => "showAdvanced"}
|
||||
%a.panel-row{"ng-click" => "reencryptPressed()"} Resync All Items
|
||||
|
||||
|
||||
%a.panel-row.condensed{"ng-if" => "securityUpdateAvailable()", "ng-click" => "clickedSecurityUpdate()"} Security Update Available
|
||||
.notification.default{"ng-if" => "securityUpdateData.showForm"}
|
||||
%p
|
||||
%a{"href" => "https://standardnotes.org/help/security-update", "target" => "_blank"} Learn more.
|
||||
%form.panel-form.stretch{"ng-if" => "!securityUpdateData.processing", "ng-submit" => "submitSecurityUpdateForm()"}
|
||||
%p Enter your password to update:
|
||||
%input.panel-row{:type => 'password', "ng-model" => "securityUpdateData.password", "placeholder" => "Enter password"}
|
||||
.button-group.stretch.panel-row.form-submit
|
||||
%button.button.info{"ng-type" => "submit"}
|
||||
.label Update
|
||||
.panel-row{"ng-if" => "securityUpdateData.processing"}
|
||||
%p.info Processing...
|
||||
|
||||
%a.panel-row.condensed{"ng-click" => "openPasswordWizard('change-pw')"} Change Password
|
||||
%a.panel-row.justify-left.condensed.success{"ng-if" => "securityUpdateAvailable", "ng-click" => "openPasswordWizard('upgrade-security')"}
|
||||
.inline.circle.small.success.mr-8
|
||||
.inline Security Update Available
|
||||
|
||||
.panel-section
|
||||
%h3.title.panel-row Encryption
|
||||
@@ -186,6 +158,9 @@
|
||||
.button-group.stretch.panel-row.form-submit
|
||||
%button.button.info{"type" => "submit"}
|
||||
.label Decrypt & Import
|
||||
%p
|
||||
Importing from backup will overwrite existing notes with matching note from backup. Existing notes not found in the backup will remain as-is and won't be overwritten.
|
||||
%p If you'd like to import only a selection of notes instead of the whole file, please use the Batch Manager extension instead.
|
||||
.panel-row
|
||||
.spinner.small.info{"ng-if" => "importData.loading"}
|
||||
.footer
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.menu-panel.dropdown-menu
|
||||
|
||||
%a.no-decoration{"ng-if" => "extensions.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank"}
|
||||
%menu-row{"title" => "'Download Actions'"}
|
||||
%menu-row{"label" => "'Download Actions'"}
|
||||
|
||||
%div{"ng-repeat" => "extension in extensions"}
|
||||
.header{"ng-click" => "extension.hide = !extension.hide; $event.stopPropagation();"}
|
||||
@@ -12,7 +12,7 @@
|
||||
%div{"ng-if" => "extension.hide"} …
|
||||
|
||||
%menu-row{"ng-if" => "!extension.hide", "ng-repeat" => "action in extension.actionsWithContextForItem(item)",
|
||||
"ng-click" => "executeAction(action, extension); $event.stopPropagation();", "title" => "action.label", "subtitle" => "action.desc",
|
||||
"ng-click" => "executeAction(action, extension); $event.stopPropagation();", "label" => "action.label", "subtitle" => "action.desc",
|
||||
"spinner-class" => "action.running ? 'info' : null", "sub-rows" => "action.subrows"}
|
||||
.sublabel{"ng-if" => "action.access_type"}
|
||||
Uses
|
||||
@@ -20,7 +20,7 @@
|
||||
access to this note.
|
||||
|
||||
|
||||
.modal.medium{"ng-if" => "renderData.showRenderModal", "ng-click" => "$event.stopPropagation();"}
|
||||
.modal.medium-text.medium{"ng-if" => "renderData.showRenderModal", "ng-click" => "$event.stopPropagation();"}
|
||||
.content
|
||||
.sn-component
|
||||
.panel
|
||||
@@ -29,4 +29,4 @@
|
||||
%a.close-button.info{"ng-click" => "renderData.showRenderModal = false; $event.stopPropagation();"} Close
|
||||
.content.selectable
|
||||
%h2 {{renderData.title}}
|
||||
%p.normal{"style" => "white-space: pre-wrap; font-family: monospace; font-size: 16px;"} {{renderData.text}}
|
||||
%p.normal{"style" => "white-space: pre-wrap; font-size: 16px;"} {{renderData.text}}
|
||||
|
||||
@@ -3,24 +3,24 @@
|
||||
.section
|
||||
.header
|
||||
%h4.title Note Editor
|
||||
%menu-row{"title" => "'Plain Editor'", "circle" => "selectedEditor == null && 'success'", "ng-click" => "selectComponent($event, null)"}
|
||||
%menu-row{"label" => "'Plain Editor'", "circle" => "selectedEditor == null && 'success'", "ng-click" => "selectComponent($event, null)"}
|
||||
|
||||
%menu-row{"ng-repeat" => "editor in editors", "ng-click" => "selectComponent($event, editor)", "title" => "editor.name",
|
||||
%menu-row{"ng-repeat" => "editor in editors", "ng-click" => "selectComponent($event, editor)", "label" => "editor.name",
|
||||
"circle" => "selectedEditor === editor && 'success'",
|
||||
"has-button" => "selectedEditor == editor || defaultEditor == editor", "button-text" => "defaultEditor == editor ? 'Undefault' : 'Set Default'",
|
||||
"button-action" => "toggleDefaultForEditor(editor)", "button-class" => "defaultEditor == editor ? 'warning' : 'info'"}
|
||||
.column{"ng-if" => "component.conflict_of || shouldDisplayRunningLocallyLabel(editor)"}
|
||||
%strong.red.medium{"ng-if" => "editor.conflict_of"} Conflicted copy
|
||||
%strong.red.medium-text{"ng-if" => "editor.conflict_of"} Conflicted copy
|
||||
.sublabel{"ng-if" => "shouldDisplayRunningLocallyLabel(editor)"} Running Locally
|
||||
|
||||
%a.no-decoration{"ng-if" => "editors.length == 0", "href" => "https://standardnotes.org/extensions", "target" => "blank"}
|
||||
%menu-row{"title" => "'Download More Editors'"}
|
||||
%menu-row{"label" => "'Download More Editors'"}
|
||||
|
||||
.section{"ng-if" => "stack.length > 0"}
|
||||
.header
|
||||
%h4.title Editor Stack
|
||||
%menu-row{"ng-repeat" => "component in stack", "ng-click" => "selectComponent($event, component)", "title" => "component.name",
|
||||
%menu-row{"ng-repeat" => "component in stack", "ng-click" => "selectComponent($event, component)", "label" => "component.name",
|
||||
"circle" => "stackComponentEnabled(component) ? 'success' : 'danger'"}
|
||||
.column{"ng-if" => "component.conflict_of || shouldDisplayRunningLocallyLabel(component)"}
|
||||
%strong.red.medium{"ng-if" => "component.conflict_of"} Conflicted copy
|
||||
%strong.red.medium-text{"ng-if" => "component.conflict_of"} Conflicted copy
|
||||
.sublabel{"ng-if" => "shouldDisplayRunningLocallyLabel(component)"} Running Locally
|
||||
|
||||
18
app/assets/templates/directives/input-modal.html.haml
Normal file
18
app/assets/templates/directives/input-modal.html.haml
Normal file
@@ -0,0 +1,18 @@
|
||||
.modal.small.auto-height
|
||||
.content
|
||||
.sn-component
|
||||
.panel
|
||||
.header
|
||||
%h1.title {{title}}
|
||||
%a.close-button{"ng-click" => "dismiss()"} Close
|
||||
.content
|
||||
.panel-section
|
||||
%p.panel-row {{message}}
|
||||
.panel-row
|
||||
.panel-column.stretch
|
||||
%form{"ng-submit" => "submit()"}
|
||||
%input.form-control{:type => '{{type}}', "ng-model" => "formData.input", "placeholder" => "{{placeholder}}", "sn-autofocus" => "true", "should-focus" => "true"}
|
||||
|
||||
.footer
|
||||
%a.right{"ng-click" => "submit()"}
|
||||
Submit
|
||||
@@ -1,17 +1,17 @@
|
||||
.row
|
||||
.row{"ng-attr-title" => "{{desc}}"}
|
||||
.column
|
||||
.left
|
||||
.column{"ng-if" => "circle"}
|
||||
.circle.small{"ng-class" => "circle"}
|
||||
.column{"ng-class" => "{'faded' : faded}"}
|
||||
.label
|
||||
{{title}}
|
||||
{{label}}
|
||||
.sublabel{"ng-if" => "subtitle"}
|
||||
{{subtitle}}
|
||||
%ng-transclude
|
||||
.subrows{"ng-if" => "subRows && subRows.length > 0"}
|
||||
%menu-row{"ng-repeat" => "row in subRows", "ng-click" => "row.onClick($event); $event.stopPropagation();",
|
||||
"title" => "row.title", "subtitle" => "row.subtitle", "spinner-class" => "row.spinnerClass"}
|
||||
"label" => "row.label", "subtitle" => "row.subtitle", "spinner-class" => "row.spinnerClass"}
|
||||
|
||||
.column{"ng-if" => "hasButton"}
|
||||
.button.info{"ng-click" => "clickButton($event)", "ng-class" => "buttonClass"}
|
||||
|
||||
109
app/assets/templates/directives/password-wizard.html.haml
Normal file
109
app/assets/templates/directives/password-wizard.html.haml
Normal file
@@ -0,0 +1,109 @@
|
||||
#password-wizard.modal.small.auto-height
|
||||
.content
|
||||
.sn-component
|
||||
.panel
|
||||
.header
|
||||
%h1.title {{title}}
|
||||
%a.close-button{"ng-click" => "dismiss()"} Close
|
||||
.content
|
||||
|
||||
%div{"ng-if" => "step == 0"}
|
||||
%div{"ng-if" => "changePassword"}
|
||||
%h2.title.panel-row Change your password
|
||||
%p
|
||||
Changing your password involves changing your encryption key, which requires your data to be re-encrypted and synced.
|
||||
If you have many items, syncing your data can take several minutes.
|
||||
%p.panel-row
|
||||
%strong You must keep the application window open during this process.
|
||||
%div{"ng-if" => "securityUpdate"}
|
||||
%h2.title.panel-row Perform security update
|
||||
%p
|
||||
A new update is available for your account. Updates address improvements and enhancements to our security specification.
|
||||
This process will guide you through the update, and perform the steps necessary with your supervision.
|
||||
%p
|
||||
For more information about security updates, please visit
|
||||
%a{"href" => "https://standardnotes.org/help/security", "target" => "_blank"} standardnotes.org/help/security.
|
||||
|
||||
%p.panel-row
|
||||
.info Press Continue to proceed.
|
||||
|
||||
.panel-row
|
||||
.panel-row
|
||||
|
||||
.panel-section{"ng-if" => "step > 0"}
|
||||
|
||||
%h3.title.panel-row Step {{step}} — {{titleForStep(step)}}
|
||||
|
||||
%div{"ng-if" => "step == 1"}
|
||||
%p.panel-row
|
||||
As a result of this process, the entirety of your data will be re-encrypted and synced to your account. This is a generally safe process,
|
||||
but unforeseen factors like poor network connectivity or a sudden shutdown of your computer may cause this process to fail.
|
||||
It's best to be on the safe side before large operations such as this one.
|
||||
.panel-row
|
||||
.panel-row
|
||||
.button-group
|
||||
.button.info{"ng-click" => "downloadBackup(true)"}
|
||||
.label Download Encrypted Backup
|
||||
.button.info{"ng-click" => "downloadBackup(false)"}
|
||||
.label Download Decrypted Backup
|
||||
|
||||
%div{"ng-if" => "step == 2"}
|
||||
%p.panel-row
|
||||
As a result of this process, your encryption keys will change.
|
||||
Any device on which you use Standard Notes will need to end its session. After this process completes, you will be asked to sign back in.
|
||||
|
||||
%p.bold.panel-row.info-i Please sign out of all applications (excluding this one), including:
|
||||
%ul
|
||||
%li Desktop
|
||||
%li Web (Chrome, Firefox, Safari)
|
||||
%li Mobile (iOS and Android)
|
||||
%p.panel-row
|
||||
If you do not currently have access to a device you're signed in on, you may proceed,
|
||||
but must make signing out and back in the first step upon gaining access to that device.
|
||||
%p.panel-row Press Continue only when you have completed signing out of all your devices.
|
||||
|
||||
|
||||
%div{"ng-if" => "step == 3"}
|
||||
%div{"ng-if" => "changePassword"}
|
||||
%div{"ng-if" => "securityUpdate"}
|
||||
%p.panel-row Enter your current password. We'll run this through our encryption scheme to generate strong new encryption keys.
|
||||
.panel-row
|
||||
.panel-row
|
||||
.panel-column.stretch
|
||||
%form
|
||||
%input.form-control{:type => 'password', "ng-model" => "formData.currentPassword", "placeholder" => "Current Password", "sn-autofocus" => "true", "should-focus" => "true"}
|
||||
|
||||
%input.form-control{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPassword", "placeholder" => "New Password"}
|
||||
%input.form-control{"ng-if" => "changePassword", :type => 'password', "ng-model" => "formData.newPasswordConfirmation", "placeholder" => "Confirm New Password"}
|
||||
|
||||
%div{"ng-if" => "step == 4"}
|
||||
%p.panel-row
|
||||
Your data is being re-encrypted with your new keys and synced to your account.
|
||||
%p.panel-row.danger
|
||||
Do not close this window until this process completes.
|
||||
|
||||
.panel-row
|
||||
.panel-column
|
||||
.spinner.small.inline.info.mr-5{"ng-if" => "formData.processing"}
|
||||
.inline.bold{"ng-class" => "{'info' : !formData.statusError, 'error' : formData.statusError}"}
|
||||
{{formData.status}}
|
||||
.panel-column{"delay-hide" => "true", "show" => "syncStatus.syncOpInProgress || syncStatus.needsMoreSync", "delay" => "1000"}
|
||||
%p.info
|
||||
Syncing {{syncStatus.current}}/{{syncStatus.total}}
|
||||
|
||||
%div{"ng-if" => "step == 5"}
|
||||
%div{"ng-if" => "changePassword"}
|
||||
%p.panel-row Your password has been successfully changed.
|
||||
%div{"ng-if" => "securityUpdate"}
|
||||
%p.panel-row
|
||||
The security update has been successfully applied to your account.
|
||||
%p.panel-row
|
||||
%strong Please ensure you are running the latest version of Standard Notes on all platforms to ensure maximum compatibility.
|
||||
|
||||
%p.panel-row You may now sign back in on all your devices and close this window.
|
||||
|
||||
.footer
|
||||
.empty
|
||||
%a.right{"ng-click" => "continue()", "ng-class" => "{'disabled' : lockContinue}"}
|
||||
.spinner.small.inline.info.mr-5{"ng-if" => "showSpinner"}
|
||||
{{continueTitle}}
|
||||
@@ -1,4 +1,4 @@
|
||||
.section.editor#editor-column
|
||||
.section.editor#editor-column{"aria-label" => "Note"}
|
||||
.sn-component
|
||||
.app-bar.no-edges{"ng-if" => "ctrl.note.locked", "ng-init" => "ctrl.lockText = 'Note Locked'", "ng-mouseover" => "ctrl.lockText = 'Unlock'", "ng-mouseleave" => "ctrl.lockText = 'Note Locked'"}
|
||||
.left
|
||||
@@ -13,7 +13,7 @@
|
||||
"ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()", "ng-blur" => "ctrl.onNameBlur()",
|
||||
"select-on-click" => "true", "ng-disabled" => "ctrl.note.locked"}
|
||||
|
||||
#save-status{"ng-class" => "{'red bold': ctrl.saveError, 'warning bold': ctrl.syncTakingTooLong}", "ng-bind-html" => "ctrl.noteStatus"}
|
||||
#save-status{"ng-class" => "{'error bold': ctrl.syncTakingTooLong}", "ng-bind-html" => "ctrl.noteStatus"}
|
||||
|
||||
.editor-tags
|
||||
#note-tags-component-container{"ng-if" => "ctrl.tagsComponent"}
|
||||
@@ -26,21 +26,21 @@
|
||||
.app-bar.no-edges
|
||||
.left
|
||||
.item{"ng-click" => "ctrl.showMenu = !ctrl.showMenu; ctrl.showExtensions = false; ctrl.showEditorMenu = false;", "ng-class" => "{'selected' : ctrl.showMenu}", "click-outside" => "ctrl.showMenu = false;", "is-open" => "ctrl.showMenu"}
|
||||
.label Menu
|
||||
.label Options
|
||||
.menu-panel.dropdown-menu{"ng-if" => "ctrl.showMenu"}
|
||||
.section
|
||||
.header
|
||||
%h4.title Note Options
|
||||
%menu-row{"title" => "ctrl.note.pinned ? 'Unpin' : 'Pin'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.togglePin()"}
|
||||
%menu-row{"title" => "ctrl.note.archived ? 'Unarchive' : 'Archive'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleArchiveNote()"}
|
||||
%menu-row{"title" => "ctrl.note.locked ? 'Unlock' : 'Lock'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleLockNote()"}
|
||||
%menu-row{"title" => "'Delete'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()"}
|
||||
%menu-row{"label" => "ctrl.note.pinned ? 'Unpin' : 'Pin'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.togglePin()", "desc" => "'Pin or unpin a note from the top of your list'"}
|
||||
%menu-row{"label" => "ctrl.note.archived ? 'Unarchive' : 'Archive'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleArchiveNote()", "desc" => "'Archive or unarchive a note from your Archived system tag'"}
|
||||
%menu-row{"label" => "ctrl.note.locked ? 'Unlock' : 'Lock'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleLockNote()", "desc" => "'Locking notes prevents unintentional editing'"}
|
||||
%menu-row{"label" => "'Delete'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.deleteNote()", "desc" => "'Delete this note permanently from all your devices'"}
|
||||
|
||||
.section{"ng-if" => "!ctrl.selectedEditor"}
|
||||
.header
|
||||
%h4.title Global Display
|
||||
%menu-row{"title" => "'Monospace Font'", "circle" => "ctrl.monospaceFont ? 'success' : 'default'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleKey('monospaceFont')"}
|
||||
%menu-row{"title" => "'Spellcheck'", "circle" => "ctrl.spellcheck ? 'success' : 'default'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleKey('spellcheck')"}
|
||||
%menu-row{"label" => "'Monospace Font'", "circle" => "ctrl.monospaceFont ? 'success' : 'default'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleKey('monospaceFont')", "desc" => "'Toggles the font style for the default editor'"}
|
||||
%menu-row{"label" => "'Spellcheck'", "circle" => "ctrl.spellcheck ? 'success' : 'default'", "ng-click" => "ctrl.selectedMenuItem($event, true); ctrl.toggleKey('spellcheck')", "desc" => "'Toggles spellcheck for the default editor'"}
|
||||
|
||||
.item{"ng-click" => "ctrl.onEditorMenuClick()", "ng-class" => "{'selected' : ctrl.showEditorMenu}", "click-outside" => "ctrl.showEditorMenu = false;", "is-open" => "ctrl.showEditorMenu"}
|
||||
.label Editor
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
%panel-resizer{"panel-id" => "'editor-content'", "on-resize-finish" => "ctrl.onPanelResizeFinish","control" => "ctrl.resizeControl", "min-width" => 300, "hoverable" => "true", "property" => "'right'"}
|
||||
|
||||
%section.section{"ng-if" => "ctrl.note.errorDecrypting"}
|
||||
.section{"ng-if" => "ctrl.note.errorDecrypting"}
|
||||
%p.medium-padding{"style" => "padding-top: 0 !important;"} There was an error decrypting this item. Ensure you are running the latest version of this app, then sign out and sign back in to try again.
|
||||
|
||||
#editor-pane-component-stack
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
|
||||
.right
|
||||
|
||||
.item{"ng-if" => "ctrl.newUpdateAvailable", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}
|
||||
.item{"ng-if" => "ctrl.securityUpdateAvailable", "ng-click" => "ctrl.openSecurityUpdate()"}
|
||||
%span.success.label Security update available.
|
||||
|
||||
.item{"ng-if" => "ctrl.newUpdateAvailable == true", "ng-click" => "ctrl.clickedNewUpdateAnnouncement()"}
|
||||
%span.info.label New update available.
|
||||
|
||||
.item.no-pointer{"ng-if" => "ctrl.lastSyncDate && !ctrl.isRefreshing"}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
.section.notes#notes-column
|
||||
.section.notes#notes-column{"aria-label" => "Notes"}
|
||||
.content
|
||||
.section-title-bar#notes-title-bar
|
||||
.padded
|
||||
.section-title-bar-header
|
||||
.title {{ctrl.panelTitle()}}
|
||||
.add-button#notes-add-button{"ng-click" => "ctrl.createNewNote()"} +
|
||||
.filter-section
|
||||
.add-button#notes-add-button{"ng-click" => "ctrl.createNewNote()", "title" => "Create a new note in the selected tag"} +
|
||||
.filter-section{"role" => "search"}
|
||||
%input.filter-bar#search-bar.mousetrap{"select-on-click" => "true", "ng-model" => "ctrl.noteFilter.text", "placeholder" => "Search",
|
||||
"ng-change" => "ctrl.filterTextChanged()", "lowercase" => "true", "ng-blur" => "ctrl.onFilterEnter()", "ng-keyup" => "$event.keyCode == 13 && ctrl.onFilterEnter();"}
|
||||
"ng-change" => "ctrl.filterTextChanged()", "lowercase" => "true", "ng-blur" => "ctrl.onFilterEnter()", "ng-keyup" => "$event.keyCode == 13 && ctrl.onFilterEnter();",
|
||||
"title" => "Searches notes in the currently selected tag"}
|
||||
#search-clear-button{"ng-if" => "ctrl.noteFilter.text", "ng-click" => "ctrl.clearFilterText();"} ✕
|
||||
.sn-component#notes-menu-bar
|
||||
.app-bar.no-edges
|
||||
@@ -25,35 +26,35 @@
|
||||
.header
|
||||
%h4.title Sort By
|
||||
|
||||
%menu-row{"title" => "'Date Added'", "circle" => "ctrl.sortBy == 'created_at' && 'success'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.selectedSortByCreated()"}
|
||||
%menu-row{"title" => "'Date Modified'", "circle" => "ctrl.sortBy == 'updated_at' && 'success'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.selectedSortByUpdated()"}
|
||||
%menu-row{"title" => "'Title'", "circle" => "ctrl.sortBy == 'title' && 'success'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.selectedSortByTitle()"}
|
||||
%menu-row{"label" => "'Date Added'", "circle" => "ctrl.sortBy == 'created_at' && 'success'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.selectedSortByCreated()", "desc" => "'Sort notes by newest first'"}
|
||||
%menu-row{"label" => "'Date Modified'", "circle" => "ctrl.sortBy == 'client_updated_at' && 'success'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.selectedSortByUpdated()", "desc" => "'Sort notes with the most recently updated first'"}
|
||||
%menu-row{"label" => "'Title'", "circle" => "ctrl.sortBy == 'title' && 'success'", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.selectedSortByTitle()", "desc" => "'Sort notes alphabetically by their title'"}
|
||||
|
||||
.section{"ng-if" => "!ctrl.tag.archiveTag"}
|
||||
.header
|
||||
%h4.title Display
|
||||
|
||||
%menu-row{"title" => "'Archived Notes'", "circle" => "ctrl.showArchived ? 'success' : 'danger'", "faded" => "!ctrl.showArchived", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('showArchived')"}
|
||||
%menu-row{"title" => "'Pinned Notes'", "circle" => "ctrl.hidePinned ? 'danger' : 'success'", "faded" => "ctrl.hidePinned", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('hidePinned')"}
|
||||
%menu-row{"title" => "'Note Preview'", "circle" => "ctrl.hideNotePreview ? 'danger' : 'success'", "faded" => "ctrl.hideNotePreview", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('hideNotePreview')"}
|
||||
%menu-row{"title" => "'Date'", "circle" => "ctrl.hideDate ? 'danger' : 'success'","faded" => "ctrl.hideDate", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('hideDate')"}
|
||||
%menu-row{"title" => "'Tags'", "circle" => "ctrl.hideTags ? 'danger' : 'success'","faded" => "ctrl.hideTags", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('hideTags')"}
|
||||
%menu-row{"label" => "'Archived Notes'", "circle" => "ctrl.showArchived ? 'success' : 'danger'", "faded" => "!ctrl.showArchived", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('showArchived')", "desc" => "'Archived notes are usually hidden. You can explicitly show them with this option.'"}
|
||||
%menu-row{"label" => "'Pinned Notes'", "circle" => "ctrl.hidePinned ? 'danger' : 'success'", "faded" => "ctrl.hidePinned", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('hidePinned')", "desc" => "'Pinned notes always appear on top. You can hide them temporarily with this option so you can focus on other notes in the list.'"}
|
||||
%menu-row{"label" => "'Note Preview'", "circle" => "ctrl.hideNotePreview ? 'danger' : 'success'", "faded" => "ctrl.hideNotePreview", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('hideNotePreview')", "desc" => "'Hide the note preview for a more condensed list of notes'"}
|
||||
%menu-row{"label" => "'Date'", "circle" => "ctrl.hideDate ? 'danger' : 'success'","faded" => "ctrl.hideDate", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('hideDate')", "desc" => "'Hide the date displayed in each row'"}
|
||||
%menu-row{"label" => "'Tags'", "circle" => "ctrl.hideTags ? 'danger' : 'success'","faded" => "ctrl.hideTags", "ng-click" => "ctrl.selectedMenuItem($event); ctrl.toggleKey('hideTags')", "desc" => "'Hide the list of tags associated with each note'"}
|
||||
|
||||
|
||||
.scrollable
|
||||
.infinite-scroll#notes-scrollable{"infinite-scroll" => "ctrl.paginate()", "can-load" => "true", "threshold" => "200"}
|
||||
.note{"ng-repeat" => "note in (ctrl.sortedNotes = (ctrl.tag.notes | filter: ctrl.filterNotes | sortBy: ctrl.sortBy | limitTo:ctrl.notesToDisplay)) track by note.uuid",
|
||||
"ng-click" => "ctrl.selectNote(note, true)", "ng-class" => "{'selected' : ctrl.selectedNote == note}"}
|
||||
%strong.red.medium{"ng-if" => "note.conflict_of"} Conflicted copy
|
||||
%strong.red.medium{"ng-if" => "note.errorDecrypting"} Error decrypting
|
||||
%strong.red.medium-text{"ng-if" => "note.conflict_of"} Conflicted copy
|
||||
%strong.red.medium-text{"ng-if" => "note.errorDecrypting"} Error decrypting
|
||||
|
||||
.pinned.tinted{"ng-if" => "note.pinned", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
|
||||
%i.icon.ion-bookmark
|
||||
%strong.medium Pinned
|
||||
%strong.medium-text Pinned
|
||||
|
||||
.archived.tinted{"ng-if" => "note.archived && !ctrl.tag.archiveTag", "ng-class" => "{'tinted-selected' : ctrl.selectedNote == note}"}
|
||||
%i.icon.ion-ios-box
|
||||
%strong.medium Archived
|
||||
%strong.medium-text Archived
|
||||
|
||||
.tags-string{"ng-if" => "ctrl.shouldShowTags(note)"}
|
||||
.faded {{note.savedTagsString || note.tagsString()}}
|
||||
@@ -63,7 +64,7 @@
|
||||
.note-preview{"ng-if" => "!ctrl.hideNotePreview"}
|
||||
{{note.text}}
|
||||
.date.faded{"ng-if" => "!ctrl.hideDate"}
|
||||
%span{"ng-if" => "ctrl.sortBy == 'updated_at'"} Modified {{note.updatedAtString() || 'Now'}}
|
||||
%span{"ng-if" => "ctrl.sortBy != 'updated_at'"} {{note.createdAtString() || 'Now'}}
|
||||
%span{"ng-if" => "ctrl.sortBy == 'client_updated_at'"} Modified {{note.updatedAtString() || 'Now'}}
|
||||
%span{"ng-if" => "ctrl.sortBy != 'client_updated_at'"} {{note.createdAtString() || 'Now'}}
|
||||
|
||||
%panel-resizer{"panel-id" => "'notes-column'", "on-resize-finish" => "ctrl.onPanelResize", "control" => "ctrl.panelController", "hoverable" => "true", "collapsable" => "true"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.section.tags#tags-column
|
||||
.section.tags#tags-column{"aria-label" => "Tags"}
|
||||
|
||||
.component-view-container{"ng-if" => "ctrl.component.active"}
|
||||
%component-view.component-view{"component" => "ctrl.component"}
|
||||
@@ -7,7 +7,7 @@
|
||||
#tags-title-bar.section-title-bar
|
||||
.section-title-bar-header
|
||||
.title Tags
|
||||
.add-button#tag-add-button{"ng-click" => "ctrl.clickedAddNewTag()"} +
|
||||
.add-button#tag-add-button{"ng-click" => "ctrl.clickedAddNewTag()", "title" => "Create a new tag"} +
|
||||
|
||||
.scrollable
|
||||
.infinite-scroll
|
||||
@@ -22,8 +22,8 @@
|
||||
"ng-change" => "ctrl.tagTitleDidChange(tag)", "ng-blur" => "ctrl.saveTag($event, tag)", "spellcheck" => "false"}
|
||||
.count {{ctrl.noteCount(tag)}}
|
||||
|
||||
.red.small.bold{"ng-if" => "tag.conflict_of"} Conflicted copy
|
||||
.red.small.bold{"ng-if" => "tag.errorDecrypting"} Error decrypting
|
||||
.red.small-text.bold{"ng-if" => "tag.conflict_of"} Conflicted copy
|
||||
.red.small-text.bold{"ng-if" => "tag.errorDecrypting"} Error decrypting
|
||||
|
||||
.menu{"ng-if" => "ctrl.selectedTag == tag"}
|
||||
%a.item{"ng-click" => "ctrl.selectedRenameTag($event, tag)", "ng-if" => "!ctrl.editingTag"} Rename
|
||||
|
||||
9720
package-lock.json
generated
9720
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,10 @@
|
||||
"angular": "^1.6.1",
|
||||
"angular-mocks": "^1.6.1",
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-plugin-angularjs-annotate": "^0.9.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.1.1",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-es2016": "^6.16.0",
|
||||
"bower": "^1.8.0",
|
||||
"grunt": "^1.0.1",
|
||||
@@ -33,8 +36,8 @@
|
||||
"karma-cli": "^1.0.1",
|
||||
"karma-jasmine": "^1.1.0",
|
||||
"karma-phantomjs-launcher": "^1.0.2",
|
||||
"sn-stylekit": "1.0.12",
|
||||
"standard-file-js": "0.0.14"
|
||||
"sn-stylekit": "1.0.15",
|
||||
"standard-file-js": "0.3.0"
|
||||
},
|
||||
"license": "GPL-3.0"
|
||||
}
|
||||
|
||||
Submodule public/extensions/batch-manager updated: ef738c9f34...0cf5462bfb
Reference in New Issue
Block a user