import/export

This commit is contained in:
Mo Bitar 2016-12-11 23:43:06 -06:00
parent 358fd89989
commit 8ad3776819
13 changed files with 219 additions and 180 deletions

View File

@ -17,4 +17,12 @@ angular.module('app.frontend')
apiController.setGk(new_keys.gk);
}
// var note = new Note();
// note.content = {title: "hello", text: "world"};
// console.log("note content", note.content);
// console.log("note title", note.title);
// console.log("note json", JSON.stringify(note));
//
// console.log("Copy", _.cloneDeep(note));
});

View File

@ -102,7 +102,7 @@ angular.module('app.frontend')
this.setNote = function(note, oldNote) {
this.editorMode = 'edit';
if(note.text.length == 0) {
if(note.content.text.length == 0) {
this.focusTitle(100);
}
@ -138,7 +138,7 @@ angular.module('app.frontend')
}
this.renderedContent = function() {
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.text));
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.content.text));
}
var statusTimeout;

View File

@ -18,6 +18,7 @@ angular.module('app.frontend')
// });
scope.$on('auth:validation-success', function(ev) {
// TODO
setTimeout(function(){
ctrl.onValidationSuccess();
})
@ -64,7 +65,6 @@ angular.module('app.frontend')
})
}.bind(this))
}
this.hasLocalData = function() {
@ -119,11 +119,9 @@ angular.module('app.frontend')
}
this.onValidationSuccess = function() {
if(this.user.local_encryption_enabled) {
apiController.verifyEncryptionStatusOfAllNotes(this.user, function(success){
});
}
}
this.encryptionStatusForNotes = function() {
@ -138,31 +136,6 @@ angular.module('app.frontend')
return countEncrypted + "/" + allNotes.length + " notes encrypted";
}
this.toggleEncryptionStatus = function() {
this.encryptionConfirmation = true;
}
this.cancelEncryptionChange = function() {
this.encryptionConfirmation = false;
}
this.confirmEncryptionChange = function() {
var callback = function(success, enabled) {
if(success) {
this.encryptionConfirmation = false;
this.user.local_encryption_enabled = enabled;
}
}.bind(this)
if(this.user.local_encryption_enabled) {
apiController.disableEncryptionForUser(this.user, callback);
} else {
apiController.enableEncryptionForUser(this.user, callback);
}
}
this.downloadDataArchive = function() {
var link = document.createElement('a');
link.setAttribute('download', 'neeto.json');
@ -170,6 +143,22 @@ angular.module('app.frontend')
link.click();
}
this.importFileSelected = function(files) {
var file = files[0];
var reader = new FileReader();
reader.onload = function(e) {
apiController.importJSONData(e.target.result, function(success, response){
console.log("import response", success, response);
if(success) {
// window.location.reload();
} else {
alert("There was an error importing your data. Please try again.");
}
})
}
reader.readAsText(file);
}
this.onAuthSuccess = function(user) {
this.user.id = user.id;

View File

@ -164,8 +164,6 @@ angular.module('app.frontend')
$scope.selectedNote = null;
}
note.onDelete();
if(note.dummy) {
return;
}

View File

@ -76,18 +76,12 @@ angular.module('app.frontend')
return;
}
if(this.user.local_encryption_enabled) {
if(!confirm("Sharing this group will disable local encryption on all group notes.")) {
return;
}
}
var callback = function(username) {
apiController.shareGroup(this.user, this.group, function(response){
})
}.bind(this);
if(!this.user.getUsername()) {
if(!this.user.username) {
ngDialog.open({
template: 'frontend/modals/username.html',
controller: 'UsernameModalCtrl',
@ -99,7 +93,7 @@ angular.module('app.frontend')
disableAnimation: true
});
} else {
callback(this.user.getUsername());
callback(this.user.username);
}
}
@ -138,15 +132,12 @@ angular.module('app.frontend')
this.selectNote = function(note) {
this.selectedNote = note;
this.selectionMade()(note);
note.onDelete = function() {
this.setNotes(this.group.notes, false);
}.bind(this);
}
this.createNewNote = function() {
var title = "New Note" + (this.notes ? (" " + (this.notes.length + 1)) : "");
this.newNote = new Note({title: title, content: '', dummy: true});
this.newNote = new Note({dummy: true});
this.newNote.content.title = title;
this.newNote.shared_via_group = this.group.presentation && this.group.presentation.enabled;
this.selectNote(this.newNote);
this.addNew()(this.newNote);

View File

@ -1,22 +1,33 @@
var Note = function (json_obj) {
var content;
Object.defineProperty(this, "content", {
get: function() {
return content;
},
set: function(value) {
var finalValue = value;
if(typeof value === 'string') {
try {
decodedValue = JSON.parse(value);
finalValue = decodedValue;
}
catch(e) {
finalValue = value;
}
}
content = finalValue;
},
enumerable: true,
});
_.merge(this, json_obj);
};
Note.prototype = {
set content(content) {
try {
var data = JSON.parse(content);
this.title = data.title || data.name;
this.text = data.text || data.content;
}
catch(e) {
this.text = content;
}
if(!this.content) {
this.content = {title: "", text: ""};
}
}
Note.prototype.JSONContent = function() {
return JSON.stringify({title: this.title, text: this.text});
};
/* Returns true if note is shared individually or via group */
@ -25,7 +36,7 @@ Note.prototype.isPublic = function() {
};
Note.prototype.isEncrypted = function() {
return this.loc_eek || this.local_eek ? true : false;
return (this.loc_eek || this.local_eek) && typeof this.content === 'string' ? true : false;
}
Note.prototype.hasEnabledPresentation = function() {

View File

@ -1,10 +1,3 @@
var User = function (json_obj) {
_.merge(this, json_obj);
};
User.prototype.getUsername = function() {
if(!this.presentation) {
return null;
}
return this.presentation.root_path;
};

View File

@ -99,23 +99,19 @@ angular.module('app.services')
this._performPasswordChange(current_keys, new_keys, function(response){
if(response && !response.errors) {
// this.showNewPasswordForm = false;
if(user.local_encryption_enabled) {
// reencrypt data with new gk
this.reencryptAllNotesAndSave(user, new_keys.gk, current_keys.gk, function(success){
if(success) {
this.setGk(new_keys.gk);
alert("Your password has been changed and your data re-encrypted.");
} else {
// rollback password
this._performPasswordChange(new_keys, current_keys, function(response){
alert("There was an error changing your password. Your password has been rolled back.");
window.location.reload();
})
}
}.bind(this));
} else {
alert("Your password has been changed.");
}
// reencrypt data with new gk
this.reencryptAllNotesAndSave(user, new_keys.gk, current_keys.gk, function(success){
if(success) {
this.setGk(new_keys.gk);
alert("Your password has been changed and your data re-encrypted.");
} else {
// rollback password
this._performPasswordChange(new_keys, current_keys, function(response){
alert("There was an error changing your password. Your password has been rolled back.");
window.location.reload();
})
}
}.bind(this));
} else {
// this.showNewPasswordForm = false;
alert("There was an error changing your password. Please try again.");
@ -144,46 +140,6 @@ angular.module('app.services')
})
}
this.enableEncryptionForUser = function(user, callback) {
Restangular.one("users", user.id).one('enable_encryption').post().then(function(response){
var enabled = response.plain().local_encryption_enabled;
if(!enabled) {
callback(false, enabled);
return;
}
this.handleEncryptionStatusChange(user, enabled, callback);
}.bind(this))
}
this.disableEncryptionForUser = function(user, callback) {
Restangular.one("users", user.id).one('disable_encryption').post().then(function(response){
var enabled = response.plain().local_encryption_enabled;
if(enabled) {
// something went wrong
callback(false, enabled);
return;
}
this.handleEncryptionStatusChange(user, enabled, callback);
}.bind(this))
}
this.handleEncryptionStatusChange = function(user, encryptionEnabled, callback) {
var allNotes = user.filteredNotes();
if(encryptionEnabled) {
allNotes = allNotes.filter(function(note){return note.isPublic() == false});
this.encryptNotes(allNotes, this.retrieveGk());
} else {
this.decryptNotes(allNotes, this.retrieveGk());
}
this.saveBatchNotes(user, allNotes, encryptionEnabled, function(success) {
callback(success, encryptionEnabled);
}.bind(this));
}
/*
Ensures that if encryption is disabled, all local notes are uncrypted,
and that if it's enabled, that all local notes are encrypted
@ -193,7 +149,7 @@ angular.module('app.services')
var notesNeedingUpdate = [];
var key = this.retrieveGk();
allNotes.forEach(function(note){
if(user.local_encryption_enabled && !note.isPublic()) {
if(!note.isPublic()) {
if(!note.isEncrypted()) {
// needs encryption
this.encryptSingleNote(note, key);
@ -209,7 +165,7 @@ angular.module('app.services')
}.bind(this))
if(notesNeedingUpdate.length > 0) {
this.saveBatchNotes(user, notesNeedingUpdate, user.local_encryption_enabled, callback)
this.saveBatchNotes(user, notesNeedingUpdate, true, callback)
}
}
@ -266,9 +222,12 @@ angular.module('app.services')
})
}
if(user.local_encryption_enabled && group.notes.length > 0) {
if(group.notes.length > 0) {
// decrypt group notes first
var notes = group.notes;
notes.forEach(function(note){
note.shared_via_group = true;
})
this.decryptNotesWithLocalKey(notes);
this.saveBatchNotes(user, notes, false, function(success){
shareFn();
@ -333,17 +292,16 @@ angular.module('app.services')
this.createRequestParamsFromNote = function(note, user) {
var params = {id: note.id};
if(user.local_encryption_enabled && !note.pending_share && !note.isPublic()) {
if(!note.pending_share && !note.isPublic()) {
// encrypted
var noteCopy = _.cloneDeep(note);
this.encryptSingleNote(noteCopy, this.retrieveGk());
params.loc_enc_content = noteCopy.loc_enc_content || local_encrypted_content;
params.loc_eek = noteCopy.loc_eek || noteCopy.local_eek;
params.content = noteCopy.content;
params.loc_eek = noteCopy.loc_eek;
}
else {
// decrypted
params.content = note.JSONContent();
params.content = JSON.stringify(note.content);
}
return params;
}
@ -365,7 +323,7 @@ angular.module('app.services')
if(!user.id) {
if(confirm("Note: You are not signed in. Any note you share cannot be edited or unshared.")) {
var request = Restangular.one("notes").one("share");
_.merge(request, {name: note.title, content: note.content});
_.merge(request, {name: note.content.title, content: note.content});
request.post().then(function(response){
var presentation = response.plain();
_.merge(note, {presentation: presentation});
@ -384,16 +342,10 @@ angular.module('app.services')
})
}
if(user.local_encryption_enabled) {
if(confirm("Note: Sharing this note will remove its local encryption.")) {
note.pending_share = true;
this.saveNote(user, note, function(saved_note){
shareFn(saved_note, callback);
})
}
} else {
shareFn(note, callback);
}
note.pending_share = true;
this.saveNote(user, note, function(saved_note){
shareFn(saved_note, callback);
})
}
}
@ -407,6 +359,53 @@ angular.module('app.services')
})
}
/*
Import
*/
this.importJSONData = function(jsonString, callback) {
var data = JSON.parse(jsonString);
console.log("importing data", JSON.parse(jsonString));
// data.notes = _.map(data.notes, function(json_obj) {
// return new Note(json_obj);
// });
console.log("objectifying data", this.staticifyObject(data));
data.notes.forEach(function(note){
var presentation = data.presentations.find(function(presentation){
return presentation.presentable_type == "Note" && presentation.presentable_id == note.id;
})
if(presentation) {
// public
// console.log("public note", note);
note.content = JSON.stringify(note.content);
// console.log("after json", note);
} else {
// private
this.encryptSingleNoteWithLocalKey(note);
}
}.bind(this))
var request = Restangular.one("import");
request.data = data;
console.log("posting import request", request);
request.post().then(function(response){
callback(true, response);
})
.catch(function(error){
callback(false, error);
})
}
/*
Export
*/
this.notesDataFile = function(user) {
var textFile = null;
var makeTextFile = function (text) {
@ -424,18 +423,59 @@ angular.module('app.services')
return textFile;
}.bind(this);
// remove irrelevant keys
var notes = _.map(user.filteredNotes(), function(note){
console.log("mapping note", note);
return {
id: note.id,
title: note.title,
text: note.text,
uuid: note.uuid,
content: note.content,
group_id: note.group_id,
created_at: note.created_at,
modified_at: note.modified_at,
group_id: note.group_id
}
});
return makeTextFile(JSON.stringify(notes, null, 2 /* pretty print */));
var groups = _.map(user.groups, function(group){
return {
id: group.id,
uuid: group.uuid,
name: group.name,
created_at: group.created_at,
modified_at: group.modified_at,
}
});
var modelsWithPresentations = user.groups.concat(user.notes).filter(function(model){
return model.presentation != null;
})
var presentations = _.map(modelsWithPresentations, function(model){
return model.presentation;
})
presentations = _.map(presentations, function(presentation){
return {
id: presentation.id,
uuid: presentation.uuid,
host: presentation.host,
root_path: presentation.root_path,
relative_path: presentation.relative_path,
presentable_type: presentation.presentable_type,
presentable_id: presentation.presentable_id,
enabled: presentation.enabled,
created_at: presentation.created_at,
modified_at: presentation.modified_at,
}
});
var data = {
notes: notes,
groups: groups,
presentations: presentations
}
return makeTextFile(JSON.stringify(data, null, 2 /* pretty print */));
}
@ -539,13 +579,13 @@ angular.module('app.services')
this.encryptSingleNote = function(note, key) {
var ek = null;
if(note.isEncrypted()) {
ek = Neeto.crypto.decryptText(note.loc_eek || note.local_eek, key);
if(note.loc_eek) {
ek = Neeto.crypto.decryptText(note.loc_eek, key);
} else {
ek = Neeto.crypto.generateRandomEncryptionKey();
note.loc_eek = Neeto.crypto.encryptText(ek, key);
}
note.loc_enc_content = Neeto.crypto.encryptText(note.JSONContent(), ek);
note.content = Neeto.crypto.encryptText(JSON.stringify(note.content), ek);
note.local_encryption_scheme = "1.0";
}
@ -563,18 +603,27 @@ angular.module('app.services')
this.encryptNotes(notes, this.retrieveGk());
}
this.encryptNonPublicNotesWithLocalKey = function(notes) {
var nonpublic = notes.filter(function(note){
return !note.isPublic() && !note.pending_share;
})
this.encryptNotes(nonpublic, this.retrieveGk());
}
this.decryptSingleNoteWithLocalKey = function(note) {
this.decryptSingleNote(note, this.retrieveGk());
}
this.decryptSingleNote = function(note, key) {
var ek = Neeto.crypto.decryptText(note.loc_eek || note.local_eek, key);
var content = Neeto.crypto.decryptText(note.loc_enc_content || note.local_encrypted_content, ek);
var content = Neeto.crypto.decryptText(note.content, ek);
// console.log("decrypted contnet", content);
note.content = content;
}
this.decryptNotes = function(notes, key) {
notes.forEach(function(note){
// console.log("is encrypted?", note);
if(note.isEncrypted()) {
this.decryptSingleNote(note, key);
}

View File

@ -0,0 +1,17 @@
angular
.module('app.services')
.directive('fileChange', function() {
return {
restrict: 'A',
scope: {
handler: '&'
},
link: function (scope, element) {
element.on('change', function (event) {
scope.$apply(function(){
scope.handler({files: event.target.files});
});
});
}
};
});

View File

@ -2,7 +2,7 @@
.content
.section-title-bar.editor-heading{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
.title
%input.input#note-title-editor{"ng-model" => "ctrl.note.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
%input.input#note-title-editor{"ng-model" => "ctrl.note.content.title", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveTitle($event)",
"ng-disabled" => "ctrl.note.locked", "ng-change" => "ctrl.nameChanged()", "ng-focus" => "ctrl.onNameFocus()",
"select-on-click" => "true"}
.save-status {{ctrl.noteStatus}}
@ -56,6 +56,6 @@
"iteration-callback" => "ctrl.callback", "prebegin-fn" => "ctrl.prebeginFn", "iteration-delay" => "2000", "cursor" => ""}
%code{"ng-if" => "ctrl.currentDemoContent.text"}
.content-sampler.sampler{"typewrite" => "true", "text" => "ctrl.currentDemoContent.text", "type-delay" => "10", "iteration-callback" => "ctrl.contentCallback"}
%textarea.editable#note-text-editor{"ng-disabled" => "ctrl.note.locked", "ng-show" => "ctrl.editorMode == 'edit'", "ng-model" => "ctrl.note.text",
%textarea.editable#note-text-editor{"ng-disabled" => "ctrl.note.locked", "ng-show" => "ctrl.editorMode == 'edit'", "ng-model" => "ctrl.note.content.text",
"ng-change" => "ctrl.contentChanged()", "ng-click" => "ctrl.clickedTextArea()", "ng-focus" => "ctrl.onContentFocus()"}
.preview{"ng-if" => "ctrl.editorMode == 'preview'", "ng-bind-html" => "ctrl.renderedContent()", "ng-dblclick" => "ctrl.onPreviewDoubleClick()"}

View File

@ -52,28 +52,8 @@
.title Local Encryption
.desc Encrypt notes locally before sending to server. Neither the server owner nor an intrusive government can decrypt your locally encrypted notes.
.action-container
%span.status-title Status:
{{ctrl.user.local_encryption_enabled ? 'enabled' : 'disabled'}}
{{" | "}}
%a{"ng-click" => "ctrl.toggleEncryptionStatus()"}
{{ctrl.user.local_encryption_enabled ? 'Disable' : 'Enable'}}
.subtext{"ng-if" => "ctrl.user.local_encryption_enabled"}
{{ctrl.encryptionStatusForNotes()}} (shared notes not encrypted)
.encryption-confirmation{"ng-if" => "ctrl.encryptionConfirmation"}
%div{"ng-if" => "ctrl.user.local_encryption_enabled"}
%p Are you sure you want to disable local encryption? All currently encrypted notes will be decrypted locally, then sent back to Neeto servers over a secure connection.
%div{"ng-if" => "!ctrl.user.local_encryption_enabled"}
%p We're glad you're taking privacy and security into your own hands. There are a couple things you should note about moving to local encryption:
%ul
%li
If you forget your password, there is no way to reset it or recover it. Your data will be forever lost without your password.
(You can however still change your password, as long as you know your current password.)
%li
The strength of the encryption is tied to the strength of your password. If you take your security seriously, you should use a password of at least 32 characters long.
%p Are you sure you want to enable local encryption?
.buttons
%a.cancel{"ng-click" => "ctrl.cancelEncryptionChange()"} Cancel
%a.confirm{"ng-click" => "ctrl.confirmEncryptionChange()"} Confirm
%span.status-title Status: enabled.
{{ctrl.encryptionStatusForNotes()}} (shared notes not encrypted)
.account-item{"ng-if" => "ctrl.user.email"}
.icon-container
%img.icon.archive{"lazy-img" => "assets/archive.png"}
@ -82,6 +62,10 @@
.desc Note: data archives that you download using the link below are decrypted before save. You should take care to store them in a safe location.
.action-container
%a#download-archive{"ng-click" => "ctrl.downloadDataArchive()"} Download Latest Data Archive
%label#import-archive
%input{"type" => "file", "style" => "display: none;", "file-change" => "", "handler" => "ctrl.importFileSelected(files)"}
%span
Import Data from Archive
.account-item
.meta-container
@ -134,4 +118,3 @@
%strong.question How does local encryption work?
%p.answer Users who opt into using local encryption can add an additional layer of security over their notes. These notes will be encrypted locally on your machine before being sent over the wire. This means that when Neeto receives your notes, we have no idea what the contents are. And if a government ever forces us to give up your data, we couldn't decrypt it for them even if we wanted to.
%p This encryption is based on your password, which is also never sent over the air. The strength of this encryption is directly tied to the strength of your password.

View File

@ -34,5 +34,5 @@
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}",
"ng-attr-draggable" => "{{note.dummy ? undefined : 'true'}}", "note" => "note"}
.name
{{note.title}}
{{note.content.title}}
.date {{note.created_at || 'Now'}}

View File

@ -1,4 +1,4 @@
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
Rails.application.config.filter_parameters += [:password, :content, :name, :local_encrypted_content, :loc_eek]
Rails.application.config.filter_parameters += [:password]