sync provider wip

This commit is contained in:
Mo Bitar
2017-01-25 19:44:48 -06:00
parent 51012d7d54
commit b2200c5707
17 changed files with 173 additions and 145 deletions

View File

@@ -20,7 +20,7 @@ angular.module('app.frontend')
* Insert 4 spaces when a tab key is pressed,
* only used when inside of the text editor.
* If the shift key is pressed first, this event is
* not fired.
* not fired.
*/
var handleTab = function (event) {
if (!event.shiftKey && event.which == 9) {
@@ -146,8 +146,6 @@ angular.module('app.frontend')
note.dummy = false;
this.save()(note, function(success){
if(success) {
apiController.clearDraft();
if(statusTimeout) $timeout.cancel(statusTimeout);
statusTimeout = $timeout(function(){
this.noteStatus = "All changes saved"
@@ -171,10 +169,6 @@ angular.module('app.frontend')
this.changesMade = function() {
this.note.hasChanges = true;
this.note.dummy = false;
if(apiController.isUserSignedIn()) {
// signed out users have local autosave, dont need draft saving
apiController.saveDraftToDisk(this.note);
}
if(saveTimeout) $timeout.cancel(saveTimeout);
if(statusTimeout) $timeout.cancel(statusTimeout);
@@ -236,7 +230,6 @@ angular.module('app.frontend')
}
this.deleteNote = function() {
apiController.clearDraft();
this.remove()(this.note);
this.showMenu = false;
}

View File

@@ -21,7 +21,7 @@ angular.module('app.frontend')
this.user = apiController.user;
this.accountMenuPressed = function() {
this.serverData = {url: apiController.getServer()};
this.serverData = {url: syncManager.serverURL()};
this.showAccountMenu = !this.showAccountMenu;
this.showFaq = false;
this.showNewPasswordForm = false;

View File

@@ -7,9 +7,9 @@ angular.module('app.frontend')
syncManager.sync(null);
// refresh every 30s
setInterval(function () {
syncManager.sync(null);
}, 30000);
// setInterval(function () {
// syncManager.sync(null);
// }, 30000);
});
$scope.allTag = new Tag({all: true});

View File

@@ -48,14 +48,8 @@ angular.module('app.frontend')
if(isFirstLoad) {
$timeout(function(){
var draft = apiController.getDraft();
if(draft) {
var note = draft;
this.selectNote(note);
} else {
this.createNewNote();
isFirstLoad = false;
}
this.createNewNote();
isFirstLoad = false;
}.bind(this))
} else if(tag.notes.length == 0) {
this.createNewNote();

View File

@@ -7,7 +7,7 @@ class ItemParams {
}
paramsForExportFile() {
this.additionalFields = ["created_at", "updated_at"];
this.additionalFields = ["updated_at"];
this.forExportFile = true;
return _.omit(this.__params(), ["deleted"]);
}
@@ -16,16 +16,22 @@ class ItemParams {
return this.paramsForExportFile();
}
paramsForLocalStorage() {
this.additionalFields = ["updated_at", "dirty"];
this.forExportFile = true;
return this.__params();
}
paramsForSync() {
return __params(null, false);
return this.__params(null, false);
}
__params() {
var itemCopy = _.cloneDeep(this.item);
console.assert(!item.dummy, "Item is dummy, should not have gotten here.", item.dummy)
console.assert(!this.item.dummy, "Item is dummy, should not have gotten here.", this.item.dummy)
var params = {uuid: item.uuid, content_type: item.content_type, deleted: item.deleted};
var params = {uuid: this.item.uuid, content_type: this.item.content_type, deleted: this.item.deleted, created_at: this.item.created_at};
if(this.ek) {
this.encryptionHelper.encryptItem(itemCopy, this.ek);
@@ -42,7 +48,7 @@ class ItemParams {
}
if(this.additionalFields) {
_.merge(params, _.pick(item, this.additionalFields));
_.merge(params, _.pick(this.item, this.additionalFields));
}
return params;

View File

@@ -28,18 +28,10 @@ angular.module('app.frontend')
Auth
*/
this.defaultServerURL = function() {
return localStorage.getItem("server") || "https://n3.standardnotes.org";
}
this.getAuthParams = function() {
return JSON.parse(localStorage.getItem("auth_params"));
}
this.isUserSignedIn = function() {
return localStorage.getItem("jwt");
}
this.getAuthParamsForEmail = function(url, email, callback) {
var requestUrl = url + "/auth/params";
var request = Restangular.oneUrl(requestUrl, requestUrl);
@@ -103,7 +95,7 @@ angular.module('app.frontend')
localStorage.setItem("jwt", response.token);
localStorage.setItem("user", JSON.stringify(response.user));
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
syncManager.addKey(syncManager.SNKeyName, mk);
keyManager.addKey(SNKeyName, mk);
syncManager.addStandardFileSyncProvider(url);
}
@@ -119,6 +111,7 @@ angular.module('app.frontend')
callback(response);
}.bind(this))
.catch(function(response){
console.log("Registration error", response);
callback(response.data);
})
}.bind(this));
@@ -251,7 +244,7 @@ angular.module('app.frontend')
items: items
}
if(ek.name == syncManager.SNKeyName) {
if(ek.name == SNKeyName) {
// auth params are only needed when encrypted with a standard file key
data["auth_params"] = this.getAuthParams();
}
@@ -263,34 +256,27 @@ angular.module('app.frontend')
return JSON.parse(JSON.stringify(object));
}
/*
Drafts
*/
this.saveDraftToDisk = function(draft) {
localStorage.setItem("draft", JSON.stringify(draft));
}
this.clearDraft = function() {
localStorage.removeItem("draft");
}
this.getDraft = function() {
var draftString = localStorage.getItem("draft");
if(!draftString || draftString == 'undefined') {
return null;
}
var jsonObj = _.merge({content_type: "Note"}, JSON.parse(draftString));
return modelManager.createItem(jsonObj);
}
this.signoutOfStandardFile = function(callback) {
this.signoutOfStandardFile = function(destroyAll, callback) {
syncManager.removeStandardFileSyncProvider();
if(destroyAll) {
this.destroyLocalData(callback);
} else {
localStorage.removeItem("user");
localStorage.removeItem("jwt");
localStorage.removeItem("server");
localStorage.removeItem("auth_params");
callback();
}
}
this.destroyLocalData = function(callback) {
dbManager.clearAllItems(function(){
localStorage.clear();
callback();
if(callback) {
callback();
}
});
}
}
});

View File

@@ -10,6 +10,15 @@ class AccountDataMenu {
controller($scope, apiController, modelManager) {
'ngInject';
$scope.destroyLocalData = function() {
if(!confirm("Are you sure you want to end your session? This will delete all local items, sync providers, keys, and extensions.")) {
return;
}
apiController.destroyLocalData(function(){
window.location.reload();
})
}
}
}

View File

@@ -31,10 +31,22 @@ class AccountSyncSection {
}
$scope.removeSyncProvider = function(provider) {
syncManager.removeSyncProvider(provider);
if(provider.isStandardNotesAccount) {
alert("To remove your Standard Notes sync, sign out of your Standard Notes account.")
return;
}
if(confirm("Are you sure you want to remove this sync provider?")) {
syncManager.removeSyncProvider(provider);
}
}
$scope.changeEncryptionKey = function(provider) {
if(provider.isStandardNotesAccount) {
alert("To change your encryption key for your Standard Notes account, you need to change your password. However, this functionality is not currently supported.");
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;
}
@@ -46,6 +58,7 @@ class AccountSyncSection {
$scope.saveKey = function(provider) {
provider.showKeyForm = false;
provider.keyName = provider.formData.keyName;
syncManager.didMakeChangesToSyncProviders();
if(provider.enabled) {
syncManager.addAllDataAsNeedingSyncForProvider(provider);

View File

@@ -7,10 +7,10 @@ class AccountVendorAccountSection {
};
}
controller($scope, apiController, modelManager, $timeout, dbManager) {
controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) {
'ngInject';
$scope.loginData = {mergeLocal: true, url: apiController.defaultServerURL()};
$scope.loginData = {mergeLocal: true, url: syncManager.serverURL()};
$scope.user = apiController.user;
$scope.changePasswordPressed = function() {
@@ -19,7 +19,7 @@ class AccountVendorAccountSection {
$scope.signOutPressed = function() {
$scope.showAccountMenu = false;
apiController.signoutOfStandardFile(function(){
apiController.signoutOfStandardFile(false, function(){
window.location.reload();
})
}

View File

@@ -14,7 +14,7 @@ class ImportExportMenu {
$scope.user = apiController.user;
$scope.downloadDataArchive = function() {
if(!apiController.isUserSignedIn() && $scope.archiveFormData.encryption_type == 'ek') {
if($scope.archiveFormData.encryption_type == 'ek') {
if(!$scope.archiveFormData.ek) {
alert("You must set an encryption key to export the data encrypted.")
return;

View File

@@ -1,26 +1,38 @@
export const SNKeyName = "Standard Notes Key";
class SyncManager {
constructor(modelManager) {
constructor(modelManager, syncRunner) {
this.modelManager = modelManager;
this.syncPerformer = new SyncPerformer()
this.SNKeyName = "Standard Notes Key";
this.syncRunner = syncRunner;
this.syncRunner.setOnChangeProviderCallback(function(){
this.didMakeChangesToSyncProviders();
}.bind(this))
this.loadSyncProviders();
}
get offline() {
return this.syncProviders.length == 0;
return this.enabledProviders.length == 0;
}
serverURL() {
return localStorage.getItem("server") || "https://n3.standardnotes.org";
}
get enabledProviders() {
return this.syncProviders.filter(function(provider){return provider.enabled == true});
}
sync(callback) {
this.syncPerformer.sync(this.syncProviders, callback);
this.syncRunner.sync(this.enabledProviders, callback);
}
syncWithProvider(provider, callback) {
this.syncPerformer.performSyncWithProvider(provider, callback);
this.syncRunner.performSyncWithProvider(provider, callback);
}
loadLocalItems(callback) {
this.syncPerformer.loadLocalItems(callback);
this.syncRunner.loadLocalItems(callback);
}
syncProviderForURL(url) {
@@ -46,16 +58,17 @@ class SyncManager {
}
removeStandardFileSyncProvider() {
var sfProvider = _.find(this.syncProviders, {url: this.defaultServerURL() + "/items/sync"})
var sfProvider = _.find(this.syncProviders, {url: this.serverURL() + "/items/sync"})
_.pull(this.syncProviders, sfProvider);
this.didMakeChangesToSyncProviders();
}
addStandardFileSyncProvider(url) {
var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.syncProviders.length == 0});
defaultProvider.keyName = this.SNKeyName;
var defaultProvider = new SyncProvider({url: url + "/items/sync", primary: this.enabledProviders.length == 0});
defaultProvider.keyName = SNKeyName;
defaultProvider.enabled = this.syncProviders.length == 0;
this.syncProviders.push(defaultProvider);
this.didMakeChangesToSyncProviders();
return defaultProvider;
}
@@ -74,14 +87,15 @@ class SyncManager {
this.syncProviders.push(new SyncProvider(p));
}
} else {
// no providers saved, use default
if(this.isUserSignedIn()) {
var defaultProvider = this.addStandardFileSyncProvider(this.defaultServerURL());
// no providers saved, this means migrating from old system to new
// check if user is signed in
if(this.offline && localStorage.getItem("user")) {
var defaultProvider = this.addStandardFileSyncProvider(this.serverURL());
defaultProvider.syncToken = localStorage.getItem("syncToken");
// migrate old key structure to new
var mk = localStorage.getItem("mk");
if(mk) {
keyManager.addKey(this.SNKeyName, mk);
keyManager.addKey(SNKeyName, mk);
localStorage.removeItem("mk");
}
this.didMakeChangesToSyncProviders();
@@ -129,43 +143,3 @@ class SyncManager {
}
angular.module('app.frontend').service('syncManager', SyncManager);
class SyncProvider {
constructor(obj) {
this.encrypted = true;
_.merge(this, obj);
}
addPendingItems(items) {
if(!this.pendingItems) {
this.pendingItems = [];
}
this.pendingItems = this.pendingItems.concat(items);
}
removePendingItems(items) {
this.pendingItems = _.difference(this.pendingItems, items);
}
get status() {
if(!this.enabled) {
return null;
}
if(this.primary) return "primary";
else return "secondary";
}
asJSON() {
return {
enabled: this.enabled,
url: this.url,
primary: this.primary,
keyName: this.keyName,
syncToken: this.syncToken
}
}
}

View File

@@ -0,0 +1,43 @@
class SyncProvider {
constructor(obj) {
this.encrypted = true;
_.merge(this, obj);
}
addPendingItems(items) {
if(!this.pendingItems) {
this.pendingItems = [];
}
this.pendingItems = this.pendingItems.concat(items);
}
removePendingItems(items) {
this.pendingItems = _.difference(this.pendingItems, items);
}
get isStandardNotesAccount() {
return this.keyName == SNKeyName;
}
get status() {
if(!this.enabled) {
return null;
}
if(this.primary) return "primary";
else return "secondary";
}
asJSON() {
return {
enabled: this.enabled,
url: this.url,
primary: this.primary,
keyName: this.keyName,
syncToken: this.syncToken
}
}
}

View File

@@ -1,10 +1,12 @@
class SyncPerformer {
class SyncRunner {
constructor(modelManager, dbManager, encryptionHelper, keyManager) {
constructor($rootScope, modelManager, dbManager, encryptionHelper, keyManager, Restangular) {
this.rootScope = $rootScope;
this.modelManager = modelManager;
this.dbManager = dbManager;
this.encryptionHelper = encryptionHelper;
this.keyManager = keyManager;
this.Restangular = Restangular;
}
setOnChangeProviderCallback(callback) {
@@ -15,9 +17,14 @@ class SyncPerformer {
this.onChangeProviderCallback(provider);
}
writeItemsToLocalStorage(items, callback) {
writeItemsToLocalStorage(items, offlineOnly, callback) {
var params = items.map(function(item) {
return this.paramsForItem(item, false, ["created_at", "updated_at", "dirty"], true)
var itemParams = new ItemParams(item, null);
itemParams = itemParams.paramsForLocalStorage();
if(offlineOnly) {
delete itemParams.dirty;
}
return itemParams;
}.bind(this));
this.dbManager.saveItems(params, callback);
@@ -32,7 +39,8 @@ class SyncPerformer {
}
syncOffline(items, callback) {
this.writeItemsToLocalStorage(items, function(responseItems){
console.log("Writing items offline", items);
this.writeItemsToLocalStorage(items, true, function(responseItems){
// delete anything needing to be deleted
for(var item of items) {
if(item.deleted) {
@@ -57,10 +65,6 @@ class SyncPerformer {
}
for(let provider of providers) {
if(!provider.enabled) {
continue;
}
provider.addPendingItems(allDirtyItems);
this.didMakeChangesToSyncProvider(provider);
@@ -100,13 +104,13 @@ class SyncPerformer {
provider.repeatOnCompletion = false;
}
console.log("Syncing with provider", provider, subItems);
console.log("Syncing with provider:", provider.url, "items:", subItems.length);
// 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 = Restangular.oneUrl(provider.url, provider.url);
var request = this.Restangular.oneUrl(provider.url, provider.url);
request.limit = 150;
request.items = _.map(subItems, function(item){
var itemParams = new ItemParams(item, provider.ek);
@@ -119,12 +123,12 @@ class SyncPerformer {
request.post().then(function(response) {
console.log("Sync completion", response);
console.log("Completed sync for provider:", provider.url, "Response:", response);
provider.syncToken = response.sync_token;
if(provider.primary) {
$rootScope.$broadcast("sync:updated_token", provider.syncToken);
this.rootScope.$broadcast("sync:updated_token", provider.syncToken);
// handle cursor token (more results waiting, perform another sync)
provider.cursorToken = response.cursor_token;
@@ -136,8 +140,8 @@ class SyncPerformer {
this.handleUnsavedItemsResponse(response.unsaved, provider)
this.writeItemsToLocalStorage(saved, null);
this.writeItemsToLocalStorage(retrieved, null);
this.writeItemsToLocalStorage(saved, false, null);
this.writeItemsToLocalStorage(retrieved, false, null);
}
provider.syncOpInProgress = false;
@@ -159,7 +163,7 @@ class SyncPerformer {
provider.syncOpInProgress = false;
if(provider.primary) {
this.writeItemsToLocalStorage(allItems, null);
this.writeItemsToLocalStorage(allItems, false, null);
}
if(callback) {
@@ -194,3 +198,5 @@ class SyncPerformer {
return this.modelManager.mapResponseItemsToLocalModelsOmittingFields(responseItems, omitFields);
}
}
angular.module('app.frontend').service('syncRunner', SyncRunner);

View File

@@ -16,3 +16,5 @@
%section.account-item
%h3{"ng-click" => "showIO = !showIO"} Import/Export
%import-export-menu{"ng-if" => "showIO"}
%a{"ng-click" => "destroyLocalData()"} Destroy all local data

View File

@@ -1,18 +1,20 @@
.providers
.provider{"ng-repeat" => "provider in syncProviders"}
.type {{provider.primary == null ? 'Not enabled' : (provider.primary ? 'Primary' : 'Secondary')}}
.type {{!provider.enabled ? 'Not enabled' : (provider.primary ? 'Primary' : 'Secondary')}}
.key{"ng-if" => "provider.keyName"} Using key: {{provider.keyName}}
.url {{provider.url}}
.options
%div{"ng-if" => "!provider.enabled"}
%div{"ng-if" => "!provider.keyName || provider.showKeyForm"}
%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
%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
%div{"ng-if" => "!provider.enabled"}
%button.light{"ng-click" => "enableSyncProvider(provider, true)"} Enable as Primary sync provider
%button.light{"ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider
%button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Enable as Secondary sync provider
%button.light{"ng-if" => "provider.keyName", "ng-click" => "changeEncryptionKey(provider)"} Change Encryption Key
%button.light{"ng-click" => "removeSyncProvider(provider)"} Remove Provider

View File

@@ -4,7 +4,8 @@
.server {{serverURL}}
.links{"ng-if" => "user"}
.link-item
%a{"ng-click" => "signOutPressed()"} Sign Out
%a{"ng-click" => "signOutPressed()"} Sign out
%p Note: Signing out does not delete your local items, extensions, and keys.
.meta-container
.title Local Encryption
.desc Notes are encrypted locally before being sent to the server. Neither the server owner nor an intrusive entity can decrypt your locally encrypted notes.

View File

@@ -5,8 +5,7 @@
.items
.item.account
%a{"ng-click" => "ctrl.accountMenuPressed()"} Data
%account-data-menu
-# {"ng-if" => "ctrl.showAccountMenu"}
%account-data-menu{"ng-if" => "ctrl.showAccountMenu"}
.item
%a{"ng-click" => "ctrl.toggleExtensions()"} Extensions