initial commit

This commit is contained in:
Mo Bitar
2016-12-05 17:33:27 -06:00
commit c6ab2f4122
146 changed files with 6185 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
'use strict';
var Neeto = Neeto || {};
angular
.module('app.frontend', [
'app.services',
'ui.router',
'ng-token-auth',
'restangular',
'ipCookie',
'oc.lazyLoad',
'angularLazyImg',
'ngDialog',
])
.config(configureAuth);

View File

@@ -0,0 +1,23 @@
'use strict';
angular
.module('app.services', [
'restangular'
])
// Configure path to API
.config(function (RestangularProvider, apiControllerProvider) {
var url = apiControllerProvider.defaultServerURL();
RestangularProvider.setBaseUrl(url);
})
// Shared function for configure auth service. Can be overwritten.
function configureAuth ($authProvider, apiControllerProvider) {
var url = apiControllerProvider.defaultServerURL();
$authProvider.configure([{
default: {
apiUrl: url,
passwordResetSuccessUrl: window.location.protocol + '//' + window.location.host + '/auth/reset',
}
}]);
}

View File

@@ -0,0 +1,20 @@
angular.module('app.frontend')
.controller('BaseCtrl', function ($rootScope, $scope, $state, $auth, apiController) {
$rootScope.$on('auth:password-change-success', function(ev) {
$state.go("home");
});
$rootScope.$on('auth:password-change-error', function(ev, reason) {
alert("Error: " + reason);
});
$rootScope.resetPasswordSubmit = function() {
var new_keys = Neeto.crypto.generateEncryptionKeysForUser($rootScope.resetData.password, $rootScope.resetData.email);
var data = _.clone($rootScope.resetData);
data.password = new_keys.pw;
data.password_confirmation = new_keys.pw;
$auth.updatePassword(data);
apiController.setGk(new_keys.gk);
}
});

View File

@@ -0,0 +1,296 @@
angular.module('app.frontend')
.directive("editorSection", function($timeout){
return {
restrict: 'E',
scope: {
save: "&",
remove: "&",
note: "=",
user: "="
},
templateUrl: 'frontend/editor.html',
replace: true,
controller: 'EditorCtrl',
controllerAs: 'ctrl',
bindToController: true,
link:function(scope, elem, attrs, ctrl) {
var handler = function(event) {
if (event.ctrlKey || event.metaKey) {
switch (String.fromCharCode(event.which).toLowerCase()) {
case 's':
event.preventDefault();
$timeout(function(){
ctrl.saveNote(event);
});
break;
case 'e':
event.preventDefault();
$timeout(function(){
ctrl.clickedEditNote();
})
break;
case 'm':
event.preventDefault();
$timeout(function(){
ctrl.toggleMarkdown();
})
break;
case 'o':
event.preventDefault();
$timeout(function(){
ctrl.toggleFullScreen();
})
break;
}
}
};
window.addEventListener('keydown', handler);
scope.$on('$destroy', function(){
window.removeEventListener('keydown', handler);
})
scope.$watch('ctrl.note', function(note, oldNote){
if(note) {
ctrl.setNote(note, oldNote);
} else {
ctrl.note = {};
}
});
}
}
})
.controller('EditorCtrl', function ($sce, $timeout, apiController, markdownRenderer, $rootScope) {
this.demoNotes = [
{name: "Live print a file with tail", content: "tail -f log/production.log"},
{name: "Create SSH tunnel", content: "ssh -i .ssh/key.pem -N -L 3306:example.com:3306 ec2-user@example.com"},
{name: "List of processes running on port", content: "lsof -i:8080"},
{name: "Set ENV from file", content: "export $(cat .envfile | xargs)"},
{name: "Find process by name", content: "ps -ax | grep <application name>"},
{name: "NPM install without sudo", content: "sudo chown -R $(whoami) ~/.npm"},
{name: "Email validation regex", content: "^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"},
{name: "Ruby generate 256 bit key", content: "Digest::SHA256.hexdigest(SecureRandom.random_bytes(32))"},
{name: "Mac add user to user group", content: "sudo dseditgroup -o edit -a USERNAME -t user GROUPNAME"},
{name: "Kill Mac OS System Apache", content: "sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist"},
{name: "Docker run with mount binding and port", content: "docker run -v /home/vagrant/www/app:/var/www/app -p 8080:80 -d kpi/s3"},
{name: "MySQL grant privileges", content: "GRANT [type of permission] ON [database name].[table name] TO [username]@'%;"},
{name: "MySQL list users", content: "SELECT User FROM mysql.user;"},
];
this.showSampler = !this.user.id && this.user.filteredNotes().length == 0;
this.demoNoteNames = _.map(this.demoNotes, function(note){
return note.name;
});
this.currentDemoContent = {text: null};
this.prebeginFn = function() {
this.currentDemoContent.text = null;
}.bind(this)
this.callback = function(index) {
this.currentDemoContent.text = this.demoNotes[index].content;
}.bind(this)
this.contentCallback = function(index) {
}
this.setNote = function(note, oldNote) {
this.editorMode = 'edit';
if(note.content.length == 0) {
this.focusTitle(100);
}
if(oldNote && oldNote != note) {
if(oldNote.hasChanges) {
this.save()(oldNote, null);
} else if(oldNote.dummy) {
this.remove()(oldNote);
}
}
}
this.onPreviewDoubleClick = function() {
this.editorMode = 'edit';
this.focusEditor(100);
}
this.focusEditor = function(delay) {
setTimeout(function(){
var element = document.getElementById("note-text-editor");
element.focus();
}, delay)
}
this.focusTitle = function(delay) {
setTimeout(function(){
document.getElementById("note-title-editor").focus();
}, delay)
}
this.clickedTextArea = function() {
this.showMenu = false;
}
this.renderedContent = function() {
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText(this.note.content));
}
var statusTimeout;
this.saveNote = function($event) {
var note = this.note;
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"
}.bind(this), 200)
}
}.bind(this));
}
this.saveTitle = function($event) {
$event.target.blur();
this.saveNote($event);
this.focusEditor();
}
var saveTimeout;
this.changesMade = function() {
this.note.hasChanges = true;
this.note.dummy = false;
apiController.saveDraftToDisk(this.note);
if(saveTimeout) $timeout.cancel(saveTimeout);
if(statusTimeout) $timeout.cancel(statusTimeout);
saveTimeout = $timeout(function(){
this.noteStatus = "Saving...";
this.saveNote();
}.bind(this), 150)
}
this.contentChanged = function() {
this.changesMade();
}
this.nameChanged = function() {
this.changesMade();
}
this.onNameFocus = function() {
this.editingName = true;
}
this.onContentFocus = function() {
this.showSampler = false;
$rootScope.$broadcast("editorFocused");
this.editingUrl = false;
}
this.onNameBlur = function() {
this.editingName = false;
}
this.toggleFullScreen = function() {
this.fullscreen = !this.fullscreen;
if(this.fullscreen) {
if(this.editorMode == 'edit') {
// refocus
this.focusEditor(0);
}
} else {
}
}
this.selectedMenuItem = function() {
this.showMenu = false;
}
this.toggleMarkdown = function() {
if(this.editorMode == 'preview') {
this.editorMode = 'edit';
} else {
this.editorMode = 'preview';
}
}
this.editUrlPressed = function() {
this.showMenu = false;
var url = this.publicUrlForNote(this.note);
url = url.replace(this.note.presentation.root_path, "");
this.url = {base: url, token : this.note.presentation.root_path};
this.editingUrl = true;
}
this.saveUrl = function($event) {
$event.target.blur();
var original = this.note.presentation.root_path;
this.note.presentation.root_path = this.url.token;
apiController.saveNote(this.user, this.note, function(note){
if(!note) {
this.note.token = original;
this.url.token = original;
alert("This URL is not available.");
} else {
this.editingUrl = false;
}
}.bind(this))
}
this.shareNote = function() {
function openInNewTab(url) {
var a = document.createElement("a");
a.target = "_blank";
a.href = url;
a.click();
}
apiController.shareNote(this.user, this.note, function(note){
openInNewTab(this.publicUrlForNote(note));
}.bind(this))
this.showMenu = false;
}
this.unshareNote = function() {
apiController.unshareNote(this.user, this.note, function(note){
})
this.showMenu = false;
}
this.publicUrlForNote = function() {
return this.note.presentationURL();
}
this.clickedMenu = function() {
if(this.note.locked) {
alert("This note has been shared without an account, and can therefore not be changed.")
} else {
this.showMenu = !this.showMenu;
}
}
this.deleteNote = function() {
apiController.clearDraft();
this.remove()(this.note);
this.showMenu = false;
}
this.clickedEditNote = function() {
this.editorMode = 'edit';
this.focusEditor(100);
}
});

View File

@@ -0,0 +1,116 @@
angular.module('app.frontend')
.directive("groupsSection", function(){
return {
restrict: 'E',
scope: {
addNew: "&",
selectionMade: "&",
willSelect: "&",
save: "&",
groups: "=",
allGroup: "=",
user: "=",
updateNoteGroup: "&"
},
templateUrl: 'frontend/groups.html',
replace: true,
controller: 'GroupsCtrl',
controllerAs: 'ctrl',
bindToController: true,
link:function(scope, elem, attrs, ctrl) {
scope.$watch('ctrl.groups', function(newGroups){
if(newGroups) {
ctrl.setGroups(newGroups);
}
});
}
}
})
.controller('GroupsCtrl', function (apiController) {
var initialLoad = true;
this.setGroups = function(groups) {
if(initialLoad) {
initialLoad = false;
this.selectGroup(this.allGroup);
} else {
if(groups && groups.length > 0) {
this.selectGroup(groups[0]);
}
}
}
this.selectGroup = function(group) {
this.willSelect()(group);
this.selectedGroup = group;
this.selectionMade()(group);
}
this.clickedAddNewGroup = function() {
if(this.editingGroup) {
return;
}
this.newGroup = {notes : []};
if(!this.user.id) {
this.newGroup.id = Neeto.crypto.generateRandomKey()
}
this.selectedGroup = this.newGroup;
this.editingGroup = this.newGroup;
this.addNew()(this.newGroup);
}
var originalGroupName = "";
this.onGroupTitleFocus = function(group) {
originalGroupName = group.name;
}
this.groupTitleDidChange = function(group) {
this.editingGroup = group;
}
this.saveGroup = function($event, group) {
this.editingGroup = null;
if(group.name.length == 0) {
group.name = originalGroupName;
originalGroupName = "";
return;
}
$event.target.blur();
if(!group.name || group.name.length == 0) {
return;
}
this.save()(group, function(savedGroup){
_.merge(group, savedGroup);
this.selectGroup(group);
this.newGroup = null;
}.bind(this));
}
this.noteCount = function(group) {
var validNotes = apiController.filterDummyNotes(group.notes);
return validNotes.length;
}
this.handleDrop = function(e, newGroup, note) {
if(this.selectedGroup.all) {
// coming from all, remove from original group if applicable
if(note.group_id) {
var originalGroup = this.groups.filter(function(group){
return group.id == note.group_id;
})[0];
_.remove(originalGroup.notes, note);
}
} else {
_.remove(this.selectedGroup.notes, note);
}
this.updateNoteGroup()(note, newGroup, this.selectedGroup);
}.bind(this)
});

View File

@@ -0,0 +1,232 @@
angular.module('app.frontend')
.directive("header", function(){
return {
restrict: 'E',
scope: {
user: "=",
logout: "&"
},
templateUrl: 'frontend/header.html',
replace: true,
controller: 'HeaderCtrl',
controllerAs: 'ctrl',
bindToController: true,
link:function(scope, elem, attrs, ctrl) {
scope.$on('auth:login-success', function(event, user) {
ctrl.onAuthSuccess(user);
});
scope.$on('auth:validation-success', function(ev) {
setTimeout(function(){
ctrl.onValidationSuccess();
})
});
}
}
})
.controller('HeaderCtrl', function ($auth, $state, apiController, serverSideValidation, $timeout) {
this.changePasswordPressed = function() {
this.showNewPasswordForm = !this.showNewPasswordForm;
}
this.accountMenuPressed = function() {
this.serverData = {url: apiController.getServer()};
this.showAccountMenu = !this.showAccountMenu;
this.showFaq = false;
this.showNewPasswordForm = false;
}
this.changeServer = function() {
apiController.setServer(this.serverData.url, true);
}
this.signOutPressed = function() {
this.showAccountMenu = false;
$auth.signOut();
this.logout()();
apiController.clearGk();
window.location.reload();
}
this.submitPasswordChange = function() {
this.passwordChangeData.status = "Generating New Keys...";
$timeout(function(){
var current_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.current_password, this.user.email);
var new_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.new_password, this.user.email);
// var new_pw_conf_keys = Neeto.crypto.generateEncryptionKeysForUser(this.passwordChangeData.new_password_confirmation, this.user.email);
var data = {};
data.current_password = current_keys.pw;
data.password = new_keys.pw;
data.password_confirmation = new_keys.pw;
var user = this.user;
if(data.password == data.password_confirmation) {
$auth.updatePassword(data)
.then(function(response) {
this.showNewPasswordForm = false;
if(user.local_encryption_enabled) {
// reencrypt data with new gk
apiController.reencryptAllNotesAndSave(user, new_keys.gk, current_keys.gk, function(success){
if(success) {
apiController.setGk(new_keys.gk);
alert("Your password has been changed and your data re-encrypted.");
} else {
// rollback password
$auth.updatePassword({current_password: new_keys.pw, password: current_keys.pw, password_confirmation: current_keys.pw })
.then(function(response){
alert("There was an error changing your password. Your password has been rolled back.");
window.location.reload();
})
}
});
} else {
alert("Your password has been changed.");
}
}.bind(this))
.catch(function(response){
this.showNewPasswordForm = false;
alert("There was an error changing your password. Please try again.");
}.bind(this))
} else {
alert("Your new password does not match its confirmation.");
}
}.bind(this))
}
this.hasLocalData = function() {
return this.user.filteredNotes().length > 0;
}
this.mergeLocalChanged = function() {
if(!this.user.shouldMerge) {
if(!confirm("Unchecking this option means any locally stored groups and notes you have now will be deleted. Are you sure you want to continue?")) {
this.user.shouldMerge = true;
}
}
}
this.loginSubmitPressed = function() {
this.loginData.status = "Generating Login Keys...";
$timeout(function(){
var keys = Neeto.crypto.generateEncryptionKeysForUser(this.loginData.user_password, this.loginData.email);
var data = {password: keys.pw, email: this.loginData.email};
apiController.setGk(keys.gk);
$auth.submitLogin(data)
.then(function(response){
})
.catch(function(response){
this.loginData.status = response.errors[0];
}.bind(this))
}.bind(this))
}
this.submitRegistrationForm = function() {
this.loginData.status = "Generating Account Keys...";
$timeout(function(){
var keys = Neeto.crypto.generateEncryptionKeysForUser(this.loginData.user_password, this.loginData.email);
var data = {password: keys.pw, email: this.loginData.email};
apiController.setGk(keys.gk);
$auth.submitRegistration(data)
.then(function(response) {
$auth.user.id = response.data.data.id;
this.onAuthSuccess($auth.user);
}.bind(this))
.catch(function(response) {
this.loginData.status = response.data.errors.full_messages[0];
}.bind(this));
}.bind(this))
}
this.forgotPasswordSubmit = function() {
$auth.requestPasswordReset(this.resetData)
.then(function(resp) {
this.resetData.response = "Success";
// handle success response
}.bind(this))
.catch(function(resp) {
// handle error response
this.resetData.response = "Error";
}.bind(this));
}
this.onValidationSuccess = function() {
if(this.user.local_encryption_enabled) {
apiController.verifyEncryptionStatusOfAllNotes(this.user, function(success){
});
}
}
this.encryptionStatusForNotes = function() {
var allNotes = this.user.filteredNotes();
var countEncrypted = 0;
allNotes.forEach(function(note){
if(note.isEncrypted()) {
countEncrypted++;
}
}.bind(this))
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');
link.href = apiController.notesDataFile(this.user);
link.click();
}
this.onAuthSuccess = function(user) {
this.user.id = user.id;
if(this.user.shouldMerge && this.hasLocalData()) {
apiController.mergeLocalDataRemotely(this.user, function(){
window.location.reload();
});
} else {
window.location.reload();
}
this.showLogin = false;
this.showRegistration = false;
}
});

View File

@@ -0,0 +1,188 @@
angular.module('app.frontend')
.controller('HomeCtrl', function ($scope, $rootScope, Restangular, $timeout, $state, $sce, $auth, apiController) {
$rootScope.bodyClass = "app-body-class";
$rootScope.title = "Notes — Neeto, a secure code box for developers";
$rootScope.description = "A secure code box for developers to store common commands and useful notes.";
var onUserSet = function() {
$scope.defaultUser.notes = _.map($scope.defaultUser.notes, function(json_obj) {
return new Note(json_obj);
});
$scope.defaultUser.filteredNotes = function() {
return apiController.filterDummyNotes($scope.defaultUser.notes);
}
var groups = $scope.defaultUser.groups;
var allNotes = $scope.defaultUser.notes;
groups.forEach(function(group){
var notes = allNotes.filter(function(note){return note.group_id && note.group_id == group.id});
group.notes = notes;
})
$scope.allGroup = {name: "All", all: true};
$scope.groups = groups;
}
$auth.validateUser()
.then(function () {
$scope.defaultUser = new User($auth.user);
$rootScope.title = "Notes — Neeto";
onUserSet();
})
.catch(function () {
$scope.defaultUser = new User(apiController.localUser());
onUserSet();
});
/*
Groups Ctrl Callbacks
*/
$scope.updateAllGroup = function() {
var allNotes = apiController.filterDummyNotes($scope.defaultUser.notes);
$scope.defaultUser.notes = allNotes;
$scope.allGroup.notes = allNotes;
}
$scope.groupsWillMakeSelection = function(group) {
if(group.all) {
$scope.updateAllGroup();
}
}
$scope.groupsSelectionMade = function(group) {
if(!group.notes) {
group.notes = [];
}
$scope.selectedGroup = group;
}
$scope.groupsAddNew = function(group) {
$scope.defaultUser.groups.unshift(group);
}
$scope.groupsSave = function(group, callback) {
apiController.saveGroup($scope.defaultUser, group, callback);
}
/*
Called to update the group of a note after drag and drop change
The note object is a copy of the original
*/
$scope.groupsUpdateNoteGroup = function(noteCopy, newGroup, oldGroup) {
var originalNote = _.find($scope.defaultUser.notes, {id: noteCopy.id});
if(newGroup.all) {
// going to new group, nil out group_id
originalNote.group_id = null;
} else {
originalNote.group_id = newGroup.id
newGroup.notes.unshift(originalNote);
newGroup.notes.sort(function(a,b){
//subtract to get a value that is either negative, positive, or zero.
return new Date(b.created_at) - new Date(a.created_at);
});
}
originalNote.shared_via_group = newGroup.presentation && newGroup.presentation.enabled;
apiController.saveNote($scope.defaultUser, originalNote, function(note){
_.merge(originalNote, note);
});
}
/*
Notes Ctrl Callbacks
*/
$scope.notesRemoveGroup = function(group) {
var validNotes = apiController.filterDummyNotes(group.notes);
if(validNotes == 0) {
// if no more notes, delete group
apiController.deleteGroup($scope.defaultUser, group, function(){
// force scope groups to update on sub directives
$scope.groups = [];
$timeout(function(){
$scope.groups = $scope.defaultUser.groups;
})
});
} else {
alert("To delete this group, remove all its notes first.");
}
}
$scope.notesSelectionMade = function(note) {
$scope.selectedNote = note;
}
$scope.notesAddNew = function(note) {
if(!$scope.defaultUser.id) {
// generate local id for note
note.id = Neeto.crypto.generateRandomKey();
}
$scope.defaultUser.notes.unshift(note);
if(!$scope.selectedGroup.all) {
$scope.selectedGroup.notes.unshift(note);
note.group_id = $scope.selectedGroup.id;
}
}
/*
Shared Callbacks
*/
$scope.saveNote = function(note, callback) {
apiController.saveNote($scope.defaultUser, note, function(){
// add to All notes if it doesnt exist
if(!_.find($scope.defaultUser.notes, {id: note.id})) {
$scope.defaultUser.notes.unshift(note);
}
note.hasChanges = false;
if(callback) {
callback(true);
}
})
}
$scope.deleteNote = function(note) {
_.remove($scope.defaultUser.notes, note);
if($scope.selectedGroup.all && note.group_id) {
var originalGroup = _.find($scope.groups, {id: note.group_id});
if(originalGroup) {
_.remove(originalGroup.notes, note);
}
} else {
_.remove($scope.selectedGroup.notes, note);
}
if(note == $scope.selectedNote) {
$scope.selectedNote = null;
}
note.onDelete();
if(note.dummy) {
return;
}
apiController.deleteNote($scope.defaultUser, note, function(success){
})
}
/*
Header Ctrl Callbacks
*/
$scope.headerLogout = function() {
$scope.defaultUser = apiController.localUser();
$scope.groups = $scope.defaultUser.groups;
}
});

View File

@@ -0,0 +1,173 @@
angular.module('app.frontend')
.directive("notesSection", function(){
return {
scope: {
addNew: "&",
selectionMade: "&",
remove: "&",
group: "=",
user: "=",
removeGroup: "&"
},
templateUrl: 'frontend/notes.html',
replace: true,
controller: 'NotesCtrl',
controllerAs: 'ctrl',
bindToController: true,
link:function(scope, elem, attrs, ctrl) {
scope.$watch('ctrl.group', function(group, oldGroup){
if(group) {
ctrl.groupDidChange(group, oldGroup);
}
});
}
}
})
.controller('NotesCtrl', function (apiController, $timeout, ngDialog, $rootScope) {
$rootScope.$on("editorFocused", function(){
this.showMenu = false;
}.bind(this))
var isFirstLoad = true;
this.groupDidChange = function(group, oldGroup) {
this.showMenu = false;
if(this.selectedNote && this.selectedNote.dummy) {
_.remove(oldGroup.notes, this.selectedNote);
}
this.noteFilter.text = "";
this.setNotes(group.notes, false);
if(isFirstLoad) {
$timeout(function(){
var draft = apiController.getDraft();
if(draft) {
var note = draft;
this.selectNote(note);
} else {
this.createNewNote();
isFirstLoad = false;
}
}.bind(this))
} else if(group.notes.length == 0) {
this.createNewNote();
}
}
this.selectedGroupDelete = function() {
this.showMenu = false;
this.removeGroup()(this.group);
}
this.selectedGroupShare = function() {
this.showMenu = false;
if(!this.user.id) {
alert("You must be signed in to share a group.");
return;
}
if(this.group.all) {
alert("You cannot share the 'All' group.");
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()) {
ngDialog.open({
template: 'frontend/modals/username.html',
controller: 'UsernameModalCtrl',
resolve: {
user: function() {return this.user}.bind(this),
callback: function() {return callback}
},
className: 'ngdialog-theme-default',
disableAnimation: true
});
} else {
callback(this.user.getUsername());
}
}
this.selectedGroupUnshare = function() {
this.showMenu = false;
apiController.unshareGroup(this.user, this.group, function(response){
})
}
this.publicUrlForGroup = function() {
return this.group.presentation.url;
}
this.setNotes = function(notes, createNew) {
this.notes = notes;
notes.forEach(function(note){
note.visible = true;
})
apiController.decryptNotesWithLocalKey(notes);
this.selectFirstNote(createNew);
}
this.selectFirstNote = function(createNew) {
var visibleNotes = this.notes.filter(function(note){
return note.visible;
});
if(visibleNotes.length > 0) {
this.selectNote(visibleNotes[0]);
} else if(createNew) {
this.createNewNote();
}
}
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 name = "New Note" + (this.notes ? (" " + (this.notes.length + 1)) : "");
this.newNote = new Note({name: name, content: '', dummy: true});
this.newNote.shared_via_group = this.group.presentation && this.group.presentation.enabled;
this.selectNote(this.newNote);
this.addNew()(this.newNote);
}
this.noteFilter = {text : ''};
this.filterNotes = function(note) {
if(this.noteFilter.text.length == 0) {
note.visible = true;
} else {
note.visible = note.name.toLowerCase().includes(this.noteFilter.text) || note.content.toLowerCase().includes(this.noteFilter.text);
}
return note.visible;
}.bind(this)
this.filterTextChanged = function() {
$timeout(function(){
if(!this.selectedNote.visible) {
this.selectFirstNote(false);
}
}.bind(this), 100)
}
});

View File

@@ -0,0 +1,13 @@
angular.module('app.frontend')
.controller('UsernameModalCtrl', function ($scope, apiController, Restangular, user, callback, $timeout) {
$scope.formData = {};
$scope.saveUsername = function() {
apiController.setUsername(user, $scope.formData.username, function(response){
var username = response.root_path;
user.presentation = response;
callback(username);
$scope.closeThisDialog();
})
}
});

View File

@@ -0,0 +1,20 @@
var Note = function (json_obj) {
_.merge(this, json_obj);
};
/* Returns true if note is shared individually or via group */
Note.prototype.isPublic = function() {
return this.hasEnabledPresentation() || this.shared_via_group;
};
Note.prototype.isEncrypted = function() {
return this.local_eek ? true : false;
}
Note.prototype.hasEnabledPresentation = function() {
return this.presentation && this.presentation.enabled;
}
Note.prototype.presentationURL = function() {
return this.presentation.url;
}

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,116 @@
angular.module('app.frontend')
.config(function ($stateProvider, $urlRouterProvider, $locationProvider) {
$stateProvider
.state('base', {
abstract: true,
})
// Homepage
.state('home', {
url: '/',
parent: 'base',
views: {
'content@' : {
templateUrl: 'frontend/home.html',
controller: 'HomeCtrl'
}
}
})
.state('presentation', {
url: '/:root_path',
parent: 'base',
views: {
'content@' : {
templateUrl: 'frontend/presentation.html',
controller: "PresentationCtrl"
}
},
resolve: {
presentation: getPresentation
}
})
.state('group', {
url: '/:root_path/:secondary_path',
parent: 'base',
views: {
'content@' : {
templateUrl: 'frontend/presentation.html',
controller: "PresentationCtrl"
}
},
resolve: {
presentation: getPresentation
}
})
// Auth routes
.state('auth', {
abstract: true,
url: '/auth',
parent: 'base',
views: {
'content@' : {
templateUrl: 'frontend/auth/wrapper.html',
}
}
})
.state('auth.login', {
url: '/login',
templateUrl: 'frontend/auth/login.html',
})
.state('auth.forgot', {
url: '/forgot',
templateUrl: 'frontend/auth/forgot.html',
})
.state('auth.reset', {
url: '/reset?reset_password_token&email',
templateUrl: 'frontend/auth/reset.html',
controller: function($rootScope, $stateParams) {
$rootScope.resetData = {reset_password_token: $stateParams.reset_password_token, email: $stateParams.email};
// Clear reset_password_token on change state
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams, options) {
$rootScope.reset_password_token = null;
});
},
})
// 404 Error
.state('404', {
parent: 'base',
views: {
'content@' : {
templateUrl: 'frontend/errors/404.html'
}
}
});
function getPresentation ($q, $state, $stateParams, Restangular) {
var deferred = $q.defer();
var restangularQuery = Restangular.one('presentations', 'show_by_path');
restangularQuery.get({root_path: $stateParams.root_path, secondary_path: $stateParams.secondary_path})
.then(function(response) {
deferred.resolve(response);
})
.catch(function(response) {
$state.go('404');
});
return deferred.promise;
}
// Default fall back route
$urlRouterProvider.otherwise(function($injector, $location){
var state = $injector.get('$state');
state.go('404');
return $location.path();
});
// enable HTML5 Mode for SEO
$locationProvider.html5Mode(true);
});

View File

@@ -0,0 +1,528 @@
angular.module('app.services')
.provider('apiController', function () {
function domainName() {
var domain_comps = location.hostname.split(".");
var domain = domain_comps[domain_comps.length - 2] + "." + domain_comps[domain_comps.length - 1];
return domain;
}
var url;
this.defaultServerURL = function() {
if(!url) {
url = localStorage.getItem("server");
if(!url) {
url = location.protocol + "//" + domainName() + (location.port ? ':' + location.port: '');
}
}
return url;
}
this.$get = function(Restangular) {
return new ApiController(Restangular);
}
function ApiController(Restangular) {
/*
Config
*/
this.getServer = function() {
if(!url) {
url = localStorage.getItem("server");
if(!url) {
url = location.protocol + "//" + domainName() + (location.port ? ':' + location.port: '');
this.setServer(url);
}
}
return url;
}
this.setServer = function(url, refresh) {
localStorage.setItem("server", url);
if(refresh) {
window.location.reload();
}
}
/*
User
*/
this.setUsername = function(user, username, callback) {
var request = Restangular.one("users", user.id).one("set_username");
request.username = username;
request.post().then(function(response){
callback(response.plain());
})
}
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
*/
this.verifyEncryptionStatusOfAllNotes = function(user, callback) {
var allNotes = user.filteredNotes();
var notesNeedingUpdate = [];
var key = this.retrieveGk();
allNotes.forEach(function(note){
if(user.local_encryption_enabled && !note.isPublic()) {
if(!note.isEncrypted()) {
// needs encryption
this.encryptSingleNote(note, key);
notesNeedingUpdate.push(note);
}
} else {
if(note.isEncrypted()) {
// needs decrypting
this.decryptSingleNote(note, key);
notesNeedingUpdate.push(note);
}
}
}.bind(this))
if(notesNeedingUpdate.length > 0) {
this.saveBatchNotes(user, notesNeedingUpdate, user.local_encryption_enabled, callback)
}
}
/*
Groups
*/
this.restangularizeGroup = function(group, user) {
var request = Restangular.one("users", user.id).one("groups", group.id);
_.merge(request, group);
return request;
}
this.saveGroup = function(user, group, callback) {
if(user.id) {
if(!group.route) {
group = this.restangularizeGroup(group, user);
}
group.customOperation(group.id ? "put" : "post").then(function(response) {
callback(response.plain());
})
} else {
this.writeUserToLocalStorage(user);
callback(group);
}
}
this.deleteGroup = function(user, group, callback) {
if(!user.id) {
_.remove(user.groups, group);
this.writeUserToLocalStorage(user);
callback(true);
} else {
Restangular.one("users", user.id).one("groups", group.id).remove()
.then(function(response) {
_.remove(user.groups, group);
callback(true);
})
}
}
this.shareGroup = function(user, group, callback) {
var shareFn = function() {
Restangular.one("users", user.id).one("groups", group.id).one("share").post()
.then(function(response){
var obj = response.plain();
group.notes.forEach(function(note){
note.shared_via_group = true;
});
_.merge(group, {presentation: obj.presentation});
callback(obj);
})
}
if(user.local_encryption_enabled && group.notes.length > 0) {
// decrypt group notes first
var notes = group.notes;
this.decryptNotesWithLocalKey(notes);
this.saveBatchNotes(user, notes, false, function(success){
shareFn();
})
} else {
shareFn();
}
}
this.unshareGroup = function(user, group, callback) {
Restangular.one("users", user.id).one("groups", group.id).one("unshare").post()
.then(function(response){
var obj = response.plain();
_.merge(group, {presentation: obj.presentation});
callback(obj);
})
}
/*
Notes
*/
this.saveBatchNotes = function(user, notes, encryptionEnabled, callback) {
notes = _.cloneDeep(notes);
notes.forEach(function(note){
if(encryptionEnabled && !note.isPublic()) {
note.content = null;
note.name = null;
} else {
note.local_encrypted_content = null;
note.local_eek = null;
}
})
var request = Restangular.one("users", user.id).one("notes/batch_update");
request.notes = notes;
request.put().then(function(response){
var success = response.plain().success;
callback(success);
})
}
this.preprocessNoteForSaving = function(note, user) {
if(user.local_encryption_enabled && !note.pending_share && !note.isPublic()) {
// encrypt
this.encryptSingleNote(note, this.retrieveGk());
note.content = null; // dont send unencrypted content to server
note.name = null;
}
else {
// decrypt
note.local_encrypted_content = null;
note.local_eek = null;
note.local_encryption_scheme = null;
}
}
this.saveNote = function(user, note, callback) {
if(!user.id) {
this.writeUserToLocalStorage(user);
callback(note);
return;
}
var snipCopy = _.cloneDeep(note);
this.preprocessNoteForSaving(snipCopy, user);
var request = Restangular.one("users", user.id).one("notes", note.id);
_.merge(request, snipCopy);
request.customOperation(request.id ? "put" : "post")
.then(function(response) {
var responseObject = response.plain();
responseObject.content = note.content;
responseObject.name = note.name;
_.merge(note, responseObject);
callback(note);
})
.catch(function(response){
callback(null);
})
}
this.deleteNote = function(user, note, callback) {
if(!user.id) {
this.writeUserToLocalStorage(user);
callback(true);
} else {
Restangular.one("users", user.id).one("notes", note.id).remove()
.then(function(response) {
callback(true);
})
}
}
this.shareNote = function(user, note, callback) {
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.name, content: note.content});
request.post().then(function(response){
var obj = response.plain();
_.merge(note, {presentation: obj.presentation});
note.locked = true;
this.writeUserToLocalStorage(user);
callback(note);
}.bind(this))
}
} else {
var shareFn = function(note, callback) {
Restangular.one("users", user.id).one("notes", note.id).one("share").post()
.then(function(response){
var obj = response.plain();
_.merge(note, {presentation: obj.presentation});
callback(note);
})
}
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);
}
}
}
this.unshareNote = function(user, note, callback) {
Restangular.one("users", user.id).one("notes", note.id).one("unshare").post()
.then(function(response){
var obj = response.plain();
_.merge(note, {presentation: obj.presentation});
callback(note);
})
}
this.notesDataFile = function(user) {
var textFile = null;
var makeTextFile = function (text) {
var data = new Blob([text], {type: 'text/json'});
// If we are replacing a previously generated file we need to
// manually revoke the object URL to avoid memory leaks.
if (textFile !== null) {
window.URL.revokeObjectURL(textFile);
}
textFile = window.URL.createObjectURL(data);
// returns a URL you can use as a href
return textFile;
}.bind(this);
// remove irrelevant keys
var notes = _.map(user.filteredNotes(), function(note){
return {
id: note.id,
name: note.name,
content: note.content,
created_at: note.created_at,
modified_at: note.modified_at,
group_id: note.group_id
}
});
return makeTextFile(JSON.stringify(notes, null, 2 /* pretty print */));
}
/*
Merging
*/
this.mergeLocalDataRemotely = function(user, callback) {
var request = Restangular.one("users", user.id).one("merge");
var groups = user.groups;
request.notes = user.notes;
request.notes.forEach(function(note){
if(note.group_id) {
var group = groups.filter(function(group){return group.id == note.group_id})[0];
note.group_name = group.name;
}
})
request.post().then(function(response){
callback();
localStorage.removeItem('user');
})
}
this.filterDummyNotes = function(notes) {
var filtered = notes.filter(function(note){return note.dummy == false || note.dummy == null});
return filtered;
}
this.staticifyObject = function(object) {
return JSON.parse(JSON.stringify(object));
}
this.writeUserToLocalStorage = function(user) {
var saveUser = _.cloneDeep(user);
saveUser.notes = this.filterDummyNotes(saveUser.notes);
saveUser.groups.forEach(function(group){
group.notes = null;
}.bind(this))
this.writeToLocalStorage('user', saveUser);
}
this.writeToLocalStorage = function(key, value) {
localStorage.setItem(key, angular.toJson(value));
}
this.localUser = function() {
var user = JSON.parse(localStorage.getItem('user'));
if(!user) {
user = {notes: [], groups: []};
}
user.shouldMerge = true;
return user;
}
/*
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;
}
return new Note(JSON.parse(draftString));
}
/*
Encrpytion
*/
this.retrieveGk = function() {
if(!this.gk) {
this.gk = localStorage.getItem("gk");
}
return this.gk;
}
this.setGk = function(gk) {
localStorage.setItem('gk', gk);
}
this.clearGk = function() {
localStorage.removeItem("gk");
}
this.encryptSingleNote = function(note, key) {
var ek = null;
if(note.isEncrypted()) {
ek = Neeto.crypto.decryptText(note.local_eek, key);
} else {
ek = Neeto.crypto.generateRandomEncryptionKey();
note.local_eek = Neeto.crypto.encryptText(ek, key);
}
var text = JSON.stringify({name: note.name, content: note.content});
note.local_encrypted_content = Neeto.crypto.encryptText(text, ek);
note.local_encryption_scheme = "1.0";
}
this.encryptNotes = function(notes, key) {
notes.forEach(function(note){
this.encryptSingleNote(note, key);
}.bind(this));
}
this.encryptSingleNoteWithLocalKey = function(note) {
this.encryptSingleNote(note, this.retrieveGk());
}
this.encryptNotesWithLocalKey = function(notes) {
this.encryptNotes(notes, this.retrieveGk());
}
this.decryptSingleNoteWithLocalKey = function(note) {
this.decryptSingleNote(note, this.retrieveGk());
}
this.decryptSingleNote = function(note, key) {
var ek = Neeto.crypto.decryptText(note.local_eek, key);
var obj = JSON.parse(Neeto.crypto.decryptText(note.local_encrypted_content, ek));
note.name = obj.name;
note.content = obj.content;
}
this.decryptNotes = function(notes, key) {
notes.forEach(function(note){
if(note.isEncrypted()) {
this.decryptSingleNote(note, key);
}
}.bind(this));
}
this.decryptNotesWithLocalKey = function(notes) {
this.decryptNotes(notes, this.retrieveGk());
}
this.reencryptAllNotesAndSave = function(user, newKey, oldKey, callback) {
var notes = user.filteredNotes();
notes.forEach(function(note){
if(note.isEncrypted()) {
// first decrypt eek with old key
var ek = Neeto.crypto.decryptText(note.local_eek, oldKey);
// now encrypt ek with new key
note.local_eek = Neeto.crypto.encryptText(ek, newKey);
}
});
this.saveBatchNotes(user, notes, true, function(success) {
callback(success);
}.bind(this));
}
}
});

View File

@@ -0,0 +1,17 @@
angular
.module('app.services')
.directive('mbAutofocus', ['$timeout', function($timeout) {
return {
restrict: 'A',
scope: {
shouldFocus: "="
},
link : function($scope, $element) {
$timeout(function() {
if($scope.shouldFocus) {
$element[0].focus();
}
});
}
}
}]);

View File

@@ -0,0 +1,109 @@
angular
.module('app.services')
.directive('draggable', function() {
return {
scope: {
note: "="
},
link: function(scope, element) {
// this gives us the native JS object
var el = element[0];
el.draggable = true;
el.addEventListener(
'dragstart',
function(e) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('Note', JSON.stringify(scope.note));
this.classList.add('drag');
return false;
},
false
);
el.addEventListener(
'dragend',
function(e) {
this.classList.remove('drag');
return false;
},
false
);
}
}
});
angular
.module('app.services')
.directive('droppable', function() {
return {
scope: {
drop: '&',
bin: '=',
group: "="
},
link: function(scope, element) {
// again we need the native object
var el = element[0];
el.addEventListener(
'dragover',
function(e) {
e.dataTransfer.dropEffect = 'move';
// allows us to drop
if (e.preventDefault) e.preventDefault();
this.classList.add('over');
return false;
},
false
);
var counter = 0;
el.addEventListener(
'dragenter',
function(e) {
counter++;
this.classList.add('over');
return false;
},
false
);
el.addEventListener(
'dragleave',
function(e) {
counter--;
if (counter === 0) {
this.classList.remove('over');
}
return false;
},
false
);
el.addEventListener(
'drop',
function(e) {
// Stops some browsers from redirecting.
if (e.stopPropagation) e.stopPropagation();
this.classList.remove('over');
var binId = this.id;
var note = new Note(JSON.parse(e.dataTransfer.getData('Note')));
scope.$apply(function(scope) {
var fn = scope.drop();
if ('undefined' !== typeof fn) {
fn(e, scope.group, note);
}
});
return false;
},
false
);
}
}
});

View File

@@ -0,0 +1,20 @@
angular
.module('app.services')
.directive('lowercase', function() {
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var lowercase = function(inputValue) {
if (inputValue == undefined) inputValue = '';
var lowercased = inputValue.toLowerCase();
if (lowercased !== inputValue) {
modelCtrl.$setViewValue(lowercased);
modelCtrl.$render();
}
return lowercased;
}
modelCtrl.$parsers.push(lowercase);
lowercase(scope[attrs.ngModel]);
}
};
});

View File

@@ -0,0 +1,15 @@
angular
.module('app.services')
.directive('selectOnClick', ['$window', function ($window) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
element.on('focus', function () {
if (!$window.getSelection().toString()) {
// Required for mobile Safari
this.setSelectionRange(0, this.value.length)
}
});
}
};
}]);

View File

@@ -0,0 +1,17 @@
angular
.module('app.services')
.directive('note', function($timeout) {
return {
restrict: 'E',
controller: 'SingleNoteCtrl',
templateUrl: "frontend/directives/note.html",
scope: {
note: "="
},
}
})
.controller('SingleNoteCtrl', function ($rootScope, $scope, $state, markdownRenderer) {
$scope.renderedContent = function() {
return markdownRenderer.renderHtml(markdownRenderer.renderedContentForText($scope.note.content));
}
});

View File

@@ -0,0 +1,192 @@
/**
* AngularJS directive that simulates the effect of typing on a text editor - with a blinking cursor.
* This directive works as an attribute to any HTML element, and it changes the speed/delay of its animation.
*
* There's also a simple less file included for basic styling of the dialog, which can be overridden.
* The config object also lets the user define custom CSS classes for the modal.
*
* How to use:
*
* Just add the desired text to the 'text' attribute of the element and the directive takes care of the rest.
* The 'text' attribute can be a single string or an array of string. In case an array is passed, the string
* on each index is erased so the next item can be printed. When the last index is reached, that string stays
* on the screen. (So if you want to erase the last string, just push an empty string to the end of the array)
*
* These are the optional preferences:
* - initial delay: set an 'initial-delay' attribute for the element
* - type delay: set a 'type-delay' attribute for the element
* - erase delay: set a 'erase-delay' attribute for the element
* - specify cursor : set a 'cursor' attribute for the element, specifying which cursor to use
* - turn off cursor blinking: set the 'blink-cursor' attribute to "false"
* - cursor blinking speed: set a 'blink-delay' attribute for the element
* - scope callback: pass the desired scope callback as the 'callback-fn' attribute of the element
*
* Note:
* Each time/delay value should be set either on seconds (1s) or milliseconds (1000)
*
* Dependencies:
* The directive needs the css file provided in order to replicate the cursor blinking effect.
*/
angular
.module('app.services').directive('typewrite', ['$timeout', function ($timeout) {
function linkFunction($scope, $element, $attrs) {
var timer = null,
initialDelay = $attrs.initialDelay ? getTypeDelay($attrs.initialDelay) : 200,
typeDelay = $attrs.typeDelay || 200,
eraseDelay = $attrs.eraseDelay || typeDelay / 2,
blinkDelay = $attrs.blinkDelay ? getAnimationDelay($attrs.blinkDelay) : false,
cursor = $attrs.cursor || '|',
blinkCursor = typeof $attrs.blinkCursor !== 'undefined' ? $attrs.blinkCursor === 'true' : true,
currentText,
textArray,
running,
auxStyle;
if ($scope.text) {
if ($scope.text instanceof Array) {
textArray = $scope.text;
currentText = textArray[0];
} else {
currentText = $scope.text;
}
}
if (typeof $scope.start === 'undefined' || $scope.start) {
typewrite();
}
function typewrite() {
timer = $timeout(function () {
updateIt($element, 0, 0, currentText);
}, initialDelay);
}
function updateIt(element, charIndex, arrIndex, text) {
if (charIndex <= text.length) {
updateValue(element, text.substring(0, charIndex) + cursor);
charIndex++;
timer = $timeout(function () {
updateIt(element, charIndex, arrIndex, text);
}, typeDelay);
return;
} else {
charIndex--;
if($scope.iterationCallback) {
$scope.iterationCallback()(arrIndex);
}
// check if it's an array
if (textArray && arrIndex < textArray.length - 1) {
timer = $timeout(function () {
cleanAndRestart(element, charIndex, arrIndex, textArray[arrIndex]);
}, $scope.iterationDelay);
} else {
if ($scope.callbackFn) {
$scope.callbackFn();
}
blinkIt(element, charIndex, currentText);
}
}
}
function blinkIt(element, charIndex) {
var text = element.text().substring(0, element.text().length - 1);
if (blinkCursor) {
if (blinkDelay) {
auxStyle = '-webkit-animation:blink-it steps(1) ' + blinkDelay + ' infinite;-moz-animation:blink-it steps(1) ' + blinkDelay + ' infinite ' +
'-ms-animation:blink-it steps(1) ' + blinkDelay + ' infinite;-o-animation:blink-it steps(1) ' + blinkDelay + ' infinite; ' +
'animation:blink-it steps(1) ' + blinkDelay + ' infinite;';
updateValue(element, text.substring(0, charIndex) + '<span class="blink" style="' + auxStyle + '">' + cursor + '</span>');
} else {
updateValue(element, text.substring(0, charIndex) + '<span class="blink">' + cursor + '</span>');
}
} else {
updateValue(element, text.substring(0, charIndex));
}
}
function cleanAndRestart(element, charIndex, arrIndex, currentText) {
if(charIndex == 0) {
if($scope.prebeginFn) {
$scope.prebeginFn()();
}
}
if (charIndex > 0) {
currentText = currentText.slice(0, -1);
// element.html(currentText.substring(0, currentText.length - 1) + cursor);
updateValue(element, currentText + cursor);
charIndex--;
timer = $timeout(function () {
cleanAndRestart(element, charIndex, arrIndex, currentText);
}, eraseDelay);
return;
} else {
arrIndex++;
currentText = textArray[arrIndex];
timer = $timeout(function () {
updateIt(element, 0, arrIndex, currentText);
}, typeDelay);
}
}
function getTypeDelay(delay) {
if (typeof delay === 'string') {
return delay.charAt(delay.length - 1) === 's' ? parseInt(delay.substring(0, delay.length - 1), 10) * 1000 : +delay;
} else {
return false;
}
}
function getAnimationDelay(delay) {
if (typeof delay === 'string') {
return delay.charAt(delay.length - 1) === 's' ? delay : parseInt(delay.substring(0, delay.length - 1), 10) / 1000;
}
}
function updateValue(element, value) {
if (element.prop('nodeName').toUpperCase() === 'INPUT') {
return element.val(value);
}
return element.html(value);
}
$scope.$on('$destroy', function () {
if (timer) {
$timeout.cancel(timer);
}
});
$scope.$watch('start', function (newVal) {
if (!running && newVal) {
running = !running;
typewrite();
}
});
$scope.$watch('text', function (newVal, oldVal) {
if(!(newVal instanceof Array)) {
currentText = newVal;
typewrite();
}
});
}
return {
restrict: 'A',
link: linkFunction,
replace: true,
scope: {
text: '=',
callbackFn: '&',
iterationCallback: '&',
iterationDelay: '=',
prebeginFn: '&',
start: '='
}
};
}]);

View File

@@ -0,0 +1,48 @@
var Neeto = Neeto || {};
Neeto.crypto = {
generateRandomKey: function() {
return CryptoJS.lib.WordArray.random(256/8).toString();
},
decryptText: function(encrypted_content, key) {
return CryptoJS.AES.decrypt(encrypted_content, key).toString(CryptoJS.enc.Utf8);
},
encryptText: function(text, key) {
return CryptoJS.AES.encrypt(text, key).toString();
},
generateRandomEncryptionKey: function() {
var salt = Neeto.crypto.generateRandomKey();
var passphrase = Neeto.crypto.generateRandomKey();
return CryptoJS.PBKDF2(passphrase, salt, { keySize: 256/32 }).toString();
},
sha256: function(text) {
return CryptoJS.SHA256(text).toString();
},
/** Generates two deterministic 256 bit keys based on one input */
generateAsymmetricKeyPair: function(input, salt) {
var output = CryptoJS.PBKDF2(input, salt, { keySize: 512/32, hasher: CryptoJS.algo.SHA512, iterations: 3000 });
var firstHalf = _.clone(output);
var secondHalf = _.clone(output);
var sigBytes = output.sigBytes/2;
var outputLength = output.words.length;
firstHalf.words = output.words.slice(0, outputLength/2);
secondHalf.words = output.words.slice(outputLength/2, outputLength);
firstHalf.sigBytes = sigBytes;
secondHalf.sigBytes = sigBytes;
return [firstHalf.toString(), secondHalf.toString()];
},
generateEncryptionKeysForUser: function(password, email) {
var keys = Neeto.crypto.generateAsymmetricKeyPair(password, email);
var pw = keys[0];
var gk = keys[1];
return {pw: pw, gk: gk};
}
};

View File

@@ -0,0 +1,21 @@
angular.module('app.services')
.service('markdownRenderer', function ($sce) {
marked.setOptions({
breaks: true,
sanitize: true
});
this.renderedContentForText = function(text) {
if(!text || text.length == 0) {
return "";
}
return marked(text);
}
this.renderHtml = function(html_code) {
return $sce.trustAsHtml(html_code);
};
});

View File

@@ -0,0 +1,20 @@
angular.module('app.services')
.service('serverSideValidation', function ($sce) {
// Show validation errors in form.
this.showErrors = function (formErrors, form) {
angular.forEach(formErrors, function (errors, key) {
if (typeof form[key] !== 'undefined') {
form[key].$setDirty();
form[key].$setValidity('server', false);
form[key].$error.server = $sce.trustAsHtml(errors.join(', '));
}
});
};
// Get validation errors from server response and show them in form.
this.parseErrors = function (response, form) {
if (response.status === 422) {
this.showErrors(response.data, form);
}
};
});

View File

@@ -0,0 +1,5 @@
//= require app/app.services.js
//= require_tree ./app/services
//= require app/app.frontend.js
//= require_tree ./app/frontend