functional minimalism
This commit is contained in:
@@ -18,6 +18,20 @@ angular.module('app.frontend', [
|
||||
'restangular'
|
||||
])
|
||||
|
||||
.config(function (RestangularProvider, apiControllerProvider) {
|
||||
.config(function (RestangularProvider, authManagerProvider) {
|
||||
RestangularProvider.setDefaultHeaders({"Content-Type": "application/json"});
|
||||
|
||||
RestangularProvider.setFullRequestInterceptor(function(element, operation, route, url, headers, params, httpConfig) {
|
||||
var token = localStorage.getItem("jwt");
|
||||
if(token) {
|
||||
headers = _.extend(headers, {Authorization: "Bearer " + localStorage.getItem("jwt")});
|
||||
}
|
||||
|
||||
return {
|
||||
element: element,
|
||||
params: params,
|
||||
headers: headers,
|
||||
httpConfig: httpConfig
|
||||
};
|
||||
});
|
||||
})
|
||||
|
||||
@@ -88,7 +88,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager, syncManager) {
|
||||
.controller('EditorCtrl', function ($sce, $timeout, authManager, markdownRenderer, $rootScope, extensionManager, syncManager) {
|
||||
|
||||
this.setNote = function(note, oldNote) {
|
||||
this.editorMode = 'edit';
|
||||
@@ -149,7 +149,7 @@ angular.module('app.frontend')
|
||||
if(statusTimeout) $timeout.cancel(statusTimeout);
|
||||
statusTimeout = $timeout(function(){
|
||||
var status = "All changes saved"
|
||||
if(syncManager.offline) {
|
||||
if(authManager.offline()) {
|
||||
status += " (offline)";
|
||||
}
|
||||
this.saveError = false;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('app.frontend')
|
||||
.directive("header", function(apiController){
|
||||
.directive("header", function(authManager){
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {},
|
||||
@@ -22,17 +22,17 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager, syncManager) {
|
||||
.controller('HeaderCtrl', function (authManager, modelManager, $timeout, dbManager, syncManager) {
|
||||
|
||||
this.user = apiController.user;
|
||||
this.user = authManager.user;
|
||||
|
||||
this.updateOfflineStatus = function() {
|
||||
this.offline = syncManager.offline;
|
||||
this.offline = authManager.offline();
|
||||
}
|
||||
this.updateOfflineStatus();
|
||||
|
||||
this.findErrors = function() {
|
||||
this.error = syncManager.syncProviders.filter(function(provider){return provider.error}).length > 0 ? true : false;
|
||||
this.error = syncManager.error;
|
||||
}
|
||||
this.findErrors();
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
angular.module('app.frontend')
|
||||
.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager) {
|
||||
.controller('HomeCtrl', function ($scope, $rootScope, $timeout, modelManager, syncManager, authManager) {
|
||||
$rootScope.bodyClass = "app-body-class";
|
||||
|
||||
syncManager.loadLocalItems(function(items){
|
||||
syncManager.loadLocalItems(function(items) {
|
||||
$scope.$apply();
|
||||
|
||||
syncManager.sync(null);
|
||||
@@ -138,7 +138,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
syncManager.sync(function(){
|
||||
if(syncManager.offline) {
|
||||
if(authManager.offline()) {
|
||||
// when deleting items while ofline, we need to explictly tell angular to refresh UI
|
||||
setTimeout(function () {
|
||||
$scope.safeApply();
|
||||
|
||||
@@ -24,7 +24,7 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
})
|
||||
.controller('NotesCtrl', function (apiController, $timeout, $rootScope, modelManager) {
|
||||
.controller('NotesCtrl', function (authManager, $timeout, $rootScope, modelManager) {
|
||||
|
||||
$rootScope.$on("editorFocused", function(){
|
||||
this.showMenu = false;
|
||||
|
||||
@@ -56,10 +56,6 @@ class Extension extends Item {
|
||||
this.content_type = "Extension";
|
||||
}
|
||||
|
||||
get syncProviderAction() {
|
||||
return _.find(this.actions, {sync_provider: true})
|
||||
}
|
||||
|
||||
actionsInGlobalContext() {
|
||||
return this.actions.filter(function(action){
|
||||
return action.context == "global" || action.sync_provider == true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
angular.module('app.frontend')
|
||||
.provider('apiController', function () {
|
||||
.provider('authManager', function () {
|
||||
|
||||
function domainName() {
|
||||
var domain_comps = location.hostname.split(".");
|
||||
@@ -7,11 +7,11 @@ angular.module('app.frontend')
|
||||
return domain;
|
||||
}
|
||||
|
||||
this.$get = function($rootScope, Restangular, modelManager, dbManager, syncManager) {
|
||||
return new ApiController($rootScope, Restangular, modelManager, dbManager, syncManager);
|
||||
this.$get = function($rootScope, Restangular, modelManager) {
|
||||
return new AuthManager($rootScope, Restangular, modelManager);
|
||||
}
|
||||
|
||||
function ApiController($rootScope, Restangular, modelManager, dbManager, syncManager) {
|
||||
function AuthManager($rootScope, Restangular, modelManager) {
|
||||
|
||||
var userData = localStorage.getItem("user");
|
||||
if(userData) {
|
||||
@@ -24,9 +24,9 @@ angular.module('app.frontend')
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Auth
|
||||
*/
|
||||
this.offline = function() {
|
||||
return !this.user;
|
||||
}
|
||||
|
||||
this.getAuthParams = function() {
|
||||
return JSON.parse(localStorage.getItem("auth_params"));
|
||||
@@ -91,15 +91,11 @@ angular.module('app.frontend')
|
||||
}
|
||||
|
||||
this.handleAuthResponse = function(response, email, url, authParams, mk) {
|
||||
var params = {
|
||||
url: url,
|
||||
email: email,
|
||||
uuid: response.user.uuid,
|
||||
ek: mk,
|
||||
jwt: response.token,
|
||||
auth_params: _.omit(authParams, ["pw_nonce"])
|
||||
}
|
||||
syncManager.addAccountBasedSyncProvider(params);
|
||||
localStorage.setItem("server", url);
|
||||
localStorage.setItem("user", JSON.stringify(response.plain()));
|
||||
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
|
||||
localStorage.setItem("mk", mk);
|
||||
localStorage.setItem("jwt", response.token);
|
||||
}
|
||||
|
||||
this.register = function(url, email, password, callback) {
|
||||
@@ -259,14 +255,5 @@ angular.module('app.frontend')
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
this.destroyLocalData = function(callback) {
|
||||
dbManager.clearAllItems(function(){
|
||||
localStorage.clear();
|
||||
if(callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
@@ -1,27 +0,0 @@
|
||||
class AccountDataMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-data-menu.html";
|
||||
this.scope = {};
|
||||
}
|
||||
|
||||
controller($scope, apiController, modelManager, keyManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.keys = keyManager.keys;
|
||||
|
||||
$scope.destroyLocalData = function() {
|
||||
if(!confirm("Are you sure you want to end your session? This will delete all local items, sync accounts, keys, and extensions.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
apiController.destroyLocalData(function(){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('accountDataMenu', () => new AccountDataMenu);
|
||||
@@ -1,79 +0,0 @@
|
||||
class AccountExportSection {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-menu/account-export-section.html";
|
||||
this.scope = {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, apiController, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.archiveFormData = {encryption_type: $scope.user ? 'mk' : 'ek'};
|
||||
$scope.user = apiController.user;
|
||||
|
||||
$scope.downloadDataArchive = function() {
|
||||
if($scope.archiveFormData.encryption_type == 'ek') {
|
||||
if(!$scope.archiveFormData.ek) {
|
||||
alert("You must set an encryption key to export the data encrypted.")
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', 'notes.json');
|
||||
|
||||
var ek = $scope.archiveFormData.encryption_type == 'ek' ? $scope.archiveFormData.ek : null;
|
||||
var encrypted = $scope.archiveFormData.encryption_type != 'none';
|
||||
|
||||
link.href = apiController.itemsDataFile(encrypted, ek);
|
||||
link.click();
|
||||
}
|
||||
|
||||
$scope.performImport = function(data, password) {
|
||||
$scope.importData.loading = true;
|
||||
// allow loading indicator to come up with timeout
|
||||
$timeout(function(){
|
||||
apiController.importJSONData(data, password, function(success, response){
|
||||
console.log("Import response:", success, response);
|
||||
$scope.importData.loading = false;
|
||||
if(success) {
|
||||
$scope.importData = null;
|
||||
} else {
|
||||
alert("There was an error importing your data. Please try again.");
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.submitImportPassword = function() {
|
||||
$scope.performImport($scope.importData.data, $scope.importData.password);
|
||||
}
|
||||
|
||||
$scope.importFileSelected = function(files) {
|
||||
$scope.importData = {};
|
||||
|
||||
var file = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var data = JSON.parse(e.target.result);
|
||||
$timeout(function(){
|
||||
if(data.auth_params) {
|
||||
// request password
|
||||
$scope.importData.requestPassword = true;
|
||||
$scope.importData.data = data;
|
||||
} else {
|
||||
$scope.performImport(data, null);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('accountExportSection', () => new AccountExportSection);
|
||||
@@ -1,28 +0,0 @@
|
||||
class AccountKeysSection {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-menu/account-keys-section.html";
|
||||
this.scope = {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, apiController, keyManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.newKeyData = {};
|
||||
$scope.keys = keyManager.keys;
|
||||
|
||||
$scope.submitNewKeyForm = function() {
|
||||
var key = keyManager.addKey($scope.newKeyData.name, $scope.newKeyData.key);
|
||||
if(!key) {
|
||||
alert("This key name is already in use. Please use a different name.");
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.newKeyData.showForm = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('accountKeysSection', () => new AccountKeysSection);
|
||||
@@ -1,81 +0,0 @@
|
||||
class AccountNewAccountSection {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-menu/account-new-account-section.html";
|
||||
this.scope = {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {url: syncManager.defaultServerURL()};
|
||||
$scope.user = apiController.user;
|
||||
|
||||
$scope.showForm = syncManager.syncProviders.length == 0;
|
||||
|
||||
$scope.changePasswordPressed = function() {
|
||||
$scope.showNewPasswordForm = !$scope.showNewPasswordForm;
|
||||
}
|
||||
|
||||
$scope.submitExternalSyncURL = function() {
|
||||
syncManager.addSyncProviderFromURL($scope.formData.secretUrl);
|
||||
$scope.formData.showAddLinkForm = false;
|
||||
$scope.formData.secretUrl = null;
|
||||
$scope.showForm = false;
|
||||
}
|
||||
|
||||
$scope.submitPasswordChange = function() {
|
||||
$scope.passwordChangeData.status = "Generating New Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
if(data.password != data.password_confirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
return;
|
||||
}
|
||||
|
||||
apiController.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
$scope.loginSubmitPressed = function() {
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
console.log("logging in with url", $scope.formData.url);
|
||||
$timeout(function(){
|
||||
apiController.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
||||
$scope.formData.status = null;
|
||||
if(!response || response.error) {
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
if(!response || (response && !response.didDisplayAlert)) {
|
||||
alert(error.message);
|
||||
}
|
||||
} else {
|
||||
$scope.showForm = false;
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.submitRegistrationForm = function() {
|
||||
$scope.formData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
apiController.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
||||
$scope.formData.status = null;
|
||||
if(!response || response.error) {
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
alert(error.message);
|
||||
} else {
|
||||
$scope.showForm = false;
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('accountNewAccountSection', () => new AccountNewAccountSection);
|
||||
@@ -1,65 +0,0 @@
|
||||
class AccountSyncSection {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-menu/account-sync-section.html";
|
||||
this.scope = {
|
||||
};
|
||||
}
|
||||
|
||||
controller($scope, modelManager, keyManager, syncManager) {
|
||||
'ngInject';
|
||||
|
||||
$scope.syncManager = syncManager;
|
||||
$scope.syncProviders = syncManager.syncProviders;
|
||||
$scope.keys = keyManager.keys;
|
||||
// $scope.showSection = syncManager.syncProviders.length > 0;
|
||||
|
||||
$scope.enableSyncProvider = function(provider, primary) {
|
||||
if(!provider.keyName) {
|
||||
alert("You must choose an encryption key for this account before enabling it.");
|
||||
return;
|
||||
}
|
||||
|
||||
syncManager.enableSyncProvider(provider, primary);
|
||||
}
|
||||
|
||||
$scope.removeSyncProvider = function(provider) {
|
||||
if(provider.primary) {
|
||||
alert("You cannot remove your main sync account. Instead, end your session by destroying all local data. Or, choose another account to be your primary sync account.")
|
||||
return;
|
||||
}
|
||||
|
||||
if(confirm("Are you sure you want to remove this sync account?")) {
|
||||
syncManager.removeSyncProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.changeEncryptionKey = function(provider) {
|
||||
if(provider.isStandardNotesAccount) {
|
||||
alert("To change your encryption key for your Standard File account, you need to change your password. However, this functionality is not currently available.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!confirm("Changing your encryption key will re-encrypt all your notes with the new key and sync them back to the server. This can take several minutes. We strongly recommend downloading a backup of your notes before continuing.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
provider.formData = {keyName: provider.keyName};
|
||||
provider.showKeyForm = true;
|
||||
}
|
||||
|
||||
$scope.saveKey = function(provider) {
|
||||
provider.showKeyForm = false;
|
||||
provider.keyName = provider.formData.keyName;
|
||||
syncManager.didMakeChangesToSyncProviders();
|
||||
|
||||
if(provider.enabled) {
|
||||
syncManager.addAllDataAsNeedingSyncForProvider(provider);
|
||||
syncManager.sync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('accountSyncSection', () => new AccountSyncSection);
|
||||
@@ -0,0 +1,152 @@
|
||||
class AccountMenu {
|
||||
|
||||
constructor() {
|
||||
this.restrict = "E";
|
||||
this.templateUrl = "frontend/directives/account-menu.html";
|
||||
this.scope = {};
|
||||
}
|
||||
|
||||
controller($scope, authManager, modelManager, syncManager, $timeout) {
|
||||
'ngInject';
|
||||
|
||||
$scope.formData = {url: syncManager.serverURL};
|
||||
$scope.user = authManager.user;
|
||||
|
||||
$scope.changePasswordPressed = function() {
|
||||
$scope.showNewPasswordForm = !$scope.showNewPasswordForm;
|
||||
}
|
||||
|
||||
$scope.submitPasswordChange = function() {
|
||||
$scope.passwordChangeData.status = "Generating New Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
if(data.password != data.password_confirmation) {
|
||||
alert("Your new password does not match its confirmation.");
|
||||
return;
|
||||
}
|
||||
|
||||
authManager.changePassword($scope.passwordChangeData.current_password, $scope.passwordChangeData.new_password, function(response){
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
$scope.loginSubmitPressed = function() {
|
||||
$scope.formData.status = "Generating Login Keys...";
|
||||
console.log("logging in with url", $scope.formData.url);
|
||||
$timeout(function(){
|
||||
authManager.login($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
||||
$scope.formData.status = null;
|
||||
if(!response || response.error) {
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
if(!response || (response && !response.didDisplayAlert)) {
|
||||
alert(error.message);
|
||||
}
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.submitRegistrationForm = function() {
|
||||
$scope.formData.status = "Generating Account Keys...";
|
||||
|
||||
$timeout(function(){
|
||||
authManager.register($scope.formData.url, $scope.formData.email, $scope.formData.user_password, function(response){
|
||||
$scope.formData.status = null;
|
||||
if(!response || response.error) {
|
||||
var error = response ? response.error : {message: "An unknown error occured."}
|
||||
alert(error.message);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
$scope.destroyLocalData = function() {
|
||||
if(!confirm("Are you sure you want to end your session? This will delete all local items and extensions.")) {
|
||||
return;
|
||||
}
|
||||
|
||||
syncManager.destroyLocalData(function(){
|
||||
window.location.reload();
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/* Import/Export */
|
||||
|
||||
$scope.archiveFormData = {encryption_type: $scope.user ? 'mk' : 'ek'};
|
||||
$scope.user = authManager.user;
|
||||
|
||||
$scope.downloadDataArchive = function() {
|
||||
if($scope.archiveFormData.encryption_type == 'ek') {
|
||||
if(!$scope.archiveFormData.ek) {
|
||||
alert("You must set an encryption key to export the data encrypted.")
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var link = document.createElement('a');
|
||||
link.setAttribute('download', 'notes.json');
|
||||
|
||||
var ek = $scope.archiveFormData.encryption_type == 'ek' ? $scope.archiveFormData.ek : null;
|
||||
var encrypted = $scope.archiveFormData.encryption_type != 'none';
|
||||
|
||||
link.href = authManager.itemsDataFile(encrypted, ek);
|
||||
link.click();
|
||||
}
|
||||
|
||||
$scope.performImport = function(data, password) {
|
||||
$scope.importData.loading = true;
|
||||
// allow loading indicator to come up with timeout
|
||||
$timeout(function(){
|
||||
authManager.importJSONData(data, password, function(success, response){
|
||||
console.log("Import response:", success, response);
|
||||
$scope.importData.loading = false;
|
||||
if(success) {
|
||||
$scope.importData = null;
|
||||
} else {
|
||||
alert("There was an error importing your data. Please try again.");
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.submitImportPassword = function() {
|
||||
$scope.performImport($scope.importData.data, $scope.importData.password);
|
||||
}
|
||||
|
||||
$scope.importFileSelected = function(files) {
|
||||
$scope.importData = {};
|
||||
|
||||
var file = files[0];
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var data = JSON.parse(e.target.result);
|
||||
$timeout(function(){
|
||||
if(data.auth_params) {
|
||||
// request password
|
||||
$scope.importData.requestPassword = true;
|
||||
$scope.importData.data = data;
|
||||
} else {
|
||||
$scope.performImport(data, null);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
$scope.encryptionStatusForNotes = function() {
|
||||
var allNotes = modelManager.filteredNotes;
|
||||
return allNotes.length + "/" + allNotes.length + " notes encrypted";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').directive('accountMenu', () => new AccountMenu);
|
||||
@@ -1,9 +1,9 @@
|
||||
class ExtensionManager {
|
||||
|
||||
constructor(Restangular, modelManager, apiController, syncManager) {
|
||||
constructor(Restangular, modelManager, authManager, syncManager) {
|
||||
this.Restangular = Restangular;
|
||||
this.modelManager = modelManager;
|
||||
this.apiController = apiController;
|
||||
this.authManager = authManager;
|
||||
this.enabledRepeatActionUrls = JSON.parse(localStorage.getItem("enabledRepeatActionUrls")) || [];
|
||||
this.decryptedExtensions = JSON.parse(localStorage.getItem("decryptedExtensions")) || [];
|
||||
this.extensionEks = JSON.parse(localStorage.getItem("extensionEks")) || {};
|
||||
@@ -288,7 +288,7 @@ class ExtensionManager {
|
||||
performPost(action, extension, params, callback) {
|
||||
var request = this.Restangular.oneUrl(action.url, action.url);
|
||||
if(this.extensionUsesEncryptedData(extension)) {
|
||||
request.auth_params = this.apiController.getAuthParams();
|
||||
request.auth_params = this.authManager.getAuthParams();
|
||||
}
|
||||
_.merge(request, params);
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
class KeyManager {
|
||||
|
||||
constructor() {
|
||||
this.keys = JSON.parse(localStorage.getItem("keys")) || [];
|
||||
}
|
||||
|
||||
addKey(name, key) {
|
||||
var existing = this.keyForName(name);
|
||||
if(existing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var newKey = {name: name, key: key};
|
||||
this.keys.push(newKey);
|
||||
this.persist();
|
||||
return newKey;
|
||||
}
|
||||
|
||||
keyForName(name) {
|
||||
var keyObj = _.find(this.keys, function(key){
|
||||
return key.name.toLowerCase() == name.toLowerCase();
|
||||
});
|
||||
|
||||
return keyObj ? keyObj.key : null;
|
||||
}
|
||||
|
||||
deleteKey(name) {
|
||||
_.pull(this.keys, {name: name});
|
||||
this.persist();
|
||||
}
|
||||
|
||||
persist() {
|
||||
localStorage.setItem("keys", JSON.stringify(this.keys));
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('keyManager', KeyManager);
|
||||
@@ -1,172 +0,0 @@
|
||||
export const SNKeyName = "Standard Notes Key";
|
||||
|
||||
class SyncManager {
|
||||
|
||||
constructor(modelManager, syncRunner, keyManager) {
|
||||
this.modelManager = modelManager;
|
||||
this.keyManager = keyManager;
|
||||
this.syncRunner = syncRunner;
|
||||
this.syncRunner.setOnChangeProviderCallback(function(){
|
||||
this.didMakeChangesToSyncProviders();
|
||||
}.bind(this))
|
||||
this.loadSyncProviders();
|
||||
}
|
||||
|
||||
get offline() {
|
||||
return this.enabledProviders.length == 0;
|
||||
}
|
||||
|
||||
defaultServerURL() {
|
||||
// return "https://n3.standardnotes.org";
|
||||
return "http://localhost:3000";
|
||||
}
|
||||
|
||||
get enabledProviders() {
|
||||
return this.syncProviders.filter(function(provider){return provider.enabled == true});
|
||||
}
|
||||
|
||||
/* Used when adding a new account with */
|
||||
markAllOfflineItemsDirtyAndSave() {
|
||||
|
||||
}
|
||||
|
||||
sync(callback) {
|
||||
this.syncRunner.sync(this.enabledProviders, callback);
|
||||
}
|
||||
|
||||
syncWithProvider(provider, callback) {
|
||||
this.syncRunner.performSyncWithProvider(provider, callback);
|
||||
}
|
||||
|
||||
loadLocalItems(callback) {
|
||||
this.syncRunner.loadLocalItems(callback);
|
||||
}
|
||||
|
||||
syncProviderForURL(url) {
|
||||
var provider = _.find(this.syncProviders, {url: url});
|
||||
return provider;
|
||||
}
|
||||
|
||||
findOrCreateSyncProviderForUrl(url) {
|
||||
var provider = _.find(this.syncProviders, {url: url});
|
||||
if(!provider) {
|
||||
provider = new SyncProvider({url: url})
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
setEncryptionStatusForProviderURL(providerURL, encrypted) {
|
||||
this.providerForURL(providerURL).encrypted = encrypted;
|
||||
this.didMakeChangesToSyncProviders();
|
||||
}
|
||||
|
||||
get primarySyncProvider() {
|
||||
return _.find(this.syncProviders, {primary: true});
|
||||
}
|
||||
|
||||
didMakeChangesToSyncProviders() {
|
||||
localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) {
|
||||
return provider.asJSON()
|
||||
})));
|
||||
}
|
||||
|
||||
loadSyncProviders() {
|
||||
this.syncProviders = [];
|
||||
var saved = localStorage.getItem("syncProviders");
|
||||
if(saved) {
|
||||
var parsed = JSON.parse(saved);
|
||||
for(var p of parsed) {
|
||||
this.syncProviders.push(new SyncProvider(p));
|
||||
}
|
||||
} else {
|
||||
// no providers saved, this means migrating from old system to new
|
||||
// check if user is signed in
|
||||
var userJSON = localStorage.getItem("user");
|
||||
if(this.offline && userJSON) {
|
||||
var user = JSON.parse(userJSON);
|
||||
var params = {
|
||||
url: localStorage.getItem("server"),
|
||||
email: user.email,
|
||||
uuid: user.uuid,
|
||||
ek: localStorage.getItem("mk"),
|
||||
jwt: response.token,
|
||||
auth_params: JSON.parse(localStorage.getItem("auth_params")),
|
||||
}
|
||||
var defaultProvider = this.addAccountBasedSyncProvider(params);
|
||||
defaultProvider.syncToken = localStorage.getItem("syncToken");
|
||||
localStorage.removeItem("mk");
|
||||
localStorage.removeItem("syncToken");
|
||||
localStorage.removeItem("auth_params");
|
||||
localStorage.removeItem("user");
|
||||
localStorage.removeItem("server");
|
||||
this.didMakeChangesToSyncProviders();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addAccountBasedSyncProvider({url, email, uuid, ek, jwt, auth_params} = {}) {
|
||||
var provider = new SyncProvider({
|
||||
url: url + "/items/sync",
|
||||
primary: !this.primarySyncProvider,
|
||||
email: email,
|
||||
uuid: uuid,
|
||||
jwt: jwt,
|
||||
auth_params: auth_params,
|
||||
type: SN.SyncProviderType.account
|
||||
});
|
||||
|
||||
provider.keyName = provider.name;
|
||||
|
||||
this.syncProviders.push(provider);
|
||||
|
||||
this.keyManager.addKey(provider.keyName, ek);
|
||||
|
||||
this.enableSyncProvider(provider, this.enabledProviders == 0);
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
addSyncProviderFromURL(url) {
|
||||
var provider = new SyncProvider({url: url});
|
||||
provider.type = SN.SyncProviderType.URL;
|
||||
this.syncProviders.push(provider);
|
||||
this.didMakeChangesToSyncProviders();
|
||||
}
|
||||
|
||||
enableSyncProvider(syncProvider, primary) {
|
||||
// we want to sync the new provider where our current primary one is
|
||||
syncProvider.syncToken = this.primarySyncProvider ? this.primarySyncProvider.syncToken : null;
|
||||
|
||||
if(primary) {
|
||||
for(var provider of this.syncProviders) {
|
||||
provider.primary = false;
|
||||
}
|
||||
}
|
||||
|
||||
syncProvider.enabled = true;
|
||||
syncProvider.primary = primary;
|
||||
|
||||
// since we're enabling a new provider, we need to send it EVERYTHING we have now.
|
||||
this.addAllDataAsNeedingSyncForProvider(syncProvider);
|
||||
this.didMakeChangesToSyncProviders();
|
||||
this.syncWithProvider(syncProvider);
|
||||
}
|
||||
|
||||
addAllDataAsNeedingSyncForProvider(syncProvider) {
|
||||
syncProvider.addPendingItems(this.modelManager.allItems);
|
||||
}
|
||||
|
||||
removeSyncProvider(provider) {
|
||||
_.pull(this.syncProviders, provider);
|
||||
this.didMakeChangesToSyncProviders();
|
||||
}
|
||||
|
||||
clearSyncToken() {
|
||||
var primary = this.primarySyncProvider;
|
||||
if(primary) {
|
||||
primary.syncToken = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('syncManager', SyncManager);
|
||||
@@ -1,77 +0,0 @@
|
||||
SN.SyncProviderType = {
|
||||
Account: 1,
|
||||
URL: 2
|
||||
}
|
||||
|
||||
class SyncProvider {
|
||||
|
||||
constructor(obj) {
|
||||
this.encrypted = true;
|
||||
this.syncStatus = new SyncStatus();
|
||||
_.merge(this, obj);
|
||||
}
|
||||
|
||||
addPendingItems(items) {
|
||||
if(!this.pendingItems) {
|
||||
this.pendingItems = [];
|
||||
}
|
||||
|
||||
this.pendingItems = _.uniqBy(this.pendingItems.concat(items), "uuid");
|
||||
}
|
||||
|
||||
removePendingItems(items) {
|
||||
this.pendingItems = _.difference(this.pendingItems, items);
|
||||
}
|
||||
|
||||
get isStandardNotesAccount() {
|
||||
return this.keyName == SNKeyName;
|
||||
}
|
||||
|
||||
get secondary() {
|
||||
return this.status == "secondary";
|
||||
}
|
||||
|
||||
get status() {
|
||||
if(!this.enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(this.primary) return "primary";
|
||||
else return "secondary";
|
||||
}
|
||||
|
||||
get name() {
|
||||
if(this.type == SN.SyncProviderType.account) {
|
||||
return this.email + "@" + this.url;
|
||||
} else {
|
||||
return this.url;
|
||||
}
|
||||
}
|
||||
|
||||
asJSON() {
|
||||
return {
|
||||
enabled: this.enabled,
|
||||
url: this.url,
|
||||
type: this.type,
|
||||
primary: this.primary,
|
||||
keyName: this.keyName,
|
||||
syncToken: this.syncToken,
|
||||
|
||||
// account based
|
||||
email: this.email,
|
||||
uuid: this.uuid,
|
||||
jwt: this.jwt,
|
||||
auth_params: this.auth_params
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SyncStatus {
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
get statusString() {
|
||||
return `${this.current}/${this.total}`
|
||||
}
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
class SyncRunner {
|
||||
|
||||
constructor($rootScope, modelManager, dbManager, keyManager, Restangular) {
|
||||
this.rootScope = $rootScope;
|
||||
this.modelManager = modelManager;
|
||||
this.dbManager = dbManager;
|
||||
this.keyManager = keyManager;
|
||||
this.Restangular = Restangular;
|
||||
}
|
||||
|
||||
setOnChangeProviderCallback(callback) {
|
||||
this.onChangeProviderCallback = callback;
|
||||
}
|
||||
|
||||
didMakeChangesToSyncProvider(provider) {
|
||||
this.onChangeProviderCallback(provider);
|
||||
}
|
||||
|
||||
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||
var params = items.map(function(item) {
|
||||
var itemParams = new ItemParams(item, null);
|
||||
itemParams = itemParams.paramsForLocalStorage();
|
||||
if(offlineOnly) {
|
||||
delete itemParams.dirty;
|
||||
}
|
||||
return itemParams;
|
||||
}.bind(this));
|
||||
|
||||
this.dbManager.saveItems(params, callback);
|
||||
}
|
||||
|
||||
loadLocalItems(callback) {
|
||||
var params = this.dbManager.getAllItems(function(items){
|
||||
var items = this.handleItemsResponse(items, null, null);
|
||||
Item.sortItemsByDate(items);
|
||||
callback(items);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
syncOffline(items, callback) {
|
||||
this.writeItemsToLocalStorage(items, true, function(responseItems){
|
||||
// delete anything needing to be deleted
|
||||
for(var item of items) {
|
||||
if(item.deleted) {
|
||||
this.modelManager.removeItemLocally(item);
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
if(callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
sync(providers, callback, options = {}) {
|
||||
|
||||
var allDirtyItems = this.modelManager.getDirtyItems();
|
||||
|
||||
// we want to write all dirty items to disk only if the user has no sync providers, or if the sync op fails
|
||||
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
||||
if(providers.length == 0) {
|
||||
this.syncOffline(allDirtyItems, callback);
|
||||
}
|
||||
|
||||
for(let provider of providers) {
|
||||
provider.addPendingItems(allDirtyItems);
|
||||
this.didMakeChangesToSyncProvider(provider);
|
||||
|
||||
this.__performSyncWithProvider(provider, options, function(response){
|
||||
if(provider.primary) {
|
||||
if(callback) {
|
||||
callback(response)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.modelManager.clearDirtyItems(allDirtyItems);
|
||||
}
|
||||
|
||||
performSyncWithProvider(provider, callback) {
|
||||
this.__performSyncWithProvider(provider, {}, callback);
|
||||
}
|
||||
|
||||
__performSyncWithProvider(provider, options, callback) {
|
||||
if(provider.syncOpInProgress) {
|
||||
provider.repeatOnCompletion = true;
|
||||
console.log("Sync op in progress for provider; returning.", provider);
|
||||
return;
|
||||
}
|
||||
|
||||
var isContinuationSync = provider.needsMoreSync;
|
||||
|
||||
provider.repeatOnCompletion = false;
|
||||
provider.syncOpInProgress = true;
|
||||
|
||||
let submitLimit = 100;
|
||||
var allItems = provider.pendingItems;
|
||||
var subItems = allItems.slice(0, submitLimit);
|
||||
if(subItems.length < allItems.length) {
|
||||
// more items left to be synced, repeat
|
||||
provider.needsMoreSync = true;
|
||||
} else {
|
||||
provider.needsMoreSync = false;
|
||||
}
|
||||
|
||||
if(!isContinuationSync) {
|
||||
provider.syncStatus.total = allItems.length;
|
||||
provider.syncStatus.current = 0;
|
||||
}
|
||||
|
||||
// Remove dirty items now. If this operation fails, we'll re-add them.
|
||||
// This allows us to queue changes on the same item
|
||||
provider.removePendingItems(subItems);
|
||||
|
||||
var request = this.Restangular.oneUrl(provider.url, provider.url);
|
||||
request.limit = 150;
|
||||
request.items = _.map(subItems, function(item){
|
||||
var key = this.keyManager.keyForName(provider.keyName);
|
||||
var itemParams = new ItemParams(item, key);
|
||||
itemParams.additionalFields = options.additionalFields;
|
||||
return itemParams.paramsForSync();
|
||||
}.bind(this));
|
||||
|
||||
request.sync_token = provider.syncToken;
|
||||
request.cursor_token = provider.cursorToken;
|
||||
console.log("Syncing with provider:", provider, "items:", subItems.length, "token", request.sync_token);
|
||||
|
||||
var headers = provider.jwt ? {Authorization: "Bearer " + provider.jwt} : {};
|
||||
request.post("", undefined, undefined, headers).then(function(response) {
|
||||
provider.error = null;
|
||||
|
||||
console.log("Completed sync for provider:", provider.url, "Response:", response.plain());
|
||||
|
||||
provider.syncToken = response.sync_token;
|
||||
|
||||
if(provider.primary) {
|
||||
this.rootScope.$broadcast("sync:updated_token", provider.syncToken);
|
||||
|
||||
// handle cursor token (more results waiting, perform another sync)
|
||||
provider.cursorToken = response.cursor_token;
|
||||
|
||||
var retrieved = this.handleItemsResponse(response.retrieved_items, null, provider);
|
||||
// merge only metadata for saved items
|
||||
var omitFields = ["content", "auth_hash"];
|
||||
var saved = this.handleItemsResponse(response.saved_items, omitFields, provider);
|
||||
|
||||
this.handleUnsavedItemsResponse(response.unsaved, provider)
|
||||
|
||||
this.writeItemsToLocalStorage(saved, false, null);
|
||||
this.writeItemsToLocalStorage(retrieved, false, null);
|
||||
}
|
||||
|
||||
provider.syncOpInProgress = false;
|
||||
provider.syncStatus.current += subItems.length;
|
||||
|
||||
if(provider.cursorToken || provider.repeatOnCompletion || provider.needsMoreSync) {
|
||||
this.__performSyncWithProvider(provider, options, callback);
|
||||
} else {
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
}
|
||||
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
console.log("Sync error: ", response);
|
||||
var error = response.data ? response.data.error : {message: "Could not connect to server."};
|
||||
|
||||
// Re-add subItems since this operation failed. We'll have to try again.
|
||||
provider.addPendingItems(subItems);
|
||||
provider.syncOpInProgress = false;
|
||||
provider.error = error;
|
||||
|
||||
if(provider.primary) {
|
||||
this.writeItemsToLocalStorage(allItems, false, null);
|
||||
}
|
||||
|
||||
this.rootScope.$broadcast("sync:error", error);
|
||||
|
||||
if(callback) {
|
||||
callback({error: "Sync error"});
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
handleUnsavedItemsResponse(unsaved, provider) {
|
||||
if(unsaved.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Handle unsaved", unsaved);
|
||||
for(var mapping of unsaved) {
|
||||
var itemResponse = mapping.item;
|
||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||
var error = mapping.error;
|
||||
if(error.tag == "uuid_conflict") {
|
||||
item.alternateUUID();
|
||||
item.setDirty(true);
|
||||
item.markAllReferencesDirty();
|
||||
}
|
||||
}
|
||||
|
||||
this.__performSyncWithProvider(provider, {additionalFields: ["created_at", "updated_at"]}, null);
|
||||
}
|
||||
|
||||
handleItemsResponse(responseItems, omitFields, syncProvider) {
|
||||
var ek = syncProvider ? this.keyManager.keyForName(syncProvider.keyName) : null;
|
||||
EncryptionHelper.decryptMultipleItems(responseItems, ek);
|
||||
return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('syncRunner', SyncRunner);
|
||||
189
app/assets/javascripts/app/services/syncManager.js
Normal file
189
app/assets/javascripts/app/services/syncManager.js
Normal file
@@ -0,0 +1,189 @@
|
||||
class SyncManager {
|
||||
|
||||
constructor($rootScope, modelManager, authManager, dbManager, Restangular) {
|
||||
this.$rootScope = $rootScope;
|
||||
this.modelManager = modelManager;
|
||||
this.authManager = authManager;
|
||||
this.Restangular = Restangular;
|
||||
this.dbManager = dbManager;
|
||||
this.syncStatus = {};
|
||||
}
|
||||
|
||||
get serverURL() {
|
||||
return localStorage.getItem("server") || "http://localhost:3000";
|
||||
}
|
||||
|
||||
writeItemsToLocalStorage(items, offlineOnly, callback) {
|
||||
var params = items.map(function(item) {
|
||||
var itemParams = new ItemParams(item, null);
|
||||
itemParams = itemParams.paramsForLocalStorage();
|
||||
if(offlineOnly) {
|
||||
delete itemParams.dirty;
|
||||
}
|
||||
return itemParams;
|
||||
}.bind(this));
|
||||
|
||||
this.dbManager.saveItems(params, callback);
|
||||
}
|
||||
|
||||
loadLocalItems(callback) {
|
||||
var params = this.dbManager.getAllItems(function(items){
|
||||
var items = this.handleItemsResponse(items, null, null);
|
||||
Item.sortItemsByDate(items);
|
||||
callback(items);
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
syncOffline(items, callback) {
|
||||
this.writeItemsToLocalStorage(items, true, function(responseItems){
|
||||
// delete anything needing to be deleted
|
||||
for(var item of items) {
|
||||
if(item.deleted) {
|
||||
this.modelManager.removeItemLocally(item);
|
||||
}
|
||||
}
|
||||
}.bind(this))
|
||||
|
||||
if(callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
get syncURL() {
|
||||
return this.serverURL + "/items/sync";
|
||||
}
|
||||
|
||||
sync(callback, options = {}) {
|
||||
|
||||
if(this.syncOpInProgress) {
|
||||
this.repeatOnCompletion = true;
|
||||
console.log("Sync op in progress; returning.");
|
||||
return;
|
||||
}
|
||||
|
||||
var allDirtyItems = this.modelManager.getDirtyItems();
|
||||
|
||||
// we want to write all dirty items to disk only if the user is offline, or if the sync op fails
|
||||
// if the sync op succeeds, these items will be written to disk by handling the "saved_items" response from the server
|
||||
if(this.authManager.offline()) {
|
||||
this.syncOffline(allDirtyItems, callback);
|
||||
this.modelManager.clearDirtyItems(allDirtyItems);
|
||||
return;
|
||||
}
|
||||
|
||||
var isContinuationSync = this.needsMoreSync;
|
||||
|
||||
this.repeatOnCompletion = false;
|
||||
this.syncOpInProgress = true;
|
||||
|
||||
let submitLimit = 100;
|
||||
var subItems = allDirtyItems.slice(0, submitLimit);
|
||||
if(subItems.length < allDirtyItems.length) {
|
||||
// more items left to be synced, repeat
|
||||
this.needsMoreSync = true;
|
||||
} else {
|
||||
this.needsMoreSync = false;
|
||||
}
|
||||
|
||||
if(!isContinuationSync) {
|
||||
this.syncStatus.total = allDirtyItems.length;
|
||||
this.syncStatus.current = 0;
|
||||
}
|
||||
|
||||
var request = this.Restangular.oneUrl(this.syncURL, this.syncURL);
|
||||
request.limit = 150;
|
||||
request.items = _.map(subItems, function(item){
|
||||
var itemParams = new ItemParams(item, localStorage.getItem("mk"));
|
||||
itemParams.additionalFields = options.additionalFields;
|
||||
return itemParams.paramsForSync();
|
||||
}.bind(this));
|
||||
|
||||
request.sync_token = this.syncToken;
|
||||
request.cursor_token = this.cursorToken;
|
||||
|
||||
request.post().then(function(response) {
|
||||
this.modelManager.clearDirtyItems(subItems);
|
||||
this.error = null;
|
||||
this.syncToken = response.sync_token;
|
||||
this.cursorToken = response.cursor_token;
|
||||
|
||||
this.$rootScope.$broadcast("sync:updated_token", this.syncToken);
|
||||
|
||||
var retrieved = this.handleItemsResponse(response.retrieved_items, null);
|
||||
// merge only metadata for saved items
|
||||
var omitFields = ["content", "auth_hash"];
|
||||
var saved = this.handleItemsResponse(response.saved_items, omitFields);
|
||||
|
||||
this.handleUnsavedItemsResponse(response.unsaved)
|
||||
|
||||
this.writeItemsToLocalStorage(saved, false, null);
|
||||
this.writeItemsToLocalStorage(retrieved, false, null);
|
||||
|
||||
this.syncOpInProgress = false;
|
||||
this.syncStatus.current += subItems.length;
|
||||
|
||||
if(this.cursorToken || this.repeatOnCompletion || this.needsMoreSync) {
|
||||
this.sync(callback, options);
|
||||
} else {
|
||||
if(callback) {
|
||||
callback(response);
|
||||
}
|
||||
}
|
||||
|
||||
}.bind(this))
|
||||
.catch(function(response){
|
||||
console.log("Sync error: ", response);
|
||||
var error = response.data ? response.data.error : {message: "Could not connect to server."};
|
||||
|
||||
this.syncOpInProgress = false;
|
||||
this.error = error;
|
||||
this.writeItemsToLocalStorage(allDirtyItems, false, null);
|
||||
|
||||
this.$rootScope.$broadcast("sync:error", error);
|
||||
|
||||
if(callback) {
|
||||
callback({error: "Sync error"});
|
||||
}
|
||||
}.bind(this))
|
||||
}
|
||||
|
||||
handleUnsavedItemsResponse(unsaved) {
|
||||
if(unsaved.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Handle unsaved", unsaved);
|
||||
for(var mapping of unsaved) {
|
||||
var itemResponse = mapping.item;
|
||||
var item = this.modelManager.findItem(itemResponse.uuid);
|
||||
var error = mapping.error;
|
||||
if(error.tag == "uuid_conflict") {
|
||||
item.alternateUUID();
|
||||
item.setDirty(true);
|
||||
item.markAllReferencesDirty();
|
||||
}
|
||||
}
|
||||
|
||||
this.sync(null, {additionalFields: ["created_at", "updated_at"]});
|
||||
}
|
||||
|
||||
handleItemsResponse(responseItems, omitFields) {
|
||||
EncryptionHelper.decryptMultipleItems(responseItems, localStorage.getItem("mk"));
|
||||
return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
|
||||
}
|
||||
|
||||
clearSyncToken() {
|
||||
localStorage.removeItem("syncToken");
|
||||
}
|
||||
|
||||
destroyLocalData(callback) {
|
||||
this.dbManager.clearAllItems(function(){
|
||||
localStorage.clear();
|
||||
if(callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
angular.module('app.frontend').service('syncManager', SyncManager);
|
||||
@@ -239,10 +239,6 @@
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.account-panel {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.import-password {
|
||||
margin-top: 14px;
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
.panel.panel-default.account-panel.panel-right.account-data-menu
|
||||
.panel-body
|
||||
|
||||
%section.gray-bg.medium-padding{"ng-init" => "showSN = true"}
|
||||
%account-new-account-section
|
||||
|
||||
%section.gray-bg.medium-padding
|
||||
%account-sync-section
|
||||
|
||||
%section.gray-bg.medium-padding
|
||||
%account-export-section
|
||||
|
||||
%section.gray-bg.medium-padding
|
||||
%account-keys-section
|
||||
|
||||
%h4
|
||||
%a{"ng-click" => "destroyLocalData()"} Destroy all local data
|
||||
@@ -0,0 +1,61 @@
|
||||
.panel.panel-default.panel-right.account-data-menu
|
||||
.panel-body
|
||||
|
||||
-# If not user
|
||||
%div{"ng-if" => "!user"}
|
||||
%p Enter your <a href="https://standardnotes.org" target="_blank">Standard File</a> account information. You can also register for free using the default server address.
|
||||
.small-v-space
|
||||
|
||||
%form.account-form.mt-5{'name' => "loginForm"}
|
||||
%input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'formData.email'}
|
||||
%input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'}
|
||||
|
||||
%div{"ng-if" => "!formData.status"}
|
||||
%button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span Sign In
|
||||
%button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span Register
|
||||
%br
|
||||
.block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"}
|
||||
%a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten.
|
||||
|
||||
%em.block.center-align.mt-10{"ng-if" => "formData.status", "style" => "font-size: 14px;"} {{formData.status}}
|
||||
|
||||
%div{"ng-if" => "showResetForm"}
|
||||
%p{"style" => "font-size: 13px; text-align: center;"}
|
||||
Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password.
|
||||
For this reason, Standard Notes cannot offer a password reset option. You <strong>must</strong> make sure to store or remember your password.
|
||||
-# End if not user
|
||||
|
||||
-# If user
|
||||
%div{"ng-if" => "user"}
|
||||
%label Local Encryption
|
||||
%p Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes.
|
||||
%label Status:
|
||||
{{encryptionStatusForNotes()}}
|
||||
-# End if user
|
||||
|
||||
.mt-5{"ng-if" => "user"}
|
||||
%label{"ng-if" => "user"}
|
||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "true", "ng-change" => "archiveFormData.encrypted = true"}
|
||||
Encrypted
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encrypted", "ng-value" => "false", "ng-change" => "archiveFormData.encrypted = false"}
|
||||
Decrypted
|
||||
%a{"ng-click" => "downloadDataArchive()"} Download Data Archive
|
||||
|
||||
%div{"ng-if" => "!importData.loading"}
|
||||
%label#import-archive
|
||||
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
|
||||
%a.disabled
|
||||
%span
|
||||
Import Data from Archive
|
||||
%div{"ng-if" => "importData.requestPassword"}
|
||||
Enter the account password associated with the import file.
|
||||
%input{"type" => "text", "ng-model" => "importData.password"}
|
||||
%button{"ng-click" => "submitImportPassword()"} Decrypt & Import
|
||||
|
||||
.spinner{"ng-if" => "importData.loading"}
|
||||
|
||||
%a{"ng-click" => "destroyLocalData()"} Destroy all local data
|
||||
@@ -1,31 +0,0 @@
|
||||
%h3{"ng-click" => "showSection = !showSection"}
|
||||
%a Import or export data
|
||||
|
||||
%div{"ng-if" => "showSection"}
|
||||
.options{"style" => "font-size: 12px; margin-top: 4px;"}
|
||||
%label{"ng-if" => "user"}
|
||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'mk'", "ng-change" => "archiveFormData.encryption_type = 'mk'"}
|
||||
Encrypted with Standard File key
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'ek'", "ng-change" => "archiveFormData.encryption_type = 'ek'"}
|
||||
{{user ? 'Encrypted with custom key' : 'Encrypted' }}
|
||||
%div{"ng-if" => "!user || (user && archiveFormData.encryption_type == 'ek')"}
|
||||
%input{"ng-model" => "archiveFormData.ek", "placeholder" => "Encryption key"}
|
||||
%label
|
||||
%input{"type" => "radio", "ng-model" => "archiveFormData.encryption_type", "ng-value" => "'none'", "ng-change" => "archiveFormData.encryption_type = 'none'"}
|
||||
Decrypted
|
||||
|
||||
%a{"ng-click" => "downloadDataArchive()"} Download Data Archive
|
||||
|
||||
%div{"ng-if" => "!importData.loading"}
|
||||
%label#import-archive
|
||||
%input{"type" => "file", "style" => "display: none;", "file-change" => "->", "handler" => "importFileSelected(files)"}
|
||||
%a.disabled
|
||||
%span
|
||||
Import Data from Archive
|
||||
%div{"ng-if" => "importData.requestPassword"}
|
||||
Enter the account password associated with the import file.
|
||||
%input{"type" => "text", "ng-model" => "importData.password"}
|
||||
%button{"ng-click" => "submitImportPassword()"} Decrypt & Import
|
||||
|
||||
.spinner{"ng-if" => "importData.loading"}
|
||||
@@ -1,18 +0,0 @@
|
||||
%h3{"ng-click" => "showSection = !showSection"}
|
||||
%a Manage keys
|
||||
|
||||
%div{"ng-if" => "showSection"}
|
||||
%h4 Encryption Keys
|
||||
|
||||
%div{"ng-if" => "showSection"}
|
||||
%p Keys are used to encrypt and decrypt your data.
|
||||
.mt-10
|
||||
%section.white-bg{"ng-repeat" => "key in keys track by key.name"}
|
||||
%label {{key.name}}
|
||||
%p.wrap {{key.key}}
|
||||
|
||||
%a.block.mt-10{"ng-click" => "newKeyData.showForm = !newKeyData.showForm"} Add New Key
|
||||
%form{"ng-if" => "newKeyData.showForm"}
|
||||
%input{"ng-model" => "newKeyData.name", "placeholder" => "Name your key"}
|
||||
%input{"ng-model" => "newKeyData.key", "placeholder" => "Key"}
|
||||
%button.light{"ng-click" => "submitNewKeyForm()"} Add Key
|
||||
@@ -1,35 +0,0 @@
|
||||
%h3{"ng-click" => "showForm = !showForm"}
|
||||
%a Add a sync account
|
||||
%div{"ng-if" => "showForm"}
|
||||
%p Enter your <a href="https://standardnotes.org" target="_blank">Standard File</a> account information.
|
||||
.small-v-space
|
||||
|
||||
%form.account-form{'name' => "loginForm"}
|
||||
%input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'formData.url'}
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'formData.email'}
|
||||
%input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'formData.user_password'}
|
||||
|
||||
%div{"ng-if" => "!formData.status"}
|
||||
%button.btn.dark-button.half-button{"ng-click" => "loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span Sign In
|
||||
%button.btn.dark-button.half-button{"ng-click" => "submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
|
||||
%span Register
|
||||
%br
|
||||
.block{"style" => "margin-top: 10px; font-size: 14px; font-weight: bold; text-align: center;"}
|
||||
%a.btn{"ng-click" => "showResetForm = !showResetForm"} Passwords cannot be forgotten.
|
||||
|
||||
%div{"ng-if" => "!formData.status"}
|
||||
%label.center-align.block.faded — OR —
|
||||
%a.block.center-align.medium-text{"ng-if" => "!formData.showAddLinkForm", "ng-click" => "formData.showAddLinkForm = true"} Add sync using secret link
|
||||
%form{"ng-if" => "formData.showAddLinkForm"}
|
||||
%input.form-control{:autofocus => 'autofocus', :name => 'url', :placeholder => 'Secret URL', :required => true, :type => 'url', 'ng-model' => 'formData.secretUrl'}
|
||||
%button.btn.dark-button.btn-block{"ng-click" => "submitExternalSyncURL()"}
|
||||
Add Sync Account
|
||||
%a.block.center-align.mt-5{"ng-click" => "formData.showAddLinkForm = false"} Cancel
|
||||
|
||||
%em.block.center-align.mt-10{"ng-if" => "formData.status", "style" => "font-size: 14px;"} {{formData.status}}
|
||||
|
||||
%div{"ng-if" => "showResetForm"}
|
||||
%p{"style" => "font-size: 13px; text-align: center;"}
|
||||
Because notes are locally encrypted using a secret key derived from your password, there's no way to decrypt these notes if you forget your password.
|
||||
For this reason, Standard Notes cannot offer a password reset option. You <strong>must</strong> make sure to store or remember your password.
|
||||
@@ -1,30 +0,0 @@
|
||||
%h3{"ng-click" => "showSection = !showSection"}
|
||||
%a Your sync accounts ({{syncProviders.length}})
|
||||
|
||||
%div{"ng-if" => "showSection || syncManager.syncProviders.length > 0"}
|
||||
.small-v-space
|
||||
%section.white-bg.medium-padding{"ng-repeat" => "provider in syncProviders"}
|
||||
%label {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Main' : 'Secondary')}}
|
||||
%em{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}}
|
||||
%p {{provider.url}}
|
||||
%section.inline-h
|
||||
%div{"ng-if" => "!provider.keyName || provider.showKeyForm"}
|
||||
%p
|
||||
%strong Choose encryption key:
|
||||
%select{"ng-model" => "provider.formData.keyName"}
|
||||
%option{"ng-repeat" => "key in keys", "ng-selected" => "{{key.name == provider.formData.keyName}}", "value" => "{{key.name}}"}
|
||||
{{key.name}}
|
||||
%button{"ng-click" => "saveKey(provider)"} Set
|
||||
|
||||
%button.light{"ng-if" => "!provider.enabled || !provider.primary", "ng-click" => "enableSyncProvider(provider, true)"} Set as Main
|
||||
%button.light{"ng-if" => "syncProviders.length > 1 && !provider.secondary && (!provider.primary || !provider.enabled)", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary
|
||||
|
||||
%button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key
|
||||
%button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Account
|
||||
|
||||
.mt-15{"ng-if" => "provider.error"}
|
||||
%strong.red Error syncing: {{provider.error.message}}
|
||||
.mt-15{"style" => "height: 15px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"}
|
||||
.spinner{"style" => "float: left; margin-top: 3px; margin-left: 2px;"}
|
||||
%strong{"style" => "float: left; margin-left: 7px;"} Syncing:
|
||||
{{provider.syncStatus.statusString}}
|
||||
@@ -2,7 +2,7 @@
|
||||
.pull-left
|
||||
.footer-bar-link
|
||||
%a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
|
||||
%account-data-menu{"ng-if" => "ctrl.showAccountMenu"}
|
||||
%account-menu{"ng-if" => "ctrl.showAccountMenu"}
|
||||
|
||||
.footer-bar-link
|
||||
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions
|
||||
|
||||
Reference in New Issue
Block a user