sync accounts

This commit is contained in:
Mo Bitar
2017-01-26 19:50:47 -06:00
parent a255b8487e
commit b7492176d1
15 changed files with 167 additions and 114 deletions

View File

@@ -1,6 +1,7 @@
'use strict';
var Neeto = Neeto || {};
var SN = SN || {};
// detect IE8 and above, and edge.
// IE and Edge do not support pbkdf2 in WebCrypto, therefore we need to use CryptoJS
@@ -19,18 +20,4 @@ angular.module('app.frontend', [
.config(function (RestangularProvider, apiControllerProvider) {
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
};
});
})

View File

@@ -19,9 +19,9 @@ 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.
*/
* If the shift key is pressed first, this event is
* not fired.
*/
var handleTab = function (event) {
if (!event.shiftKey && event.which == 9) {
event.preventDefault();
@@ -29,13 +29,13 @@ angular.module('app.frontend')
var end = this.selectionEnd;
var spaces = " ";
// Insert 4 spaces
// Insert 4 spaces
this.value = this.value.substring(0, start)
+ spaces + this.value.substring(end);
// Place cursor 4 spaces away from where
// the tab key was pressed
this.selectionStart = this.selectionEnd = start + 4;
// Place cursor 4 spaces away from where
// the tab key was pressed
this.selectionStart = this.selectionEnd = start + 4;
}
}
@@ -88,7 +88,7 @@ angular.module('app.frontend')
}
}
})
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager) {
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope, extensionManager, syncManager) {
this.setNote = function(note, oldNote) {
this.editorMode = 'edit';
@@ -148,12 +148,18 @@ angular.module('app.frontend')
if(success) {
if(statusTimeout) $timeout.cancel(statusTimeout);
statusTimeout = $timeout(function(){
this.noteStatus = "All changes saved"
this.saveError = false;
var status = "All changes saved"
if(syncManager.offline) {
status += " (offline)";
}
this.noteStatus = status;
}.bind(this), 200)
} else {
if(statusTimeout) $timeout.cancel(statusTimeout);
statusTimeout = $timeout(function(){
this.noteStatus = "(Offline) — All changes saved"
this.saveError = true;
this.noteStatus = "Error saving"
}.bind(this), 200)
}
}.bind(this));

View File

@@ -12,6 +12,10 @@ angular.module('app.frontend')
link:function(scope, elem, attrs, ctrl) {
scope.$on("sync:updated_token", function(){
ctrl.syncUpdated();
ctrl.findErrors();
})
scope.$on("sync:error", function(){
ctrl.findErrors();
})
}
}
@@ -19,9 +23,16 @@ angular.module('app.frontend')
.controller('HeaderCtrl', function (apiController, modelManager, $timeout, dbManager, syncManager) {
this.user = apiController.user;
this.offline = syncManager.offline;
this.findErrors = function() {
this.error = syncManager.syncProviders.filter(function(provider){return provider.error}).length > 0 ? true : false;
}
this.findErrors();
this.accountMenuPressed = function() {
this.serverData = {url: syncManager.serverURL()};
this.serverData = {};
this.showAccountMenu = !this.showAccountMenu;
this.showFaq = false;
this.showNewPasswordForm = false;

View File

@@ -7,11 +7,11 @@ angular.module('app.frontend')
return domain;
}
this.$get = function($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) {
return new ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager);
this.$get = function($rootScope, Restangular, modelManager, dbManager, syncManager) {
return new ApiController($rootScope, Restangular, modelManager, dbManager, syncManager);
}
function ApiController($rootScope, Restangular, modelManager, dbManager, keyManager, syncManager) {
function ApiController($rootScope, Restangular, modelManager, dbManager, syncManager) {
var userData = localStorage.getItem("user");
if(userData) {
@@ -79,7 +79,7 @@ angular.module('app.frontend')
var params = {password: keys.pw, email: email};
_.merge(request, params);
request.post().then(function(response){
this.handleAuthResponse(response, url, authParams, mk);
this.handleAuthResponse(response, email, url, authParams, mk);
callback(response);
}.bind(this))
.catch(function(response){
@@ -90,13 +90,16 @@ angular.module('app.frontend')
}.bind(this))
}
this.handleAuthResponse = function(response, url, authParams, mk) {
localStorage.setItem("server", url);
localStorage.setItem("jwt", response.token);
localStorage.setItem("user", JSON.stringify(response.user));
localStorage.setItem("auth_params", JSON.stringify(_.omit(authParams, ["pw_nonce"])));
keyManager.addKey(SNKeyName, mk);
syncManager.addStandardFileSyncProvider(url);
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);
}
this.register = function(url, email, password, callback) {
@@ -107,7 +110,7 @@ angular.module('app.frontend')
var params = _.merge({password: keys.pw, email: email}, authParams);
_.merge(request, params);
request.post().then(function(response){
this.handleAuthResponse(response, url, authParams, mk);
this.handleAuthResponse(response, email, url, authParams, mk);
callback(response);
}.bind(this))
.catch(function(response){
@@ -256,19 +259,6 @@ angular.module('app.frontend')
return JSON.parse(JSON.stringify(object));
}
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();

View File

@@ -3,8 +3,7 @@ class AccountDataMenu {
constructor() {
this.restrict = "E";
this.templateUrl = "frontend/directives/account-data-menu.html";
this.scope = {
};
this.scope = {};
}
controller($scope, apiController, modelManager, keyManager) {
@@ -13,7 +12,7 @@ class AccountDataMenu {
$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 providers, keys, and extensions.")) {
if(!confirm("Are you sure you want to end your session? This will delete all local items, sync accounts, keys, and extensions.")) {
return;
}

View File

@@ -10,7 +10,7 @@ class AccountNewAccountSection {
controller($scope, apiController, modelManager, $timeout, dbManager, syncManager) {
'ngInject';
$scope.formData = {mergeLocal: true, url: syncManager.serverURL()};
$scope.formData = {mergeLocal: true, url: syncManager.defaultServerURL()};
$scope.user = apiController.user;
$scope.showForm = syncManager.syncProviders.length == 0;
@@ -20,15 +20,10 @@ class AccountNewAccountSection {
}
$scope.submitExternalSyncURL = function() {
syncManager.addSyncProviderFromURL($scope.newSyncData.url);
$scope.newSyncData.showAddSyncForm = false;
}
$scope.signOutPressed = function() {
$scope.showAccountMenu = false;
apiController.signoutOfStandardFile(false, function(){
window.location.reload();
})
syncManager.addSyncProviderFromURL($scope.formData.secretUrl);
$scope.formData.showAddLinkForm = false;
$scope.formData.secretUrl = null;
$scope.showForm = false;
}
$scope.submitPasswordChange = function() {

View File

@@ -16,7 +16,7 @@ class AccountSyncSection {
$scope.enableSyncProvider = function(provider, primary) {
if(!provider.keyName) {
alert("You must choose an encryption key for this provider before enabling it.");
alert("You must choose an encryption key for this account before enabling it.");
return;
}
@@ -24,19 +24,19 @@ class AccountSyncSection {
}
$scope.removeSyncProvider = function(provider) {
if(provider.isStandardNotesAccount) {
alert("To remove your Standard Notes sync, sign out of your Standard Notes account.")
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 provider?")) {
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 Notes account, you need to change your password. However, this functionality is not currently supported.");
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;
}

View File

@@ -16,7 +16,6 @@ angular
// Whenever the scope variable updates we simply
// show if it evaluates to 'true' and hide if 'false'
scope.$watch('show', function(newVal){
console.log("show value changed", newVal);
newVal ? showSpinner() : hideSpinner();
});
@@ -29,7 +28,6 @@ angular
}
function showElement(show) {
console.log("show:", show);
show ? elem.css({display:''}) : elem.css({display:'none'});
}

View File

@@ -2,8 +2,9 @@ export const SNKeyName = "Standard Notes Key";
class SyncManager {
constructor(modelManager, syncRunner) {
constructor(modelManager, syncRunner, keyManager) {
this.modelManager = modelManager;
this.keyManager = keyManager;
this.syncRunner = syncRunner;
this.syncRunner.setOnChangeProviderCallback(function(){
this.didMakeChangesToSyncProviders();
@@ -15,14 +16,19 @@ class SyncManager {
return this.enabledProviders.length == 0;
}
serverURL() {
return localStorage.getItem("server") || "https://n3.standardnotes.org";
defaultServerURL() {
return "https://n3.standardnotes.org";
}
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);
}
@@ -57,21 +63,6 @@ class SyncManager {
return _.find(this.syncProviders, {primary: true});
}
removeStandardFileSyncProvider() {
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.primarySyncProvider()});
defaultProvider.keyName = SNKeyName;
defaultProvider.enabled = this.syncProviders.length == 0;
this.syncProviders.push(defaultProvider);
this.didMakeChangesToSyncProviders();
return defaultProvider;
}
didMakeChangesToSyncProviders() {
localStorage.setItem("syncProviders", JSON.stringify(_.map(this.syncProviders, function(provider) {
return provider.asJSON()
@@ -89,22 +80,58 @@ class SyncManager {
} else {
// 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(SNKeyName, mk);
localStorage.removeItem("mk");
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.didMakeChangesToSyncProviders();
this.keyManager.addKey(provider.keyName, ek);
if(this.syncProviders.length == 0) {
this.enableSyncProvider(provider, true);
}
return provider;
}
addSyncProviderFromURL(url) {
var provider = new SyncProvider({url: url});
provider.type = SN.SyncProviderType.URL;
this.syncProviders.push(provider);
this.didMakeChangesToSyncProviders();
}
@@ -123,8 +150,6 @@ class SyncManager {
this.addAllDataAsNeedingSyncForProvider(syncProvider);
this.didMakeChangesToSyncProviders();
this.syncWithProvider(syncProvider);
this.syncWithProvider(syncProvider);
this.syncWithProvider(syncProvider);
}
addAllDataAsNeedingSyncForProvider(syncProvider) {

View File

@@ -1,3 +1,8 @@
SN.SyncProviderType = {
Account: 1,
URL: 2
}
class SyncProvider {
constructor(obj) {
@@ -31,13 +36,28 @@ class SyncProvider {
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
syncToken: this.syncToken,
// account based
email: this.email,
uuid: this.uuid,
jwt: this.jwt,
auth_params: this.auth_params
}
}
}

View File

@@ -39,7 +39,6 @@ class SyncRunner {
}
syncOffline(items, callback) {
console.log("Writing items offline", items);
this.writeItemsToLocalStorage(items, true, function(responseItems){
// delete anything needing to be deleted
for(var item of items) {
@@ -128,7 +127,9 @@ class SyncRunner {
request.sync_token = provider.syncToken;
request.cursor_token = provider.cursorToken;
request.post().then(function(response) {
var headers = provider.jwt ? {Authorization: "Bearer " + provider.jwt} : {};
request.post("", undefined, undefined, headers).then(function(response) {
provider.error = null;
if(!provider.primary) {
console.log("Completed sync for provider:", provider.url, "Response:", response);
@@ -170,15 +171,19 @@ class SyncRunner {
}.bind(this))
.catch(function(response){
console.log("Sync error: ", response);
var error = 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"});
}

View File

@@ -14,6 +14,10 @@
margin-top: 10px;
}
.mt-15 {
margin-top: 15px;
}
.faded {
opacity: 0.5;
}
@@ -74,11 +78,15 @@
section {
padding: 5px;
padding-bottom: 2px;
margin-top: 5px;
&.inline-h {
padding: 5px 0px;
padding-top: 5px;
padding-left: 0;
padding-right: 0;
}
}
input {
@@ -121,6 +129,10 @@
.large-padding {
padding: 15px;
}
.red {
color: red;
}
}
.footer-bar-link {

View File

@@ -23,7 +23,7 @@
%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.url'}
%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

View File

@@ -3,7 +3,7 @@
%div{"ng-if" => "showSection"}
.small-v-space
%section.white-bg{"ng-repeat" => "provider in syncProviders"}
%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}}
@@ -16,11 +16,15 @@
{{key.name}}
%button{"ng-click" => "saveKey(provider)"} Set
%div{"ng-if" => "!provider.enabled"}
%button.light{"ng-click" => "enableSyncProvider(provider, true)"} Set as Main
%button.light{"ng-if" => "syncProviders.length > 1", "ng-click" => "enableSyncProvider(provider, false)"} Add as Secondary
%button.light{"ng-if" => "!provider.enabled || !provider.primary", "ng-click" => "enableSyncProvider(provider, true)"} Set as Main
%button.light{"ng-if" => "syncProviders.length > 1 && (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 Provider
%div{"style" => "height: 30px;", "delay-hide" => "true", "show" => "provider.syncOpInProgress", "delay" => "1000"}
%strong{"style" => "float: left;"} Syncing: {{provider.syncStatus.statusString}}
.spinner{"style" => "float: right"}
%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}}

View File

@@ -1,7 +1,7 @@
.footer-bar
.pull-left
.footer-bar-link
%a{"ng-click" => "ctrl.accountMenuPressed()"} Account
%a{"ng-click" => "ctrl.accountMenuPressed()", "ng-class" => "{red: ctrl.error}"} Account
%account-data-menu{"ng-if" => "ctrl.showAccountMenu"}
.footer-bar-link
@@ -14,11 +14,12 @@
.pull-right
.footer-bar-link
.footer-bar-link{"style" => "margin-right: 5px;"}
%div{"ng-if" => "ctrl.lastSyncDate", "style" => "float: left; font-weight: normal; margin-right: 8px;"}
%span{"ng-if" => "!ctrl.isRefreshing"}
Last refreshed {{ctrl.lastSyncDate | appDateTime}}
%span{"ng-if" => "ctrl.isRefreshing"}
.spinner{"style" => "margin-top: 2px;"}
%a{"ng-click" => "ctrl.refreshData()"} Refresh
%strong{"ng-if" => "ctrl.offline"} Offline
%a{"ng-if" => "!ctrl.offline", "ng-click" => "ctrl.refreshData()"} Refresh