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

3
.bowerrc Normal file
View File

@@ -0,0 +1,3 @@
{
"allow_root": true
}

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# OS & IDE
.DS_Store
# Ignore bundler config.
/.bundle
# Ignore all logfiles and tempfiles.
/log/*
!/log/.keep
/tmp
/app/assets/templates/generated/
# Ignore bower components
/vendor/assets/bower_components/
/node_modules
/vendor/assets/javascripts/app.js
/vendor/assets/javascripts/compiled.js
/vendor/assets/javascripts/compiled.min.js
/vendor/assets/javascripts/lib.js
/vendor/assets/javascripts/templates.js
/vendor/assets/stylesheets/app.css
/vendor/assets/stylesheets/app.css.map
/.sass-cache
# Ignore ENV variables config
.env
.ssh
dump.rdb
# Ignore compiled assets
/public/assets
# Ignore user uploads
/public/uploads/*
!/public/uploads/.keep

31
Capfile Normal file
View File

@@ -0,0 +1,31 @@
# Load DSL and set up stages
require "capistrano/setup"
# Include default deployment tasks
require "capistrano/deploy"
# Include tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
# https://github.com/capistrano/rvm
# https://github.com/capistrano/rbenv
# https://github.com/capistrano/chruby
# https://github.com/capistrano/bundler
# https://github.com/capistrano/rails
# https://github.com/capistrano/passenger
#
require 'capistrano/rvm'
# require 'capistrano/rbenv'
# require 'capistrano/chruby'
require 'capistrano/bundler'
require 'capistrano/rails/assets'
# require 'capistrano/rails/migrations'
require 'capistrano/passenger'
# require 'capistrano/sidekiq'
require 'capistrano/git-submodule-strategy'
# require "whenever/capistrano" # Update crontab on deploy
# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

48
Gemfile Normal file
View File

@@ -0,0 +1,48 @@
source 'https://rubygems.org'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '5.0.0.1'
gem 'sass'
gem "non-stupid-digest-assets"
gem 'uglifier'
gem 'activemodel-serializers-xml'
gem 'rack-cors', :require => 'rack/cors'
gem 'dotenv-rails', '~> 2.1.1'
gem 'bower-rails', '~> 0.10.0'
gem 'devise_token_auth', '~> 0.1.39'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc
# Used for 'respond_to' feature
gem 'responders', '~> 2.0'
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug'
# Access an IRB console on exception pages or by using <%= console %> in views
gem 'web-console', '~> 2.0'
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
gem 'puma'
# Deployment tools
gem 'capistrano'
gem 'capistrano-bundler'
gem 'capistrano-passenger', '>= 0.2.0'
gem 'capistrano-rails'
gem 'capistrano-rvm'
gem 'capistrano-sidekiq'
gem 'capistrano-git-submodule-strategy', '~> 0.1.22'
end

220
Gemfile.lock Normal file
View File

@@ -0,0 +1,220 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (5.0.0.1)
actionpack (= 5.0.0.1)
nio4r (~> 1.2)
websocket-driver (~> 0.6.1)
actionmailer (5.0.0.1)
actionpack (= 5.0.0.1)
actionview (= 5.0.0.1)
activejob (= 5.0.0.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.0.0.1)
actionview (= 5.0.0.1)
activesupport (= 5.0.0.1)
rack (~> 2.0)
rack-test (~> 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.0.0.1)
activesupport (= 5.0.0.1)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
activejob (5.0.0.1)
activesupport (= 5.0.0.1)
globalid (>= 0.3.6)
activemodel (5.0.0.1)
activesupport (= 5.0.0.1)
activemodel-serializers-xml (1.0.1)
activemodel (> 5.x)
activerecord (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
activerecord (5.0.0.1)
activemodel (= 5.0.0.1)
activesupport (= 5.0.0.1)
arel (~> 7.0)
activesupport (5.0.0.1)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
airbrussh (1.1.1)
sshkit (>= 1.6.1, != 1.7.0)
arel (7.1.4)
bcrypt (3.1.11)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bower-rails (0.10.0)
builder (3.2.2)
byebug (9.0.6)
capistrano (3.6.1)
airbrussh (>= 1.0.0)
capistrano-harrow
i18n
rake (>= 10.0.0)
sshkit (>= 1.9.0)
capistrano-bundler (1.2.0)
capistrano (~> 3.1)
sshkit (~> 1.2)
capistrano-git-submodule-strategy (0.1.22)
capistrano (~> 3.1)
capistrano-harrow (0.5.3)
capistrano-passenger (0.2.0)
capistrano (~> 3.0)
capistrano-rails (1.2.0)
capistrano (~> 3.1)
capistrano-bundler (~> 1.1)
capistrano-rvm (0.1.2)
capistrano (~> 3.0)
sshkit (~> 1.2)
capistrano-sidekiq (0.10.0)
capistrano
sidekiq (>= 3.4)
concurrent-ruby (1.0.2)
connection_pool (2.2.1)
debug_inspector (0.0.2)
devise (4.2.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1)
responders
warden (~> 1.2.3)
devise_token_auth (0.1.39)
devise (> 3.5.2, <= 4.2)
rails (< 6)
dotenv (2.1.1)
dotenv-rails (2.1.1)
dotenv (= 2.1.1)
railties (>= 4.0, < 5.1)
erubis (2.7.0)
execjs (2.7.0)
globalid (0.3.7)
activesupport (>= 4.1.0)
i18n (0.7.0)
json (1.8.3)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.4)
mime-types (>= 1.16, < 4)
method_source (0.8.2)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_portile2 (2.1.0)
minitest (5.9.1)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (3.2.0)
nio4r (1.2.1)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
non-stupid-digest-assets (1.0.9)
sprockets (>= 2.0)
orm_adapter (0.5.0)
puma (3.6.2)
rack (2.0.1)
rack-cors (0.4.0)
rack-protection (1.5.3)
rack
rack-test (0.6.3)
rack (>= 1.0)
rails (5.0.0.1)
actioncable (= 5.0.0.1)
actionmailer (= 5.0.0.1)
actionpack (= 5.0.0.1)
actionview (= 5.0.0.1)
activejob (= 5.0.0.1)
activemodel (= 5.0.0.1)
activerecord (= 5.0.0.1)
activesupport (= 5.0.0.1)
bundler (>= 1.3.0, < 2.0)
railties (= 5.0.0.1)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.1)
activesupport (>= 4.2.0, < 6.0)
nokogiri (~> 1.6.0)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
railties (5.0.0.1)
actionpack (= 5.0.0.1)
activesupport (= 5.0.0.1)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rake (11.3.0)
rdoc (4.3.0)
redis (3.3.2)
responders (2.3.0)
railties (>= 4.2.0, < 5.1)
sass (3.4.22)
sdoc (0.4.2)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
sidekiq (4.2.7)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
spring (2.0.0)
activesupport (>= 4.2)
sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.0)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sshkit (1.11.4)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
thor (0.19.4)
thread_safe (0.3.5)
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (3.0.3)
execjs (>= 0.3.0, < 3)
warden (1.2.6)
rack (>= 1.0)
web-console (2.3.0)
activemodel (>= 4.0)
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
sprockets-rails (>= 2.0, < 4.0)
websocket-driver (0.6.4)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
PLATFORMS
ruby
DEPENDENCIES
activemodel-serializers-xml
bower-rails (~> 0.10.0)
byebug
capistrano
capistrano-bundler
capistrano-git-submodule-strategy (~> 0.1.22)
capistrano-passenger (>= 0.2.0)
capistrano-rails
capistrano-rvm
capistrano-sidekiq
devise_token_auth (~> 0.1.39)
dotenv-rails (~> 2.1.1)
non-stupid-digest-assets
puma
rack-cors
rails (= 5.0.0.1)
responders (~> 2.0)
sass
sdoc (~> 0.4.0)
spring
uglifier
web-console (~> 2.0)
BUNDLED WITH
1.13.6

149
Gruntfile.js Normal file
View File

@@ -0,0 +1,149 @@
module.exports = function(grunt) {
grunt.initConfig({
watch: {
haml: {
files: ['app/assets/templates/**/*.haml'],
tasks: ['newer:haml', 'ngtemplates', 'concat'],
options: {
spawn: false,
},
},
js: {
files: ['app/assets/javascripts/**/*.js'],
tasks: ['concat'],
options: {
spawn: false,
},
},
css: {
files: ['app/assets/stylesheets/**/*.scss'],
tasks: ['sass', 'concat'],
options: {
spawn: false,
},
}
},
sass: {
dist: {
options: {
style: 'expanded'
},
files: {
'vendor/assets/stylesheets/app.css': 'app/assets/stylesheets/frontend.css.scss'
}
}
},
haml: {
dist: {
expand: true,
ext: '.html',
extDot: 'last',
src: ['app/assets/templates/**/*.haml'],
dest: 'app/assets/templates/generated/',
rename: function (dest, src) {
return dest + src.replace(".html", "");
}
},
},
ngtemplates: {
templates: {
cwd: 'app/assets/templates/generated/app/assets/templates',
src: ['**/*.html'],
dest: 'vendor/assets/javascripts/templates.js',
options: {
module: 'app.frontend'
}
}
},
concat: {
options: {
separator: ';',
},
app: {
src: [
'app/assets/javascripts/app/*.js',
'app/assets/javascripts/app/frontend/*.js',
'app/assets/javascripts/app/frontend/controllers/*.js',
'app/assets/javascripts/app/frontend/models/*.js',
'app/assets/javascripts/app/services/*.js',
'app/assets/javascripts/app/services/directives/*.js',
'app/assets/javascripts/app/services/helpers/*.js',
],
dest: 'vendor/assets/javascripts/app.js',
},
lib: {
src: [
'vendor/assets/bower_components/angular/angular.js',
'vendor/assets/bower_components/angular-ui-router/release/angular-ui-router.js',
'vendor/assets/bower_components/ng-token-auth/dist/ng-token-auth.js',
'vendor/assets/bower_components/angular-cookie/angular-cookie.js',
'vendor/assets/bower_components/lodash/dist/lodash.js',
'vendor/assets/bower_components/restangular/dist/restangular.js',
'vendor/assets/bower_components/marked/lib/marked.js',
'vendor/assets/bower_components/oclazyload/dist/ocLazyLoad.js',
'vendor/assets/bower_components/angular-lazy-img/release/angular-lazy-img.js',
'vendor/assets/bower_components/ng-dialog/js/ngDialog.min.js',
'vendor/assets/javascripts/crypto/*.js'
],
dest: 'vendor/assets/javascripts/lib.js',
},
dist: {
src: ['vendor/assets/javascripts/lib.js', 'vendor/assets/javascripts/app.js', 'vendor/assets/javascripts/templates.js'],
dest: 'vendor/assets/javascripts/compiled.js',
},
css: {
src: [
'vendor/assets/stylesheets/app.css',
'vendor/assets/bower_components/ng-dialog/css/ngDialog.css',
'vendor/assets/bower_components/ng-dialog/css/ngDialog-theme-default.css',
'vendor/assets/bower_components/angular-typewrite/dist/angular-typewrite.css',
],
dest: 'vendor/assets/stylesheets/app.css'
}
},
ngAnnotate: {
options: {
singleQuotes: true,
},
neeto: {
files: {
'vendor/assets/javascripts/compiled.js': 'vendor/assets/javascripts/compiled.js',
},
}
},
uglify: {
compiled: {
src: ['vendor/assets/javascripts/compiled.js'],
dest: 'vendor/assets/javascripts/compiled.min.js'
}
},
});
grunt.loadNpmTasks('grunt-newer');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-haml2html');
grunt.loadNpmTasks('grunt-angular-templates');
grunt.loadNpmTasks('grunt-contrib-cssmin');
grunt.loadNpmTasks('grunt-contrib-concat');
grunt.loadNpmTasks('grunt-contrib-sass');
grunt.loadNpmTasks('grunt-ng-annotate');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.registerTask('default', ['haml', 'ngtemplates', 'sass', 'concat', 'ngAnnotate', 'uglify']);
};

28
README.rdoc Normal file
View File

@@ -0,0 +1,28 @@
== README
This README would normally document whatever steps are necessary to get the
application up and running.
Things you may want to cover:
* Ruby version
* System dependencies
* Configuration
* Database creation
* Database initialization
* How to run the test suite
* Services (job queues, cache servers, search engines, etc.)
* Deployment instructions
* ...
Please feel free to use a different markup language if you do not plan to run
<tt>rake doc:app</tt>.

6
Rakefile Normal file
View File

@@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
Rails.application.load_tasks

0
app/assets/images/.keep Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
app/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

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

View File

@@ -0,0 +1,142 @@
.nt-dropdown-menu {
border-radius: 0;
padding: 0 0;
margin-top: 15px;
border: none;
width: 280px;
li {
height: 35px;
overflow: hidden;
// padding-top: 2px;
.text {
padding: 10px;
padding-top: 7px;
height: 100%;
height: 100%;
float: left;
}
.shortcut {
float: right;
font-size: 12px;
font-weight: normal;
opacity: 0.5;
margin-top: 10px;
padding-right: 10px;
}
}
}
.nt-dropdown-menu.dark {
background-color: $dark-gray;
color: white;
li {
&:hover {
background-color: #f5f5f5;
color: black;
a {
color: black !important;
}
}
a {
color: white !important;
height: 100%;
font-weight: bold !important;
}
.text {
color: white;
}
}
}
.nt-dropdown-menu.light {
background-color: white;
color: black;
li {
&:hover {
background-color: $dark-gray;
color: white;
a {
color: white;
}
}
a {
color: black;
}
.text {
color: black;
}
}
}
.menu-right-container {
float: right;
margin-top: 3px;
color: white;
white-space: nowrap;
width: 70%;
overflow: hidden;
display: inline-block;
a {
color: white;
// font-size: 12px;
}
.public-link {
font-weight: bold;
font-size: 12px;
height: 20px;
text-align: right;
text-overflow: ellipsis;
overflow: hidden;
.url {
text-align: right;
.icon {
margin-right: 4px;
}
a {
color: white;
text-decoration: none;
overflow: hidden;
white-space: nowrap;
width: 90%;
text-overflow: ellipsis;
&:hover {
text-decoration: underline;;
}
}
}
.edit-url {
// float: left;
border: none;
outline: none;
margin-top: -3px;
}
}
> .icon {
float: left;
position: relative;
}
.info-panel {
min-width: 255px;
text-align: left;
font-size: 14px;
margin-right: 18px;
margin-bottom: 18px;
font-weight: normal;
}
}

View File

@@ -0,0 +1,239 @@
.editor {
width: 50%;
&.fullscreen {
width: 100%;
position: absolute;
left: 0px;
top: 0px;
z-index: 200;
padding: 0;
}
.section-title-bar {
border-bottom: none !important;
}
$heading-height: 100px;
.editor-heading {
position: absolute;
width: 100%;
padding: 15px;
padding-top: 0px;
background-color: white;
min-height: 100px;
width: 100%;
padding-right: 10px;
> .title {
font-size: 18px;
font-weight: bold;
padding-top: 0px;
width: 100%;
> .input {
float: left;
text-overflow:ellipsis;
width: 90%;
font-weight: bold;
border: none;
outline: none;
background-color: transparent;
&:disabled {
color: black;
}
}
}
.save-status {
float: right;
font-size: 12px;
text-transform: none;
font-weight: normal;
margin-top: -18px;
width: 120px;
text-align: right;
color: rgba(black, 0.5);
}
}
.editor-content {
max-height: 100%;
height: 100%;
clear: both;
min-width: 0;
padding-left: 10px;
padding-right: 10px;
overflow: auto;
padding-top: $heading-height;
.sampler-container {
margin-top: 10px;
padding: 15px;
padding-top: 17px;
font-size: 17px;
// opacity: 0.5;
}
.sampler {
// opacity: 0.5;
color: rgba(black, 0.3);
}
.editable {
font-family: monospace;
max-height: 100%;
height: 100%;
width: 100%;
border: none;
outline: none;
padding: 15px;
padding-top: 17px;
font-size: 17px;
resize: none;
}
.preview {
// font-family: monospace;
max-height: 100%;
height: 100%;
line-height: 23px;
overflow-y: scroll;
padding: 0px 15px;
}
}
}
.markdown {
margin-top: 6px;
margin-left: 15px;
float: right;
text-align: right;
right: 0;
}
.full-screen-button {
cursor: pointer;
position: absolute;
top: 25px;
right: 20px;
z-index: 100;
}
.editor-menu {
padding-top: 0px;
width: 100%;
position: absolute;
padding-left: inherit;
padding-right: inherit;
left: 0;
right: 0;
bottom: 0px;
background-color: $dark-gray;
color: white;
padding-top: 8px;
height: 36px;
cursor: default;
ul {
li {
text-align: left;
a {
font-size: 14px;
font-weight: bold;
padding: 0 0;
}
}
}
}
ol {
list-style-type: decimal;
list-style-position: inside;
-webkit-margin-before: 1em;
-webkit-margin-after: 1em;
-webkit-margin-start: 0px;
-webkit-margin-end: 0px;
-webkit-padding-start: 0px;
}
.nav-tabs {
a {
color: black;
text-decoration: none;
}
a {
background-color: transparent;
}
}
.nav {
padding-left: 0;
margin-bottom: 0;
list-style: none;
}
ol, ul {
margin-top: 0;
margin-bottom: 10px;
}
.nav-pills>li {
float: left;
}
.nav>li {
position: relative;
display: block;
}
.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover {
color: #fff;
background-color: #337ab7;
}
.nav-pills>li>a {
border-radius: 4px;
}
.nav>li>a {
position: relative;
display: block;
padding: 10px 15px;
}
.nav-tabs>li {
float: left;
margin-bottom: -1px;
}
.nav>li {
position: relative;
display: block;
}
.nav-tabs>li.active>a, .nav-tabs>li.active>a:focus, .nav-tabs>li.active>a:hover {
color: black;
cursor: pointer;
background-color: #fff;
border: 1px solid #ddd;
border-bottom-color: transparent;
}
.nav-tabs>li>a {
margin-right: 2px;
line-height: 1.42857143;
border: 1px solid transparent;
// border-radius: 4px 4px 0 0;
cursor: pointer;
}
.nav>li>a {
position: relative;
display: block;
padding: 10px 15px;
}

View File

@@ -0,0 +1,92 @@
@font-face {
font-family: ProximaNova;
src: font-url('ProximaNova/ProximaNova-Regular.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Regular.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Regular.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Regular.svg') format('svg');
}
@font-face {
font-family: ProximaNova;
font-style: italic;
src: font-url('ProximaNova/ProximaNova-Regular-Italic.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Regular-Italic.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Regular-Italic.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Regular-Italic.svg') format('svg');
}
@font-face {
font-family: ProximaNova;
font-weight: 600;
src: font-url('ProximaNova/ProximaNova-Semibold.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Semibold.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Semibold.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Semibold.svg') format('svg');
}
@font-face {
font-family: ProximaNova;
font-weight: 600;
font-style: italic;
src: font-url('ProximaNova/ProximaNova-Semibold-Italic.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Semibold-Italic.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Semibold-Italic.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Semibold-Italic.svg') format('svg');
}
@font-face {
font-family: ProximaNova;
font-weight: bold;
src: font-url('ProximaNova/ProximaNova-Bold.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Bold.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Bold.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Bold.svg') format('svg');
}
@font-face {
font-family: ProximaNova;
font-weight: bold;
font-style: italic;
src: font-url('ProximaNova/ProximaNova-Bold-Italic.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Bold-Italic.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Bold-Italic.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Bold-Italic.svg') format('svg');
}
@font-face {
font-family: ProximaNova;
font-weight: 100;
src: font-url('fonts/ProximaNova-Thin.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Thin.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Thin.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Thin.svg') format('svg');
}
@font-face {
font-family: ProximaNova;
font-weight: 100;
font-style: italic;
src: font-url('fonts/ProximaNova-Thin-Italic.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Thin-Italic.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Thin-Italic.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Thin-Italic.svg') format('svg');
}
@font-face {
font-family: ProximaNova;
font-weight: 900;
src: font-url('ProximaNova/ProximaNova-Extrabold.eot');
src: local(''),
font-url('ProximaNova/ProximaNova-Extrabold.woff') format('woff'),
font-url('ProximaNova/ProximaNova-Extrabold.ttf') format('truetype'),
font-url('ProximaNova/ProximaNova-Extrabold.svg') format('svg');
}

View File

@@ -0,0 +1,68 @@
.groups {
width: 22%;
.groups-title-bar {
color: #0707ff;
}
.group {
height: 50px;
border-bottom: 1px solid $bg-color;
padding: 12px;
cursor: pointer;
background-color: white;
transition: height .1s ease-in-out;
position: relative;
> .icon {
float: left;
padding-top: 6px;
margin-right: 5px;
}
> .title {
width: 80%;
background-color: transparent;
font-weight: 600;
float: left;
color: $main-text-color;
border: none;
cursor: pointer;
text-overflow: ellipsis;
width: 75%;
}
> .count {
position: absolute;
right: 17px;
padding-top: 3px;
}
&.selected {
background-color: #5151ff;
color: white;
> .title {
color: white;
cursor: text;
}
}
/* When a note is dragged over group */
&.over {
background-color: rgba(#5151ff, 0.8);
color: white;
border: 2px dashed white;
> .title {
color: white;
}
}
&:hover:not(.selected) {
background-color: rgba(#5151ff, 0.8);
color: white;
> .title {
color: white;
}
}
}
}

View File

@@ -0,0 +1,292 @@
.header {
position: relative;
width: 100%;
background-color: $bg-color;
height: $header-height;
max-height: $header-height;
z-index: 100;
font-size: 14px;
color: $dark-gray;
// padding-top: 5px;
margin-top: 4px;
a {
color: $dark-gray;
}
}
.header-content {
margin-bottom: 0px;
padding-top: 0px;
border-radius: 0px;
left: 0px;
right: 0px;
.header-name {
display: inline-block;
vertical-align: middle;
font-weight: bold;
font-size: 20px;
padding-top: 12px;
}
.tagline {
display: inline-block;
vertical-align: middle;
font-size: 14px;
font-weight: normal;
padding-left: 11px;
margin-left: 0px;
height: 22px;
margin-top: 2px;
}
}
.panel-status-text {
margin-top: 20px;
font-style: italic;
font-size: 14px;
}
.menu {
float: right;
padding-top: 5px;
margin-top: 10px;
color: black;
z-index: 1000;
margin-bottom: 0px;
font-size: 18px;
.login-panel .login-input {
border-radius: 0px;
}
.items {
.advanced-brand {
font-size: 18px;
&.btn {
border-radius: 3px;
font-size: 14px;
}
font-weight: bold;
.n {
font-style: italic;
}
.advanced {
margin-left: -4px;
text-transform: uppercase;
font-style: normal;
}
.beta {
font-size: 7px;
font-style: normal;
}
}
.item {
display: inline-block;
margin-right: 7px;
position: relative;
cursor: pointer;
a {
color: $dark-gray;
}
.panel {
position: absolute;
right: 0px;
min-width: 300px;
z-index: 1000;
margin-top: 15px;
box-shadow: 0px 0px 15px rgba(black, 0.2);
border: none;
cursor: default;
max-height: 85vh;
overflow: scroll;
background-color: white;
.storage-text {
font-size: 14px;
}
.checkbox {
font-size: 14px;
font-weight: normal;
margin-left: auto;
margin-right: auto;
}
}
}
}
}
.half-button {
$spacing: 2px;
width: calc(50% - #{$spacing});
margin-left: $spacing/2.0;
margin-right: $spacing/2.0;
float: left;
}
.item.account {
.link-item {
margin-bottom: 8px;
a {
font-size: 14px;
color: #00228f;
font-weight: bold;
}
}
input {
border-radius: 0px;
}
.account-panel {
padding: 12px;
padding-bottom: 6px;
.account-items {
margin-top: 0px;
}
.account-item {
width: 100%;
margin-bottom: 34px;
a {
color: #00228f;
font-weight: bold;
cursor: pointer;
}
> .icon-container {
display: block;
margin-bottom: 10px;
}
> .meta-container {
display: block;
}
> .action-container {
font-size: 14px;
margin-top: 6px;
.status-title {
font-weight: bold;
}
.subtext {
font-size: 12px;
margin-top: 2px;
}
}
.encryption-confirmation {
position: relative;
.buttons {
.cancel {
font-weight: normal;
margin-right: 3px;
}
}
}
&:last-child {
margin-bottom: 8px !important;
}
> .icon-container {
margin-bottom: 10px;
.icon {
height: 35px;
&.archive {
height: 30px;
}
}
}
.meta-container {
> .title {
font-size: 16px;
font-weight: bold;
}
> .desc {
font-size: 14px;
margin-top: 3px;
line-height: 18px;
}
}
}
.membership-settings {
font-size: 14px;
}
}
}
.account-form {
margin-top: 10px;
}
.registration-login {
.login-forgot {
margin-top: 20px;
clear: both;
a {
display: block;
font-size: 13px !important;
text-align: center;
}
}
}
.account-menu {
width: 200px;
}
.faq-panel {
width: 350px;
font-size: 16px;
z-index: 1000;
p {
font-size: 16px;
}
.faq-item {
margin-bottom: 22px;
}
.question {
margin-bottom: 12px;
}
.answer {
margin-top: 8px;
}
ul {
li {
margin-bottom: 10px;
}
}
}

View File

@@ -0,0 +1,181 @@
$main-text-color: black;
$secondary-text-color: rgba($main-text-color, 0.8);
$bg-color: #e3e3e3;
@mixin MQ-Small() {
@media (max-width: $screen-xs-max) {
@content;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
@content;
}
}
@mixin MQ-Medium() {
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
@content;
}
}
@mixin MQ-Large() {
@media (min-width: $screen-lg-min) {
@content;
}
}
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
}
html,
body {
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
color: $main-text-color;
-webkit-font-smoothing: antialiased;
min-height: 100%;
height: 100%;
font-size: 20px;
margin: 0;
}
.dark-button {
background-color: #2e2e2e;
border: 0;
padding: 6px 18px;
font-size: 16px;
cursor: pointer;
color: #fff;
border-radius: 2px;
border: 1px solid transparent;
-webkit-appearance: none;
-webkit-font-smoothing: antialiased;
-webkit-tap-highlight-color: transparent;
&:hover {
background-color: black;
}
}
a {
text-decoration: none;
&:hover {
text-decoration: underline;;
}
}
pre {
padding: 16px;
overflow: auto;
line-height: 1.45;
background-color: #f7f7f7;
border-radius: 3px;
}
code {
word-wrap: break-word;
line-height: 1.45;
}
p {
overflow: auto;
}
.main-ui-view {
min-height: 100vh;
height: 100vh;
overflow: auto;
}
.app-body-class {
height: 100%;
background-color: $bg-color;
min-width: 100px;
overflow: auto;
min-width: 900px;
}
$header-height: 50px;
.app-container {
display: table;
background-color: $bg-color;
width: 100%;
height: calc(100% - #{$header-height});
padding: 15px;
padding-top: 0px;
font-size: 0;
margin-top: 0;
}
$section-header-height: 70px;
.app {
height: 100%;
width: 100%;
display: table-row;
vertical-align: top;
.light-button {
background-color: $bg-color;
font-weight: bold;
color: $main-text-color;
font-size: 16px;
text-align: center;
height: 35px;
border-radius: 4px;
padding-top: 6px;
&:hover {
background-color: #cdcdcd;
}
}
.section {
padding: 8px;
padding-bottom: 0px;
display: block;
height: 100%;
float: left;
overflow-y: scroll;
overflow-x: hidden;
min-width: 0;
font-size: 17px;
> .content {
height: 100%;
background-color: white;
position: relative;
box-shadow: 0px 0px 2px rgba(gray, 0.3);
}
.section-title-bar {
padding: 20px;
height: $section-header-height;
font-weight: bold;
border-bottom: 1px solid $bg-color;
> .title {
float: left;
text-transform: uppercase;
white-space: nowrap;
text-overflow: ellipsis;
width: 85%;
overflow: hidden;
}
> .add-button {
float: right;
font-size: 30px;
margin-top: -10px;
cursor: pointer;
}
}
}
}

View File

@@ -0,0 +1,410 @@
//== Media queries breakpoints
//
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
// Extra small screen / phone
//** Deprecated `$screen-xs` as of v3.0.1
$screen-xs: 480px !default;
//** Deprecated `$screen-xs-min` as of v3.2.0
$screen-xs-min: $screen-xs !default;
//** Deprecated `$screen-phone` as of v3.0.1
$screen-phone: $screen-xs-min !default;
// Small screen / tablet
//** Deprecated `$screen-sm` as of v3.0.1
$screen-sm: 768px !default;
$screen-sm-min: $screen-sm !default;
//** Deprecated `$screen-tablet` as of v3.0.1
$screen-tablet: $screen-sm-min !default;
// Medium screen / desktop
//** Deprecated `$screen-md` as of v3.0.1
$screen-md: 992px !default;
$screen-md-min: $screen-md !default;
//** Deprecated `$screen-desktop` as of v3.0.1
$screen-desktop: $screen-md-min !default;
// Large screen / wide desktop
//** Deprecated `$screen-lg` as of v3.0.1
$screen-lg: 1200px !default;
$screen-lg-min: $screen-lg !default;
//** Deprecated `$screen-lg-desktop` as of v3.0.1
$screen-lg-desktop: $screen-lg-min !default;
// So media queries don't overlap when required, provide a maximum
$screen-xs-max: ($screen-sm-min - 1) !default;
$screen-sm-max: ($screen-md-min - 1) !default;
$screen-md-max: ($screen-lg-min - 1) !default;
@mixin MQ-Xsmall() {
@media (max-width: $screen-xs-max) {
@content;
}
}
@mixin MQ-Small() {
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
@content;
}
}
@mixin MQ-Medium() {
@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
@content;
}
}
@mixin MQ-Large() {
@media (min-width: $screen-lg-min) {
@content;
}
}
* {
box-sizing: border-box;
}
*:focus {outline:0;}
.navbar {
min-height: 0px !important;
background-color: white;
height: 80px;
margin-bottom: 0px;
padding-top: 10px;
border-radius: 0px;
}
@media (min-width: 768px) {
.navbar {
border-radius: 4px;
}
}
.navbar {
position: relative;
min-height: 50px;
margin-bottom: 20px;
border: 1px solid transparent;
}
@media (min-width: 768px) {
.navbar-header {
float: left;
}
}
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
margin-right: 0;
margin-left: 0;
}
@media (min-width: 768px) {
.navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand {
margin-left: -15px;
}
}
.navbar-brand {
float: left;
padding: 15px 15px;
font-size: 18px;
line-height: 20px;
height: 50px;
}
@media (min-width: 768px) {
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
margin-right: 0;
margin-left: 0;
}
}
.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
margin-right: -15px;
margin-left: -15px;
}
@media (min-width: 768px) {
.navbar-collapse.collapse {
display: block !important;
height: auto !important;
padding-bottom: 0;
overflow: visible !important;
}
}
@media (min-width: 768px) {
.navbar-collapse {
width: auto;
border-top: 0;
box-shadow: none;
}
}
.navbar-collapse {
overflow-x: visible;
padding-right: 15px;
padding-left: 15px;
border-top: 1px solid transparent;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
-webkit-overflow-scrolling: touch;
}
.collapse {
display: none;
}
@media (min-width: 768px) {
.navbar-right {
float: right !important;
margin-right: -15px;
}
}
@media (min-width: 768px) {
.navbar-text {
float: left;
margin-left: 15px;
margin-right: 15px;
}
}
.navbar-text {
margin-top: 15px;
margin-bottom: 15px;
}
.dropup, .dropdown {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
// display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
font-size: 14px;
text-align: left;
background-color: #fff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
}
.dropdown-menu>li>a {
display: block;
padding: 3px 20px;
clear: both;
font-weight: 400;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
}
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
text-decoration: none;
cursor: pointer;
}
.caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 2px;
vertical-align: middle;
border-top: 4px dashed;
border-top: 4px solid\9;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
button:focus {outline:0;}
.dropdown-menu-right {
left: auto;
right: 0;
}
.open > .dropdown-menu {
display: block;
}
.btn {
display: inline-block;
margin-bottom: 0;
font-weight: normal;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
border: 1px solid transparent;
white-space: nowrap;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857;
border-radius: 4px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.btn-block {
display: block;
width: 100%;
}
// ul, menu, dir {
// display: block;
// list-style-type: disc;
// -webkit-margin-before: 1em;
// -webkit-margin-after: 1em;
// -webkit-margin-start: 0px;
// -webkit-margin-end: 0px;
// -webkit-padding-start: 40px;
// }
.dropdown-menu .divider {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5;
}
.panel {
position: absolute;
right: 0px;
min-width: 300px;
z-index: 1000;
margin-top: 10px;
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.2);
border: none;
background-color: white;
}
.panel-top {
bottom: 0px;
}
.panel-left {
left: -50px;
}
.panel-centered {
position: relative;
width: 400px;
margin: 0 auto;
padding: 10px;
text-align: center;
}
.panel-body {
padding: 15px;
}
.form-control {
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857;
color: #555555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
input, button, select, textarea {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.has-feedback {
position: relative;
}
.form-group {
margin-bottom: 15px;
}
.checkbox {
font-size: 14px;
font-weight: bold;
margin-left: auto;
margin-right: auto;
margin-bottom: 10px;
}
.btn-link {
background-color: transparent;
-webkit-box-shadow: none;
box-shadow: none;
text-decoration: none;
}
.btn-link:hover, .btn-link:focus {
color: #23527c;
text-decoration: underline;
background-color: transparent;
}
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated-fast {
-webkit-animation-duration: 0.5s;
animation-duration: 0.5s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.fadeInDown {
-webkit-animation-name: fadeInDown;
animation-name: fadeInDown;
}
.fadeIn {
-webkit-animation-name: fadeIn;
animation-name: fadeIn;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeInDown {
0% {
opacity: 0;
-webkit-transform: translate3d(0,-100%,0);
transform: translate3d(0,-100%,0);
}
100% {
opacity: 1;
-webkit-transform: none;
transform: none;
}
}

View File

@@ -0,0 +1,78 @@
.notes {
width: 28%;
.notes-title-bar {
color: #ff6551;
height: 136px !important;
}
.group-menu-bar {
position: relative;
margin: 0 -20px;
width: auto;
margin-top: 14px;
}
.filter-section {
clear: left;
height: 32px;
margin-top: 14px;
.filter-bar {
background-color: $bg-color;
border-radius: 4px;
height: 100%;
color: #909090;
text-align: center;
font-weight: normal;
font-size: 16px;
line-height: 35px;
border: none;
width: 100%;
}
}
.notes-footer {
border-top: 1px solid $bg-color;
position: absolute;
bottom: 0px;
margin-top: 1px solid $bg-color;
width: 100%;
text-align: center;
padding: 10px;
> .new-button {
}
}
.note {
width: 100%;
padding: 15px;
height: 70px;
border-bottom: 1px solid $bg-color;
cursor: pointer;
background-color: white;
> .name {
font-weight: 600;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
> .date {
font-size: 12px;
}
&.selected {
background-color: #ff6551;
color: white;
}
&:hover:not(.selected) {
background-color: rgba(#ff6551, 0.8);
color: white;
}
}
}

View File

@@ -0,0 +1,63 @@
$dark-gray: #2e2e2e;
@import "app/mostrap";
@import "app/common";
@import "app/main";
@import "app/header";
@import "app/groups";
@import "app/notes";
@import "app/editor";
@font-face {
font-family: 'icomoon';
src: url('icomoon/icomoon.eot');
src: url('icomoon/icomoon.eot') format('embedded-opentype'),
url('icomoon/icomoon.ttf') format('truetype'),
url('icomoon/icomoon.woff') format('woff'),
url('icomoon/icomoon.svg') format('svg');
font-weight: normal;
font-style: normal;
}
[class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
// line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 10px;
}
.inline-icon {
display: inline-block;
// margin-right: -5px;
margin-left: 2px;
}
.icon-lock:before {
content: "\e98f";
}
.icon-rss:before {
content: "\ea9c";
}
.icon-markdown:before {
content: "\e901";
}
.icon-keyboard:before {
content: "\e900";
}
.icon-enlarge:before {
content: "\e989";
}

View File

@@ -0,0 +1,16 @@
.panel.panel-default
.panel-heading
%h3.panel-title Forgot Your Password?
.panel-body
%p We'll send reset instructions to your email.
%form{'ng-submit' => 'requestPasswordReset(forgotData)', 'ng-init' => 'forgotData = {}'}
.form-group.has-feedback
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'forgotData.email'}/
%span.glyphicon.glyphicon-envelope.form-control-feedback
.row
.col-sm-8.hidden-xs
%a.btn.btn-link{'ui-sref' => 'auth.login'} Go back to the login page
.col-sm-4
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Reset Password
.col-xs-12.visible-xs
%a.btn.btn-link.btn-block{'ui-sref' => 'auth.login'} Go back to the login page

View File

@@ -0,0 +1,18 @@
.panel.panel-default
.panel-heading
%h3.panel-title Sign in to start your session
.panel-body
%form{'ng-submit' => 'submitLogin(loginData)', 'ng-init' => 'loginData = {}'}
.form-group.has-feedback
%input.form-control{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Username', :required => true, :type => 'email', 'ng-model' => 'loginData.email'}/
%span.glyphicon.glyphicon-user.form-control-feedback
.form-group.has-feedback
%input.form-control{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'loginData.password'}/
%span.glyphicon.glyphicon-lock.form-control-feedback
.row
.col-sm-8.hidden-xs
%a.btn.btn-link{'ui-sref' => 'auth.forgot'} I forgot my password
.col-sm-4
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Sign in
.col-xs-12.visible-xs
%a.btn.btn-link.btn-block{'ui-sref' => 'auth.forgot'} I forgot my password

View File

@@ -0,0 +1,15 @@
.panel.panel-default.panel-centered
.panel-heading
%h3.panel-title Reset Password
.panel-body
%p Type your new password.
%form{'ng-submit' => 'resetPasswordSubmit()'}
.form-group.has-feedback
%input.form-control{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'resetData.password'}/
%span.glyphicon.glyphicon-lock.form-control-feedback
.form-group.has-feedback
%input.form-control{:placeholder => 'Password confirmation', :name => 'password_confirmation', :required => true, :type => 'password', 'ng-model' => 'resetData.password_confirmation'}/
%span.glyphicon.glyphicon-lock.form-control-feedback
.row
.col-sm-4.col-sm-offset-8
%button.btn.btn-main.btn-block.btn-flat{:type => 'submit'} Update Password

View File

@@ -0,0 +1,6 @@
.login-box.margin-auto{"style" => "margin-top: 150px;"}
%uib-alert{:type => '{{data.authAlert.type}}', :close => 'data.authAlert = null', 'ng-if' => 'data.authAlert'}
{{data.authAlert.msg}}
%ui-view

View File

@@ -0,0 +1,61 @@
.section.editor{"ng-class" => "{'fullscreen' : ctrl.fullscreen}"}
.content
.section-title-bar.editor-heading{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
.title
%input.input#note-title-editor{"ng-model" => "ctrl.note.name", "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}}
.editor-menu
%ul.nav.nav-pills
%li.dropdown
%a.dropdown-toggle{"ng-click" => "ctrl.clickedMenu()"}
File
%span.caret{"ng-if" => "!ctrl.note.locked"}
%span{"ng-if" => " ctrl.note.locked"}
.inline-icon.icon-lock
%span.sr-only
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu && !ctrl.note.locked"}
-# %li
-# %a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.saveNote($event, note)"} Save
-# .shortcut Cmd + S
%li
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleFullScreen()"} Toggle Fullscreen
.shortcut Cmd + O
%li
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.toggleMarkdown()"} Toggle Markdown Preview
.shortcut Cmd + M
%li{"ng-if" => "!ctrl.note.hasEnabledPresentation()"}
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.shareNote()"} Share
%li{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.editUrlPressed()"} Edit URL
%li{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.unshareNote()"} Unshare
%li
%a.text{"ng-click" => "ctrl.deleteNote()"} Delete
.markdown.icon{"ng-if" => "ctrl.editorMode == 'preview'", "ng-click" => "ctrl.showMarkdown = !ctrl.showMarkdown"}
.icon-markdown
.panel.panel-default.info-panel{"ng-if" => "ctrl.showMarkdown"}
.panel-body{"style" => "text-align: center; color: black;"}
This editor is Markdown enabled.
.menu-right-container
.public-link{"ng-if" => "ctrl.note.hasEnabledPresentation()"}
%a.url{"ng-if" => "!ctrl.editingUrl", "href" => "{{ctrl.publicUrlForNote(ctrl.note)}}", "target" => "_blank"}
%span.icon-rss.icon
{{ctrl.publicUrlForNote(note)}}
.edit-url{"ng-if" => "ctrl.editingUrl"}
{{ctrl.url.base}}
%input.input{"ng-model" => "ctrl.url.token", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveUrl($event)",
"ng-disabled" => "ctrl.note.locked", "ng-change" => "ctrl.urlChanged()", "ng-focus" => "ctrl.onUrlFocus()",
"select-on-click" => "true", "autofocus" => "true"}
.editor-content{"ng-class" => "{'shared' : ctrl.note.isPublic() }"}
.sampler-container{"ng-if" => "ctrl.showSampler", "ng-click" => "ctrl.focusEditor()"}
%strong.name-sampler.sampler{"typewrite" => "true", "text" => "ctrl.demoNoteNames", "type-delay" => "30", "initial-delay" => "1.5s",
"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.content",
"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

@@ -0,0 +1,16 @@
.section.groups
.content
.section-title-bar.groups-title-bar
.title Groups
.add-button{"ng-click" => "ctrl.clickedAddNewGroup()"} +
.group{"ng-if" => "ctrl.allGroup", "ng-click" => "ctrl.selectGroup(ctrl.allGroup)", "ng-class" => "{'selected' : ctrl.selectedGroup == ctrl.allGroup}",
"droppable" => true, "drop" => "ctrl.handleDrop", "group" => "ctrl.allGroup"}
%input.title{"ng-disabled" => "true", "ng-model" => "ctrl.allGroup.name"}
.count {{ctrl.noteCount(ctrl.allGroup)}}
.group{"ng-repeat" => "group in ctrl.groups", "ng-click" => "ctrl.selectGroup(group)", "ng-class" => "{'selected' : ctrl.selectedGroup == group}",
"droppable" => true, "drop" => "ctrl.handleDrop", "group" => "group"}
.icon.icon-rss{"ng-if" => "group.presentation.enabled"}
%input.title{"ng-disabled" => "group != ctrl.selectedGroup", "ng-model" => "group.name",
"ng-keyup" => "$event.keyCode == 13 && ctrl.saveGroup($event, group)", "mb-autofocus" => "true", "should-focus" => "ctrl.newGroup",
"ng-change" => "ctrl.groupTitleDidChange(group)", "ng-focus" => "ctrl.onGroupTitleFocus(group)"}
.count {{ctrl.noteCount(group)}}

View File

@@ -0,0 +1,137 @@
.header
.header-content
%nav.animated-fast.fadeInDown
%a.navbar-brand{"ui-sref" => "home"}
.header-name
neeto
%span.tagline{"ng-if" => "!ctrl.user.id", "ng-cloak" => "true"} secure code box for developers
.menu.navbar-text.navbar-right
.items
.item.account
%button.btn.dark-button.advanced-brand{"ng-click" => "ctrl.accountMenuPressed()"}
%div{"ng-if" => "ctrl.user.email"} {{ctrl.user.email}}
%div{"ng-if" => "!ctrl.user.email"} Sign in or Register
.panel.panel-default.account-panel{"ng-if" => "ctrl.showAccountMenu"}
.panel-body
.account-items
.account-item.registration-login{"ng-if" => "!ctrl.user.email"}
.meta-container
.title Sign in or Register
.desc
%form.account-form{'name' => "loginForm"}
.form-group.has-feedback
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.loginData.email'}
.form-group.has-feedback
%input.form-control.login-input{:placeholder => 'Password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.loginData.user_password'}
.checkbox{"ng-if" => "ctrl.hasLocalData()"}
%label
%input{"type" => "checkbox", "ng-model" => "ctrl.user.shouldMerge", "ng-bind" => "true", "ng-change" => "ctrl.mergeLocalChanged()"}
Merge local notes
%button.btn.dark-button.half-button{"ng-click" => "ctrl.loginSubmitPressed()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
%span Sign In
%button.btn.dark-button.half-button{"ng-click" => "ctrl.submitRegistrationForm()", "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
%span Register
%br
.login-forgot
%a.btn.btn-link{"ng-click" => "ctrl.showResetForm = !ctrl.showResetForm"} I forgot my password
.panel-status-text{"ng-if" => "ctrl.loginData.status", "style" => "font-size: 14px;"} {{ctrl.loginData.status}}
%form{"style" => "margin-top: 20px;", "ng-if" => "ctrl.showResetForm", "ng-init" => "ctrl.resetData = {}", 'ng-submit' => 'ctrl.forgotPasswordSubmit()', 'name' => "resetForm"}
.form-group.has-feedback
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'email', :placeholder => 'Email', :required => true, :type => 'email', 'ng-model' => 'ctrl.resetData.email'}
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
%span Send Reset Email
.panel-status-text{"ng-if" => "ctrl.resetData.response", "style" => "font-size: 14px;"}
{{ctrl.resetData.response}}
.account-item{"ng-if" => "ctrl.user.email"}
.icon-container
%img.icon{"lazy-img" => "assets/encryption.png"}
.meta-container
.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
.account-item{"ng-if" => "ctrl.user.email"}
.icon-container
%img.icon.archive{"lazy-img" => "assets/archive.png"}
.meta-container
.title Data Archives
.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
.account-item
.meta-container
.title Server
.desc Use a custom Neeto server to store and retrieve your account data.
.action-container
%form.account-form{'ng-submit' => 'ctrl.changeServer()', 'name' => "serverChangeForm"}
.form-group.has-feedback
%input.form-control{:name => 'server', :placeholder => 'Server URL', :required => true, :type => 'text', 'ng-model' => 'ctrl.serverData.url'}
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
%span.ladda-label Change Server
.links{"ng-if" => "ctrl.user.email"}
.link-item
%a{"ng-click" => "ctrl.changePasswordPressed()"} Change Password
%form.account-form{"ng-if" => "ctrl.showNewPasswordForm", 'ng-submit' => 'ctrl.submitPasswordChange()', 'name' => "passwordChangeForm"}
.form-group.has-feedback
%input.form-control.login-input{:autofocus => 'autofocus', :name => 'current', :placeholder => 'Current password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.current_password'}
.form-group.has-feedback
%input.form-control.login-input{:placeholder => 'New password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password', "autocomplete" => "new-password"}
.form-group.has-feedback
%input.form-control.login-input{:placeholder => 'Confirm password', :name => 'password', :required => true, :type => 'password', 'ng-model' => 'ctrl.passwordChangeData.new_password_confirmation', "autocomplete" => "new-password"}
%button.btn.dark-button.btn-block{:type => 'submit', "data-style" => "expand-right", "data-size" => "s", "state" => "buttonState"}
%span.ladda-label Change Password
.panel-status-text{"ng-if" => "ctrl.passwordChangeData.status", "style" => "font-size: 14px;"}
{{ctrl.passwordChangeData.status}}
.link-item
%a{"ng-click" => "ctrl.signOutPressed()"} Sign Out
.item
%a.menuItem{"ng-click" => "ctrl.showFaq = !ctrl.showFaq"}
faq
.panel.panel-default.faq-panel{"ng-if" => "ctrl.showFaq"}
.panel-body
.faq-item
%strong.question What is Neeto?
%p.answer Neeto is a secure code box for developers to store common commands and useful notes.
.faq-item
%strong.question What privacy features does Neeto have?
%p.answer
We don't want your data. We really don't. Storing your data in our database is a liability for us. But there's a tradeoff between privacy and convenience. We try to be convenient but also focus highly on privacy and security.
%ul
%li Neeto can be used locally, so that no data is sent to our servers whatsoever. This means however that your data is controlled by your browser, and can vanish without notice.
%li When signed in, your data is always sent through a secure connection to Neeto servers.
%li When the server receives your data, it is encrypted before being saved.
%li Your password is never sent to Neeto servers. Instead, we derive a key from your password and send that instead. This way, Neeto can never know the original contents of your password.
%li With Neeto Advanced, you can enable Local Encryption. This encrypts your notes locally on your machine <strong>before</strong> sending to Neeto servers. This means that technically even Neeto couldn't see the contents of your notes (and for that matter any government eavesdroppers.)
%li Neeto does not use Google Analytics, a seemingly harmless tool that Google uses to track your web usage patterns and sells it to advertisers.
.faq-item
%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

@@ -0,0 +1,12 @@
.main-ui-view
%header{"user" => "defaultUser", "logout" => "headerLogout"}
.app-container
.app
%groups-section{"save" => "groupsSave", "add-new" => "groupsAddNew", "will-select" => "groupsWillMakeSelection", "selection-made" => "groupsSelectionMade", "all-group" => "allGroup",
"groups" => "groups", "user" => "defaultUser", "update-note-group" => "groupsUpdateNoteGroup"}
%notes-section{"remove-group" => "notesRemoveGroup", "user" => "defaultUser", "add-new" => "notesAddNew", "selection-made" => "notesSelectionMade",
"group" => "selectedGroup", "user-id" => "defaultUser.id", "remove" => "deleteNote"}
%editor-section{"ng-if" => "selectedNote", "note" => "selectedNote", "remove" => "deleteNote",
"user" => "defaultUser", "save" => "saveNote"}

View File

@@ -0,0 +1,6 @@
.about.animated.fadeIn
.title About
.summary Namewhale helps you find a unique name for your startup. Using an intelligent, seed-based algorithm, names are generated based on the sound, style, and feel of the seed words you chose.
.links
%a{"href" => "https://itunes.apple.com/us/app/namewhale/id1028881375?ls=1&mt=8", "target" => "_blank"} Namewhale on the AppStore
%a{"href" => "https://twitter.com/namewhale", "target" => "_blank"} @namewhale

View File

@@ -0,0 +1,4 @@
%footer.footer{"ng-class" => "footerClass"}
.container
.row
.footer-about-section

View File

@@ -0,0 +1,2 @@
%strong Choose a public username for all your shared note groups.
%input{"style" => "margin-top: 10px; padding-left: 8px;", "type" => "text", "ng-keyup" => "$event.keyCode == 13 && saveUsername($event)", "ng-model" => "formData.username"}

View File

@@ -0,0 +1,38 @@
.section.notes
.content
.section-title-bar.notes-title-bar
.title {{ctrl.group.name}} notes
.add-button{"ng-click" => "ctrl.createNewNote()"} +
%br
.filter-section
%input.filter-bar{"select-on-click" => "true", "ng-model" => "ctrl.noteFilter.text", "placeholder" => "Filter", "ng-change" => "ctrl.filterTextChanged()", "lowercase" => "true"}
.editor-menu.group-menu-bar
%ul.nav.nav-pills
%li.dropdown
%a.dropdown-toggle{"ng-click" => "ctrl.showMenu = !ctrl.showMenu"}
File
%span.caret
%span.sr-only
%ul.dropdown-menu.dropdown-menu-left.nt-dropdown-menu.dark{"ng-if" => "ctrl.showMenu"}
%li{"ng-if" => "!ctrl.group.presentation.enabled"}
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupShare($event)"} Share Group
%li{"ng-if" => "ctrl.group.presentation.enabled"}
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupUnshare()"} Unshare Group
%li{"ng-if" => "!ctrl.group.all"}
%a.text{"ng-click" => "ctrl.selectedMenuItem(); ctrl.selectedGroupDelete()"} Delete Group
.menu-right-container
.public-link{"ng-if" => "ctrl.group.presentation.enabled"}
%a.url{"ng-if" => "!ctrl.editingUrl", "href" => "{{ctrl.publicUrlForGroup(ctrl.group)}}", "target" => "_blank"}
%span.icon-rss.icon
{{ctrl.publicUrlForGroup()}}
.edit-url{"ng-if" => "ctrl.editingUrl"}
{{ctrl.url.base}}
%input.input{"ng-model" => "ctrl.url.token", "ng-keyup" => "$event.keyCode == 13 && ctrl.saveUrl($event)",
"ng-change" => "ctrl.urlChanged()", "ng-focus" => "ctrl.onUrlFocus()",
"select-on-click" => "true", "autofocus" => "true"}
.note{"ng-repeat" => "note in ctrl.notes | filter: ctrl.filterNotes",
"ng-click" => "ctrl.selectNote(note)", "ng-class" => "{'selected' : ctrl.selectedNote == note}",
"ng-attr-draggable" => "{{note.dummy ? undefined : 'true'}}", "note" => "note"}
.name
{{note.name}}
.date {{note.created_at || 'Now'}}

View File

View File

@@ -0,0 +1,27 @@
class ApplicationController < ActionController::Base
include DeviseTokenAuth::Concerns::SetUserByToken
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :null_session
after_action :set_csrf_cookie
layout :false
def frontend
set_app_domain
end
rescue_from ActionView::MissingTemplate do |exception|
end
protected
def set_app_domain
@appDomain = request.domain
@appDomain << ':' + request.port.to_s unless request.port.blank?
end
def set_csrf_cookie
cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
end
end

View File

View File

@@ -0,0 +1,2 @@
module ApplicationHelper
end

0
app/mailers/.keep Normal file
View File

0
app/models/.keep Normal file
View File

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html ng-app="app.frontend" ng-controller="BaseCtrl">
<head>
<meta charset="utf-8"/>
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
<meta content="width=device-width, initial-scale=1" name="viewport"/>
<link href="favicon/apple-touch-icon.png" rel="apple-touch-icon" sizes="180x180"></link>
<link href="favicon/favicon-32x32.png" rel="icon" sizes="32x32" type="image/png"></link>
<link href="favicon/favicon-16x16.png" rel="icon" sizes="16x16" type="image/png"></link>
<link href="favicon/manifest.json" rel="manifest"></link>
<link color="#5bbad5" href="favicon/safari-pinned-tab.svg" rel="mask-icon"></link>
<meta name="theme-color" content="#ffffff">
<meta ng-bind="title" content="Neeto" name="apple-mobile-web-app-title"/>
<meta ng-bind="title" content="Neeto" name="application-name"/>
<base href="/"></base>
<title ng-bind="title">Neeto</title>
<meta name="description" content="A private and secure personal notes/blogging system."/>
<meta name="twitter:title" content="Neeto, a private and secure notes app."/>
<meta name="twitter:description" content="A private and secure personal notes/blogging system."/>
<meta name="twitter:site" content="@neetoapp"/>
<meta name="twitter:card" content="summary"/>
<meta name="og:title" content="Neeto, a private and secure notes app."/>
<meta name="og:description" content="A private and secure personal notes/blogging system."/>
<% if Rails.env.development? %>
<%= javascript_include_tag "compiled.js", debug: false %>
<% else %>
<%= javascript_include_tag "compiled.min.js", debug: false %>
<!-- Piwik -->
<script type="text/javascript">
var _paq = _paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//dash.neeto.io/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img src="//dash.neeto.io/piwik.php?idsite=1" style="border:0;" alt="" /></p></noscript>
<!-- End Piwik Code -->
<% end %>
<%= stylesheet_link_tag "app", media: "all", debug: false %>
</head>
<body ng-class="bodyClass">
<div ui-view="content"></div>
</body>
</html>

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Namewhale</title>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>

3
bin/bundle Executable file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env ruby
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
load Gem.bin_path('bundler', 'bundle')

4
bin/rails Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

4
bin/rake Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative '../config/boot'
require 'rake'
Rake.application.run

34
bin/setup Executable file
View File

@@ -0,0 +1,34 @@
#!/usr/bin/env ruby
require 'pathname'
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
# This script is a starting point to setup your application.
# Add necessary setup steps to this file.
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
# cp 'config/database.yml.sample', 'config/database.yml'
# end
puts "\n== Preparing database =="
system! 'bin/rails db:setup'
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
puts "\n== Restarting application server =="
system! 'bin/rails restart'
end

15
bin/spring Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env ruby
# This file loads spring without using Bundler, in order to be fast.
# It gets overwritten when you run the `spring binstub` command.
unless defined?(Spring)
require "rubygems"
require "bundler"
if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)
Gem.paths = { "GEM_PATH" => [Bundler.bundle_path.to_s, *Gem.path].uniq }
gem "spring", match[1]
require "spring/binstub"
end
end

29
bin/update Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env ruby
require 'pathname'
require 'fileutils'
include FileUtils
# path to your application root.
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
def system!(*args)
system(*args) || abort("\n== Command #{args} failed ==")
end
chdir APP_ROOT do
# This script is a way to update your development environment automatically.
# Add necessary update steps to this file.
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
system('bundle check') || system!('bundle install')
puts "\n== Updating database =="
system! 'bin/rails db:migrate'
puts "\n== Removing old logs and tempfiles =="
system! 'bin/rails log:clear tmp:clear'
puts "\n== Restarting application server =="
system! 'bin/rails restart'
end

25
bower.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "neeto",
"lib": {
"name": "bower-rails generated lib assets",
"dependencies": {}
},
"vendor": {
"name": "bower-rails generated vendor assets",
"dependencies": {
"angular": "^1.5.9",
"angular-ui-router": "^0.2.18",
"restangular": "^1.5.2",
"ng-token-auth": "^0.0.29",
"oclazyload": "^1.0.9",
"font-awesome": "^4.6.2",
"angular-cookie": "4.1.0",
"marked": "0.3.4",
"angular-lazy-img": "1.1.0",
"ng-dialog" : "0.6.4"
},
"resolutions": {
"angular": "1.5.9"
}
}
}

4
config.ru Normal file
View File

@@ -0,0 +1,4 @@
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
run Rails.application

71
config/application.rb Normal file
View File

@@ -0,0 +1,71 @@
require_relative 'boot'
# require 'rails/all'
require "active_model/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "active_job/railtie" # Only for Rails >= 4.2
require "action_cable/engine" # Only for Rails >= 5.0
require "sprockets/railtie"
require "rails/test_unit/railtie"
# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
module Neeto
class Application < Rails::Application
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# config.i18n.default_locale = :de
# Do not swallow errors in after_commit/after_rollback callbacks.
# config.active_record.raise_in_transactional_callbacks = true
# Cross-Origin Resource Sharing (CORS) for Rack compatible web applications.
config.middleware.insert_before 0, "Rack::Cors" do
allow do
origins '*'
resource '*', :headers => :any, :methods => [:get, :post, :put, :patch, :delete, :options], :expose => ['Access-Token', 'Client', 'UID']
end
end
# config.middleware.use Rack::Deflater
config.middleware.insert_before(Rack::Sendfile, Rack::Deflater)
# Disable auto creation of additional resources with "rails generate"
config.generators do |g|
g.test_framework false
g.view_specs false
g.helper_specs false
g.stylesheets = false
g.javascripts = false
g.helper = false
end
config.action_mailer.default_url_options = { host: ENV['APP_HOST'] }
# SMTP settings
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
:address => ENV['SMTP_HOST'],
:port => ENV['SMTP_PORT'],
:domain => ENV['SMTP_DOMAIN'],
:user_name => ENV['SMTP_USERNAME'],
:password => ENV['SMTP_PASSWORD'],
:authentication => 'login',
:enable_starttls_auto => true # detects and uses STARTTLS
}
end
end

3
config/boot.rb Normal file
View File

@@ -0,0 +1,3 @@
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.

10
config/cable.yml Normal file
View File

@@ -0,0 +1,10 @@
development:
adapter: redis
url: redis://localhost:6379
test:
adapter: async
production:
adapter: redis
url: redis://localhost:6379/1

40
config/database.yml Normal file
View File

@@ -0,0 +1,40 @@
# MySQL. Versions 5.0+ are recommended.
#
# Install the MYSQL driver
# gem install mysql2
#
# Ensure the MySQL gem is defined in your Gemfile
# gem 'mysql2'
#
# And be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
#
default: &default
adapter: mysql2
encoding: utf8
pool: 5
username: <%= ENV['DB_USERNAME'] %>
password: <%= ENV['DB_PASSWORD'] %>
database: <%= ENV['DB_DATABASE'] %>
host: <%= ENV['DB_HOST'] %>
port: <%= ENV['DB_PORT'] %>
development:
<<: *default
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
# You can use this database configuration with:
#
# production:
# url: <%= ENV['DATABASE_URL'] %>
#
staging:
<<: *default
production:
<<: *default

70
config/deploy.rb Normal file
View File

@@ -0,0 +1,70 @@
CAP_CONFIG = YAML.load_file("config/cap.yml")
# config valid only for current version of Capistrano
lock '3.6.1'
set :application, 'neeto'
set :repo_url, CAP_CONFIG["default"]["repo_url"]
# Default branch is :master
# ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp
# Default deploy_to directory is /var/www/my_app_name
# set :deploy_to, '/var/www/my_app_name'
# Default value for :scm is :git
set :scm, :git
set :git_strategy, Capistrano::Git::SubmoduleStrategy
# Default value for :format is :airbrussh.
# set :format, :airbrussh
# You can configure the Airbrussh format using :format_options.
# These are the defaults.
# set :format_options, command_output: true, log_file: 'log/capistrano.log', color: :auto, truncate: :auto
# Default value for :pty is false
# set :pty, true
# Default value for :linked_files is []
# set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')
set :linked_files, fetch(:linked_files, []).push('.env')
# Default value for linked_dirs is []
# set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system', 'public/uploads')
# Default value for keep_releases is 5
# set :keep_releases, 5
set :rvm_ruby_version, '2.3.0'
namespace :deploy do
task :npm_install do
on roles(:app) do
within release_path do
# string commands dont work, have to use special *%w syntax
execute *%w[ npm install ]
execute *%w[ grunt ]
end
end
end
after :restart, :clear_cache do
on roles(:web), in: :groups, limit: 3, wait: 5 do
# Here we can do anything such as:
within release_path do
end
end
end
end
before 'deploy:compile_assets', 'bower:install'
after 'bower:install', 'deploy:npm_install'
set :ssh_options, {
keys: %W( #{CAP_CONFIG['default']['key_path']} ),
forward_agent: false,
auth_methods: %w(publickey)
}

View File

@@ -0,0 +1,63 @@
# server-based syntax
# ======================
# Defines a single server with a list of roles and multiple properties.
# You can define all roles on a single server, or split them:
# server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value
# server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value
# server 'db.example.com', user: 'deploy', roles: %w{db}
server CAP_CONFIG['production']['server'], user: CAP_CONFIG['production']['user'], roles: %w{app db web}
set :deploy_to, CAP_CONFIG['production']['deploy_to']
# role-based syntax
# ==================
# Defines a role with one or multiple servers. The primary server in each
# group is considered to be the first unless any hosts have the primary
# property set. Specify the username and a domain or IP for the server.
# Don't use `:all`, it's a meta role.
# role :app, %w{deploy@example.com}, my_property: :my_value
# role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
# role :db, %w{deploy@example.com}
# Configuration
# =============
# You can set any configuration variable like in config/deploy.rb
# These variables are then only loaded and set in this stage.
# For available Capistrano configuration variables see the documentation page.
# http://capistranorb.com/documentation/getting-started/configuration/
# Feel free to add new variables to customise your setup.
# Custom SSH Options
# ==================
# You may pass any option but keep in mind that net/ssh understands a
# limited set of options, consult the Net::SSH documentation.
# http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
#
# Global options
# --------------
# set :ssh_options, {
# keys: %w(/home/rlisowski/.ssh/id_rsa),
# forward_agent: false,
# auth_methods: %w(password)
# }
#
# The server-based syntax can be used to override options:
# ------------------------------------
# server 'example.com',
# user: 'user_name',
# roles: %w{web app},
# ssh_options: {
# user: 'user_name', # overrides user setting above
# keys: %w(/home/user_name/.ssh/id_rsa),
# forward_agent: false,
# auth_methods: %w(publickey password)
# # password: 'please use keys'
# }

65
config/deploy/staging.rb Normal file
View File

@@ -0,0 +1,65 @@
# server-based syntax
# ======================
# Defines a single server with a list of roles and multiple properties.
# You can define all roles on a single server, or split them:
# server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value
# server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value
# server 'db.example.com', user: 'deploy', roles: %w{db}
server CAP_CONFIG['staging']['server'], user: CAP_CONFIG['staging']['user'], roles: %w{app db web}
set :branch, CAP_CONFIG['staging']['branch']
set :deploy_to, CAP_CONFIG['staging']['deploy_to']
# role-based syntax
# ==================
# Defines a role with one or multiple servers. The primary server in each
# group is considered to be the first unless any hosts have the primary
# property set. Specify the username and a domain or IP for the server.
# Don't use `:all`, it's a meta role.
# role :app, %w{deploy@example.com}, my_property: :my_value
# role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
# role :db, %w{deploy@example.com}
# Configuration
# =============
# You can set any configuration variable like in config/deploy.rb
# These variables are then only loaded and set in this stage.
# For available Capistrano configuration variables see the documentation page.
# http://capistranorb.com/documentation/getting-started/configuration/
# Feel free to add new variables to customise your setup.
# Custom SSH Options
# ==================
# You may pass any option but keep in mind that net/ssh understands a
# limited set of options, consult the Net::SSH documentation.
# http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
#
# Global options
# --------------
# set :ssh_options, {
# keys: %w(/home/rlisowski/.ssh/id_rsa),
# forward_agent: false,
# auth_methods: %w(password)
# }
#
# The server-based syntax can be used to override options:
# ------------------------------------
# server 'example.com',
# user: 'user_name',
# roles: %w{web app},
# ssh_options: {
# user: 'user_name', # overrides user setting above
# keys: %w(/home/user_name/.ssh/id_rsa),
# forward_agent: false,
# auth_methods: %w(publickey password)
# # password: 'please use keys'
# }

5
config/environment.rb Normal file
View File

@@ -0,0 +1,5 @@
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!

View File

@@ -0,0 +1,51 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# config.assets.js_compressor = Uglifier.new(mangle: false)\
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
# In the development environment your application's code is reloaded on
# every request. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
config.reload_classes_only_on_change = true
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
# config.public_file_server.enabled = true
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise an error on page load if there are pending migrations.
# config.active_record.migration_error = :page_load
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
# yet still be able to expire them through the digest params.
config.assets.digest = true
# Adds additional error checking when serving assets at runtime.
# Checks for improperly declared sprockets dependencies.
# Raises helpful error messages.
config.assets.raise_runtime_errors = true
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
end

View File

@@ -0,0 +1,96 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = false
config.action_controller.perform_caching = true
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Mount Action Cable outside main process or domain
# config.action_cable.mount_path = nil
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
# Enable Rack::Cache to put a simple HTTP cache in front of your application
# Add `rack-cache` to your Gemfile before enabling this.
# For large-scale production use, consider using a caching reverse proxy like
# NGINX, varnish or squid.
# config.action_dispatch.rack_cache = true
# Compress JavaScripts and CSS.
config.assets.compress = true
# config.assets.js_compressor = Uglifier.new(mangle: false)
config.assets.js_compressor = :uglifier
config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
# yet still be able to expire them through the digest params.
config.assets.digest = true
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = :debug
# Prepend all log lines with the following tags.
# config.log_tags = [ :subdomain, :uuid ]
# Use a different logger for distributed setups.
# config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Do not dump schema after migrations.
# config.active_record.dump_schema_after_migration = false
end

View File

@@ -0,0 +1,81 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
# Eager load code on boot. This eager loads most of Rails and
# your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
# Full error reports are disabled and caching is turned on.
config.consider_all_requests_local = true
config.action_controller.perform_caching = true
# Compress JavaScripts and CSS.
config.assets.compress = true
# config.assets.js_compressor = Uglifier.new(mangle: false)
config.assets.js_compressor = :uglifier
config.assets.css_compressor = :sass
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# Asset digests allow you to set far-future HTTP expiration dates on all assets,
# yet still be able to expire them through the digest params.
# config.assets.digest = true
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
config.log_level = :debug
# Prepend all log lines with the following tags.
# config.log_tags = [ :subdomain, :uuid ]
# Use a different logger for distributed setups.
# config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
# Do not dump schema after migrations.
# config.active_record.dump_schema_after_migration = false
end

View File

@@ -0,0 +1,42 @@
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
# preloads Rails for running tests, you may have to set it to true.
config.eager_load = false
# Configure public file server for tests with Cache-Control for performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
'Cache-Control' => 'public, max-age=3600'
}
# Show full error reports and disable caching.
config.consider_all_requests_local = true
config.action_controller.perform_caching = false
# Raise exceptions instead of rendering exception templates.
config.action_dispatch.show_exceptions = false
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raises error for missing translations
# config.action_view.raise_on_missing_translations = true
end

View File

@@ -0,0 +1,6 @@
# Be sure to restart your server when you modify this file.
# ApplicationController.renderer.defaults.merge!(
# http_host: 'example.org',
# https: false
# )

View File

@@ -0,0 +1,16 @@
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path
# Rails.application.config.assets.paths << Emoji.images_path
Rails.application.config.assets.paths << Rails.root.join('vendor', 'assets', 'fonts')
Rails.application.config.assets.precompile << /\.(?:svg|eot|woff|ttf)\z/
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
# Rails.application.config.assets.precompile += %w( search.js )
Rails.application.config.assets.precompile += %w( app.css compiled.min.js compiled.js )

View File

@@ -0,0 +1,7 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!

View File

@@ -0,0 +1,19 @@
BowerRails.configure do |bower_rails|
# Tell bower-rails what path should be considered as root. Defaults to Dir.pwd
# bower_rails.root_path = Dir.pwd
# Invokes rake bower:install before precompilation. Defaults to false
# bower_rails.install_before_precompile = true
# Invokes rake bower:resolve before precompilation. Defaults to false
# bower_rails.resolve_before_precompile = true
# Invokes rake bower:clean before precompilation. Defaults to false
# bower_rails.clean_before_precompile = true
# Invokes rake bower:install:deployment instead rake bower:install. Defaults to false
# bower_rails.use_bower_install_deployment = true
#
# Invokes rake bower:install and rake bower:install:deployment with -F (force) flag. Defaults to false
# bower_rails.force_install = true
end

View File

@@ -0,0 +1,5 @@
# Be sure to restart your server when you modify this file.
# Specify a serializer for the signed and encrypted cookie jars.
# Valid options are :json, :marshal, and :hybrid.
Rails.application.config.action_dispatch.cookies_serializer = :json

View File

@@ -0,0 +1,48 @@
DeviseTokenAuth.setup do |config|
# By default the authorization headers will change after each request. The
# client is responsible for keeping track of the changing tokens. Change
# this to false to prevent the Authorization header from changing after
# each request.
# config.change_headers_on_each_request = true
# By default, users will need to re-authenticate after 2 weeks. This setting
# determines how long tokens will remain valid after they are issued.
# config.token_lifespan = 2.weeks
# Sets the max number of concurrent devices per user, which is 10 by default.
# After this limit is reached, the oldest tokens will be removed.
# config.max_number_of_devices = 10
# Sometimes it's necessary to make several requests to the API at the same
# time. In this case, each request in the batch will need to share the same
# auth token. This setting determines how far apart the requests can be while
# still using the same auth token.
# config.batch_request_buffer_throttle = 5.seconds
# This route will be the prefix for all oauth2 redirect callbacks. For
# example, using the default '/omniauth', the github oauth2 provider will
# redirect successful authentications to '/omniauth/github/callback'
# config.omniauth_prefix = "/omniauth"
# By default sending current password is not needed for the password update.
# Uncomment to enforce current_password param to be checked before all
# attribute updates. Set it to :password if you want it to be checked only if
# password is updated.
config.check_current_password_before_update = :password
# By default we will use callbacks for single omniauth.
# It depends on fields like email, provider and uid.
# config.default_callbacks = true
# Makes it possible to change the headers names
# config.headers_names = {:'access-token' => 'access-token',
# :'client' => 'client',
# :'expiry' => 'expiry',
# :'uid' => 'uid',
# :'token-type' => 'token-type' }
# By default, only Bearer Token authentication is implemented out of the box.
# If, however, you wish to integrate with legacy Devise authentication, you can
# do so by enabling this flag. NOTE: This feature is highly experimental!
# config.enable_standard_devise_support = false
end

View File

@@ -0,0 +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, :local_eek]

View File

@@ -0,0 +1,16 @@
# Be sure to restart your server when you modify this file.
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym 'RESTful'
# end

View File

@@ -0,0 +1,4 @@
# Be sure to restart your server when you modify this file.
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf

View File

@@ -0,0 +1,23 @@
# Be sure to restart your server when you modify this file.
#
# This file contains migration options to ease your Rails 5.0 upgrade.
#
# Once upgraded flip defaults one by one to migrate to the new default.
#
# Read the Rails 5.0 release notes for more info on each option.
# Enable per-form CSRF tokens. Previous versions had false.
Rails.application.config.action_controller.per_form_csrf_tokens = false
# Enable origin-checking CSRF mitigation. Previous versions had false.
Rails.application.config.action_controller.forgery_protection_origin_check = false
# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
# Previous versions had false.
ActiveSupport.to_time_preserves_timezone = false
# Require `belongs_to` associations by default. Previous versions had false.
# Rails.application.config.active_record.belongs_to_required_by_default = false
# Do not halt callback chains when a callback returns false. Previous versions had true.
ActiveSupport.halt_callback_chains_on_return_false = true

View File

@@ -0,0 +1,3 @@
# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_neeto_session'

View File

@@ -0,0 +1,14 @@
# Be sure to restart your server when you modify this file.
# This file contains settings for ActionController::ParamsWrapper which
# is enabled by default.
# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
ActiveSupport.on_load(:action_controller) do
wrap_parameters format: [:json]
end
# To enable root element in JSON for ActiveRecord objects.
# ActiveSupport.on_load(:active_record) do
# self.include_root_in_json = true
# end

23
config/locales/en.yml Normal file
View File

@@ -0,0 +1,23 @@
# Files in the config/locales directory are used for internationalization
# and are automatically loaded by Rails. If you want to use locales other
# than English, add the necessary files in this directory.
#
# To use the locales, use `I18n.t`:
#
# I18n.t 'hello'
#
# In views, this is aliased to just `t`:
#
# <%= t('hello') %>
#
# To use a different locale, set it with `I18n.locale`:
#
# I18n.locale = :es
#
# This would use the information in config/locales/es.yml.
#
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.
en:
hello: "Hello world"

53
config/puma.rb Normal file
View File

@@ -0,0 +1,53 @@
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum, this matches the default thread size of Active Record.
#
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
threads threads_count, threads_count
#!/usr/bin/env puma
# start puma with:
# RAILS_ENV=production bundle exec puma -C ./config/puma.rb
# Specifies the `port` that Puma will listen on to receive requests, default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked webserver processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory. If you use this option
# you need to make sure to reconnect any threads in the `on_worker_boot`
# block.
#
# preload_app!
# The code in the `on_worker_boot` will be called if you are using
# clustered mode by specifying a number of `workers`. After each worker
# process is booted this block will be run, if you are using `preload_app!`
# option you will want to use this block to reconnect to any threads
# or connections that may have been created at application boot, Ruby
# cannot share connections between processes.
#
# on_worker_boot do
# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
# end
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart

Some files were not shown because too many files have changed in this diff Show More