diff --git a/.bowerrc b/.bowerrc
new file mode 100644
index 0000000..666f347
--- /dev/null
+++ b/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "client/bower_components"
+}
diff --git a/.buildignore b/.buildignore
new file mode 100644
index 0000000..fc98b8e
--- /dev/null
+++ b/.buildignore
@@ -0,0 +1 @@
+*.coffee
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c2cdfb8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,21 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+
+[*]
+
+# Change these settings to your own preference
+indent_style = space
+indent_size = 2
+
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..2125666
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c029da6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+public
+.tmp
+.idea
+client/bower_components
+dist
+/server/config/local.env.js
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..7989278
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+language: node_js
+node_js:
+ - '0.10'
+ - '0.11'
+before_script:
+ - npm install -g bower grunt-cli
+ - bower install
+services: mongodb
\ No newline at end of file
diff --git a/.yo-rc.json b/.yo-rc.json
new file mode 100644
index 0000000..6911acc
--- /dev/null
+++ b/.yo-rc.json
@@ -0,0 +1,47 @@
+{
+ "generator-angular-fullstack": {
+ "insertRoutes": true,
+ "registerRoutesFile": "server/routes.js",
+ "routesNeedle": "// Insert routes below",
+ "routesBase": "/api/",
+ "pluralizeRoutes": true,
+ "insertSockets": true,
+ "registerSocketsFile": "server/config/socketio.js",
+ "socketsNeedle": "// Insert sockets below",
+ "filters": {
+ "js": true,
+ "jade": true,
+ "stylus": true,
+ "uirouter": true,
+ "bootstrap": true,
+ "uibootstrap": true,
+ "mongoose": true,
+ "auth": true
+ }
+ },
+ "generator-ng-component": {
+ "routeDirectory": "client/app/",
+ "directiveDirectory": "client/app/",
+ "filterDirectory": "client/app/",
+ "serviceDirectory": "client/app/",
+ "basePath": "client",
+ "moduleName": "",
+ "filters": [
+ "uirouter"
+ ],
+ "extensions": [
+ "js",
+ "jade",
+ "styl"
+ ],
+ "directiveSimpleTemplates": "",
+ "directiveComplexTemplates": "",
+ "filterTemplates": "",
+ "serviceTemplates": "",
+ "factoryTemplates": "",
+ "controllerTemplates": "",
+ "decoratorTemplates": "",
+ "providerTemplates": "",
+ "routeTemplates": ""
+ }
+}
\ No newline at end of file
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000..76b158c
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,696 @@
+// Generated on 2015-06-18 using generator-angular-fullstack 2.0.13
+'use strict';
+
+module.exports = function (grunt) {
+ var localConfig;
+ try {
+ localConfig = require('./server/config/local.env');
+ } catch(e) {
+ localConfig = {};
+ }
+
+ // Load grunt tasks automatically, when needed
+ require('jit-grunt')(grunt, {
+ express: 'grunt-express-server',
+ useminPrepare: 'grunt-usemin',
+ ngtemplates: 'grunt-angular-templates',
+ cdnify: 'grunt-google-cdn',
+ protractor: 'grunt-protractor-runner',
+ injector: 'grunt-asset-injector',
+ buildcontrol: 'grunt-build-control'
+ });
+
+ // Time how long tasks take. Can help when optimizing build times
+ require('time-grunt')(grunt);
+
+ // Define the configuration for all the tasks
+ grunt.initConfig({
+
+ // Project settings
+ pkg: grunt.file.readJSON('package.json'),
+ yeoman: {
+ // configurable paths
+ client: require('./bower.json').appPath || 'client',
+ dist: 'dist'
+ },
+ express: {
+ options: {
+ port: process.env.PORT || 9000
+ },
+ dev: {
+ options: {
+ script: 'server/app.js',
+ debug: true
+ }
+ },
+ prod: {
+ options: {
+ script: 'dist/server/app.js'
+ }
+ }
+ },
+ open: {
+ server: {
+ url: 'http://localhost:<%= express.options.port %>'
+ }
+ },
+ watch: {
+ injectJS: {
+ files: [
+ '<%= yeoman.client %>/{app,components}/**/*.js',
+ '!<%= yeoman.client %>/{app,components}/**/*.spec.js',
+ '!<%= yeoman.client %>/{app,components}/**/*.mock.js',
+ '!<%= yeoman.client %>/app/app.js'],
+ tasks: ['injector:scripts']
+ },
+ injectCss: {
+ files: [
+ '<%= yeoman.client %>/{app,components}/**/*.css'
+ ],
+ tasks: ['injector:css']
+ },
+ mochaTest: {
+ files: ['server/**/*.spec.js'],
+ tasks: ['env:test', 'mochaTest']
+ },
+ jsTest: {
+ files: [
+ '<%= yeoman.client %>/{app,components}/**/*.spec.js',
+ '<%= yeoman.client %>/{app,components}/**/*.mock.js'
+ ],
+ tasks: ['newer:jshint:all', 'karma']
+ },
+ injectStylus: {
+ files: [
+ '<%= yeoman.client %>/{app,components}/**/*.styl'],
+ tasks: ['injector:stylus']
+ },
+ stylus: {
+ files: [
+ '<%= yeoman.client %>/{app,components}/**/*.styl'],
+ tasks: ['stylus', 'autoprefixer']
+ },
+ jade: {
+ files: [
+ '<%= yeoman.client %>/{app,components}/*',
+ '<%= yeoman.client %>/{app,components}/**/*.jade'],
+ tasks: ['jade']
+ },
+ gruntfile: {
+ files: ['Gruntfile.js']
+ },
+ livereload: {
+ files: [
+ '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.css',
+ '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.html',
+ '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js',
+ '!{.tmp,<%= yeoman.client %>}{app,components}/**/*.spec.js',
+ '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js',
+ '<%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}'
+ ],
+ options: {
+ livereload: true
+ }
+ },
+ express: {
+ files: [
+ 'server/**/*.{js,json}'
+ ],
+ tasks: ['express:dev', 'wait'],
+ options: {
+ livereload: true,
+ nospawn: true //Without this option specified express won't be reloaded
+ }
+ }
+ },
+
+ // Make sure code styles are up to par and there are no obvious mistakes
+ jshint: {
+ options: {
+ jshintrc: '<%= yeoman.client %>/.jshintrc',
+ reporter: require('jshint-stylish')
+ },
+ server: {
+ options: {
+ jshintrc: 'server/.jshintrc'
+ },
+ src: [
+ 'server/**/*.js',
+ '!server/**/*.spec.js'
+ ]
+ },
+ serverTest: {
+ options: {
+ jshintrc: 'server/.jshintrc-spec'
+ },
+ src: ['server/**/*.spec.js']
+ },
+ all: [
+ '<%= yeoman.client %>/{app,components}/**/*.js',
+ '!<%= yeoman.client %>/{app,components}/**/*.spec.js',
+ '!<%= yeoman.client %>/{app,components}/**/*.mock.js'
+ ],
+ test: {
+ src: [
+ '<%= yeoman.client %>/{app,components}/**/*.spec.js',
+ '<%= yeoman.client %>/{app,components}/**/*.mock.js'
+ ]
+ }
+ },
+
+ // Empties folders to start fresh
+ clean: {
+ dist: {
+ files: [{
+ dot: true,
+ src: [
+ '.tmp',
+ '<%= yeoman.dist %>/*',
+ '!<%= yeoman.dist %>/.git*',
+ '!<%= yeoman.dist %>/.openshift',
+ '!<%= yeoman.dist %>/Procfile'
+ ]
+ }]
+ },
+ server: '.tmp'
+ },
+
+ // Add vendor prefixed styles
+ autoprefixer: {
+ options: {
+ browsers: ['last 1 version']
+ },
+ dist: {
+ files: [{
+ expand: true,
+ cwd: '.tmp/',
+ src: '{,*/}*.css',
+ dest: '.tmp/'
+ }]
+ }
+ },
+
+ // Debugging with node inspector
+ 'node-inspector': {
+ custom: {
+ options: {
+ 'web-host': 'localhost'
+ }
+ }
+ },
+
+ // Use nodemon to run server in debug mode with an initial breakpoint
+ nodemon: {
+ debug: {
+ script: 'server/app.js',
+ options: {
+ nodeArgs: ['--debug-brk'],
+ env: {
+ PORT: process.env.PORT || 9000
+ },
+ callback: function (nodemon) {
+ nodemon.on('log', function (event) {
+ console.log(event.colour);
+ });
+
+ // opens browser on initial server start
+ nodemon.on('config:update', function () {
+ setTimeout(function () {
+ require('open')('http://localhost:8080/debug?port=5858');
+ }, 500);
+ });
+ }
+ }
+ }
+ },
+
+ // Automatically inject Bower components into the app
+ wiredep: {
+ target: {
+ src: '<%= yeoman.client %>/index.html',
+ ignorePath: '<%= yeoman.client %>/',
+ exclude: [/bootstrap-sass-official/, /bootstrap.js/, '/json3/', '/es5-shim/', /bootstrap.css/, /font-awesome.css/ ]
+ }
+ },
+
+ // Renames files for browser caching purposes
+ rev: {
+ dist: {
+ files: {
+ src: [
+ '<%= yeoman.dist %>/public/{,*/}*.js',
+ '<%= yeoman.dist %>/public/{,*/}*.css',
+ '<%= yeoman.dist %>/public/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
+ '<%= yeoman.dist %>/public/assets/fonts/*'
+ ]
+ }
+ }
+ },
+
+ // Reads HTML for usemin blocks to enable smart builds that automatically
+ // concat, minify and revision files. Creates configurations in memory so
+ // additional tasks can operate on them
+ useminPrepare: {
+ html: ['<%= yeoman.client %>/index.html'],
+ options: {
+ dest: '<%= yeoman.dist %>/public'
+ }
+ },
+
+ // Performs rewrites based on rev and the useminPrepare configuration
+ usemin: {
+ html: ['<%= yeoman.dist %>/public/{,*/}*.html'],
+ css: ['<%= yeoman.dist %>/public/{,*/}*.css'],
+ js: ['<%= yeoman.dist %>/public/{,*/}*.js'],
+ options: {
+ assetsDirs: [
+ '<%= yeoman.dist %>/public',
+ '<%= yeoman.dist %>/public/assets/images'
+ ],
+ // This is so we update image references in our ng-templates
+ patterns: {
+ js: [
+ [/(assets\/images\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images']
+ ]
+ }
+ }
+ },
+
+ // The following *-min tasks produce minified files in the dist folder
+ imagemin: {
+ dist: {
+ files: [{
+ expand: true,
+ cwd: '<%= yeoman.client %>/assets/images',
+ src: '{,*/}*.{png,jpg,jpeg,gif}',
+ dest: '<%= yeoman.dist %>/public/assets/images'
+ }]
+ }
+ },
+
+ svgmin: {
+ dist: {
+ files: [{
+ expand: true,
+ cwd: '<%= yeoman.client %>/assets/images',
+ src: '{,*/}*.svg',
+ dest: '<%= yeoman.dist %>/public/assets/images'
+ }]
+ }
+ },
+
+ // Allow the use of non-minsafe AngularJS files. Automatically makes it
+ // minsafe compatible so Uglify does not destroy the ng references
+ ngAnnotate: {
+ dist: {
+ files: [{
+ expand: true,
+ cwd: '.tmp/concat',
+ src: '*/**.js',
+ dest: '.tmp/concat'
+ }]
+ }
+ },
+
+ // Package all the html partials into a single javascript payload
+ ngtemplates: {
+ options: {
+ // This should be the name of your apps angular module
+ module: 'manticoreApp',
+ htmlmin: {
+ collapseBooleanAttributes: true,
+ collapseWhitespace: true,
+ removeAttributeQuotes: true,
+ removeEmptyAttributes: true,
+ removeRedundantAttributes: true,
+ removeScriptTypeAttributes: true,
+ removeStyleLinkTypeAttributes: true
+ },
+ usemin: 'app/app.js'
+ },
+ main: {
+ cwd: '<%= yeoman.client %>',
+ src: ['{app,components}/**/*.html'],
+ dest: '.tmp/templates.js'
+ },
+ tmp: {
+ cwd: '.tmp',
+ src: ['{app,components}/**/*.html'],
+ dest: '.tmp/tmp-templates.js'
+ }
+ },
+
+ // Replace Google CDN references
+ cdnify: {
+ dist: {
+ html: ['<%= yeoman.dist %>/public/*.html']
+ }
+ },
+
+ // Copies remaining files to places other tasks can use
+ copy: {
+ dist: {
+ files: [{
+ expand: true,
+ dot: true,
+ cwd: '<%= yeoman.client %>',
+ dest: '<%= yeoman.dist %>/public',
+ src: [
+ '*.{ico,png,txt}',
+ '.htaccess',
+ 'bower_components/**/*',
+ 'assets/images/{,*/}*.{webp}',
+ 'assets/fonts/**/*',
+ 'index.html'
+ ]
+ }, {
+ expand: true,
+ cwd: '.tmp/images',
+ dest: '<%= yeoman.dist %>/public/assets/images',
+ src: ['generated/*']
+ }, {
+ expand: true,
+ dest: '<%= yeoman.dist %>',
+ src: [
+ 'package.json',
+ 'server/**/*'
+ ]
+ }]
+ },
+ styles: {
+ expand: true,
+ cwd: '<%= yeoman.client %>',
+ dest: '.tmp/',
+ src: ['{app,components}/**/*.css']
+ }
+ },
+
+ buildcontrol: {
+ options: {
+ dir: 'dist',
+ commit: true,
+ push: true,
+ connectCommits: false,
+ message: 'Built %sourceName% from commit %sourceCommit% on branch %sourceBranch%'
+ },
+ heroku: {
+ options: {
+ remote: 'heroku',
+ branch: 'master'
+ }
+ },
+ openshift: {
+ options: {
+ remote: 'openshift',
+ branch: 'master'
+ }
+ }
+ },
+
+ // Run some tasks in parallel to speed up the build process
+ concurrent: {
+ server: [
+ 'jade',
+ 'stylus',
+ ],
+ test: [
+ 'jade',
+ 'stylus',
+ ],
+ debug: {
+ tasks: [
+ 'nodemon',
+ 'node-inspector'
+ ],
+ options: {
+ logConcurrentOutput: true
+ }
+ },
+ dist: [
+ 'jade',
+ 'stylus',
+ 'imagemin',
+ 'svgmin'
+ ]
+ },
+
+ // Test settings
+ karma: {
+ unit: {
+ configFile: 'karma.conf.js',
+ singleRun: true
+ }
+ },
+
+ mochaTest: {
+ options: {
+ reporter: 'spec'
+ },
+ src: ['server/**/*.spec.js']
+ },
+
+ protractor: {
+ options: {
+ configFile: 'protractor.conf.js'
+ },
+ chrome: {
+ options: {
+ args: {
+ browser: 'chrome'
+ }
+ }
+ }
+ },
+
+ env: {
+ test: {
+ NODE_ENV: 'test'
+ },
+ prod: {
+ NODE_ENV: 'production'
+ },
+ all: localConfig
+ },
+
+ // Compiles Jade to html
+ jade: {
+ compile: {
+ options: {
+ data: {
+ debug: false
+ }
+ },
+ files: [{
+ expand: true,
+ cwd: '<%= yeoman.client %>',
+ src: [
+ '{app,components}/**/*.jade'
+ ],
+ dest: '.tmp',
+ ext: '.html'
+ }]
+ }
+ },
+
+ // Compiles Stylus to CSS
+ stylus: {
+ server: {
+ options: {
+ paths: [
+ '<%= yeoman.client %>/bower_components',
+ '<%= yeoman.client %>/app',
+ '<%= yeoman.client %>/components'
+ ],
+ "include css": true
+ },
+ files: {
+ '.tmp/app/app.css' : '<%= yeoman.client %>/app/app.styl'
+ }
+ }
+ },
+
+ injector: {
+ options: {
+
+ },
+ // Inject application script files into index.html (doesn't include bower)
+ scripts: {
+ options: {
+ transform: function(filePath) {
+ filePath = filePath.replace('/client/', '');
+ filePath = filePath.replace('/.tmp/', '');
+ return '';
+ },
+ starttag: '',
+ endtag: ''
+ },
+ files: {
+ '<%= yeoman.client %>/index.html': [
+ ['{.tmp,<%= yeoman.client %>}/{app,components}/**/*.js',
+ '!{.tmp,<%= yeoman.client %>}/app/app.js',
+ '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.spec.js',
+ '!{.tmp,<%= yeoman.client %>}/{app,components}/**/*.mock.js']
+ ]
+ }
+ },
+
+ // Inject component styl into app.styl
+ stylus: {
+ options: {
+ transform: function(filePath) {
+ filePath = filePath.replace('/client/app/', '');
+ filePath = filePath.replace('/client/components/', '');
+ return '@import \'' + filePath + '\';';
+ },
+ starttag: '// injector',
+ endtag: '// endinjector'
+ },
+ files: {
+ '<%= yeoman.client %>/app/app.styl': [
+ '<%= yeoman.client %>/{app,components}/**/*.styl',
+ '!<%= yeoman.client %>/app/app.styl'
+ ]
+ }
+ },
+
+ // Inject component css into index.html
+ css: {
+ options: {
+ transform: function(filePath) {
+ filePath = filePath.replace('/client/', '');
+ filePath = filePath.replace('/.tmp/', '');
+ return '';
+ },
+ starttag: '',
+ endtag: ''
+ },
+ files: {
+ '<%= yeoman.client %>/index.html': [
+ '<%= yeoman.client %>/{app,components}/**/*.css'
+ ]
+ }
+ }
+ },
+ });
+
+ // Used for delaying livereload until after server has restarted
+ grunt.registerTask('wait', function () {
+ grunt.log.ok('Waiting for server reload...');
+
+ var done = this.async();
+
+ setTimeout(function () {
+ grunt.log.writeln('Done waiting!');
+ done();
+ }, 1500);
+ });
+
+ grunt.registerTask('express-keepalive', 'Keep grunt running', function() {
+ this.async();
+ });
+
+ grunt.registerTask('serve', function (target) {
+ if (target === 'dist') {
+ return grunt.task.run(['build', 'env:all', 'env:prod', 'express:prod', 'wait', 'open', 'express-keepalive']);
+ }
+
+ if (target === 'debug') {
+ return grunt.task.run([
+ 'clean:server',
+ 'env:all',
+ 'injector:stylus',
+ 'concurrent:server',
+ 'injector',
+ 'wiredep',
+ 'autoprefixer',
+ 'concurrent:debug'
+ ]);
+ }
+
+ grunt.task.run([
+ 'clean:server',
+ 'env:all',
+ 'injector:stylus',
+ 'concurrent:server',
+ 'injector',
+ 'wiredep',
+ 'autoprefixer',
+ 'express:dev',
+ 'wait',
+ 'open',
+ 'watch'
+ ]);
+ });
+
+ grunt.registerTask('server', function () {
+ grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
+ grunt.task.run(['serve']);
+ });
+
+ grunt.registerTask('test', function(target) {
+ if (target === 'server') {
+ return grunt.task.run([
+ 'env:all',
+ 'env:test',
+ 'mochaTest'
+ ]);
+ }
+
+ else if (target === 'client') {
+ return grunt.task.run([
+ 'clean:server',
+ 'env:all',
+ 'injector:stylus',
+ 'concurrent:test',
+ 'injector',
+ 'autoprefixer',
+ 'karma'
+ ]);
+ }
+
+ else if (target === 'e2e') {
+ return grunt.task.run([
+ 'clean:server',
+ 'env:all',
+ 'env:test',
+ 'injector:stylus',
+ 'concurrent:test',
+ 'injector',
+ 'wiredep',
+ 'autoprefixer',
+ 'express:dev',
+ 'protractor'
+ ]);
+ }
+
+ else grunt.task.run([
+ 'test:server',
+ 'test:client'
+ ]);
+ });
+
+ grunt.registerTask('build', [
+ 'clean:dist',
+ 'injector:stylus',
+ 'concurrent:dist',
+ 'injector',
+ 'wiredep',
+ 'useminPrepare',
+ 'autoprefixer',
+ 'ngtemplates',
+ 'concat',
+ 'ngAnnotate',
+ 'copy:dist',
+ 'cdnify',
+ 'cssmin',
+ 'uglify',
+ 'rev',
+ 'usemin'
+ ]);
+
+ grunt.registerTask('default', [
+ 'newer:jshint',
+ 'test',
+ 'build'
+ ]);
+};
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..9591157
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,662 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2651d0f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,30 @@
+_This is totally not ready yet. Not even pre-alpha. Only boilerplate scaffolding. Don't look!_
+
+# Manticore
+
+Realtime collaboration for rich office documents.
+
+## Setup
+
+1. Install [MongoDB](https://www.mongodb.org/) via your package manager.
+2. `npm install -g bower grunt-cli`
+3. Get server dependencies: `npm install`
+4. Get client dependencies: `bower install`
+
+## Run
+
+Executing `grunt serve` runs the node server which listens at `localhost:9000`.
+
+## Configure
+
+All environment variables are stored in `server/config/local.env.js`. Since it
+is unwise to checkin sensitive information into version control, this file is
+blacklisted in `.gitignore`. To get your own usable version, copy the existing
+`local.env.sample.js` under `server/config` to the aforementioned file, and then
+make the necessary modifications for your deployment.
+
+## Develop
+
+Install [AngularJS Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk)
+from the Chrome extensions store. This will let you inspect live Angular scopes,
+among other things.
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..24d76b8
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,22 @@
+{
+ "name": "manticore",
+ "version": "0.0.0",
+ "dependencies": {
+ "angular": ">=1.2.*",
+ "json3": "~3.3.1",
+ "es5-shim": "~3.0.1",
+ "jquery": "~1.11.0",
+ "bootstrap": "~3.1.1",
+ "angular-resource": ">=1.2.*",
+ "angular-cookies": ">=1.2.*",
+ "angular-sanitize": ">=1.2.*",
+ "angular-bootstrap": "~0.11.0",
+ "font-awesome": ">=4.1.0",
+ "lodash": "~2.4.1",
+ "angular-ui-router": "~0.2.10"
+ },
+ "devDependencies": {
+ "angular-mocks": ">=1.2.*",
+ "angular-scenario": ">=1.2.*"
+ }
+}
diff --git a/client/.htaccess b/client/.htaccess
new file mode 100644
index 0000000..cb84cb9
--- /dev/null
+++ b/client/.htaccess
@@ -0,0 +1,543 @@
+# Apache Configuration File
+
+# (!) Using `.htaccess` files slows down Apache, therefore, if you have access
+# to the main server config file (usually called `httpd.conf`), you should add
+# this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html.
+
+# ##############################################################################
+# # CROSS-ORIGIN RESOURCE SHARING (CORS) #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Cross-domain AJAX requests |
+# ------------------------------------------------------------------------------
+
+# Enable cross-origin AJAX requests.
+# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
+# http://enable-cors.org/
+
+#
+# Header set Access-Control-Allow-Origin "*"
+#
+
+# ------------------------------------------------------------------------------
+# | CORS-enabled images |
+# ------------------------------------------------------------------------------
+
+# Send the CORS header for images when browsers request it.
+# https://developer.mozilla.org/en/CORS_Enabled_Image
+# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
+# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/
+
+
+
+
+ SetEnvIf Origin ":" IS_CORS
+ Header set Access-Control-Allow-Origin "*" env=IS_CORS
+
+
+
+
+# ------------------------------------------------------------------------------
+# | Web fonts access |
+# ------------------------------------------------------------------------------
+
+# Allow access from all domains for web fonts
+
+
+
+ Header set Access-Control-Allow-Origin "*"
+
+
+
+
+# ##############################################################################
+# # ERRORS #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | 404 error prevention for non-existing redirected folders |
+# ------------------------------------------------------------------------------
+
+# Prevent Apache from returning a 404 error for a rewrite if a directory
+# with the same name does not exist.
+# http://httpd.apache.org/docs/current/content-negotiation.html#multiviews
+# http://www.webmasterworld.com/apache/3808792.htm
+
+Options -MultiViews
+
+# ------------------------------------------------------------------------------
+# | Custom error messages / pages |
+# ------------------------------------------------------------------------------
+
+# You can customize what Apache returns to the client in case of an error (see
+# http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.:
+
+ErrorDocument 404 /404.html
+
+
+# ##############################################################################
+# # INTERNET EXPLORER #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Better website experience |
+# ------------------------------------------------------------------------------
+
+# Force IE to render pages in the highest available mode in the various
+# cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf.
+
+
+ Header set X-UA-Compatible "IE=edge"
+ # `mod_headers` can't match based on the content-type, however, we only
+ # want to send this header for HTML pages and not for the other resources
+
+ Header unset X-UA-Compatible
+
+
+
+# ------------------------------------------------------------------------------
+# | Cookie setting from iframes |
+# ------------------------------------------------------------------------------
+
+# Allow cookies to be set from iframes in IE.
+
+#
+# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\""
+#
+
+# ------------------------------------------------------------------------------
+# | Screen flicker |
+# ------------------------------------------------------------------------------
+
+# Stop screen flicker in IE on CSS rollovers (this only works in
+# combination with the `ExpiresByType` directives for images from below).
+
+# BrowserMatch "MSIE" brokenvary=1
+# BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1
+# BrowserMatch "Opera" !brokenvary
+# SetEnvIf brokenvary 1 force-no-vary
+
+
+# ##############################################################################
+# # MIME TYPES AND ENCODING #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Proper MIME types for all files |
+# ------------------------------------------------------------------------------
+
+
+
+ # Audio
+ AddType audio/mp4 m4a f4a f4b
+ AddType audio/ogg oga ogg
+
+ # JavaScript
+ # Normalize to standard type (it's sniffed in IE anyways):
+ # http://tools.ietf.org/html/rfc4329#section-7.2
+ AddType application/javascript js jsonp
+ AddType application/json json
+
+ # Video
+ AddType video/mp4 mp4 m4v f4v f4p
+ AddType video/ogg ogv
+ AddType video/webm webm
+ AddType video/x-flv flv
+
+ # Web fonts
+ AddType application/font-woff woff
+ AddType application/vnd.ms-fontobject eot
+
+ # Browsers usually ignore the font MIME types and sniff the content,
+ # however, Chrome shows a warning if other MIME types are used for the
+ # following fonts.
+ AddType application/x-font-ttf ttc ttf
+ AddType font/opentype otf
+
+ # Make SVGZ fonts work on iPad:
+ # https://twitter.com/FontSquirrel/status/14855840545
+ AddType image/svg+xml svg svgz
+ AddEncoding gzip svgz
+
+ # Other
+ AddType application/octet-stream safariextz
+ AddType application/x-chrome-extension crx
+ AddType application/x-opera-extension oex
+ AddType application/x-shockwave-flash swf
+ AddType application/x-web-app-manifest+json webapp
+ AddType application/x-xpinstall xpi
+ AddType application/xml atom rdf rss xml
+ AddType image/webp webp
+ AddType image/x-icon ico
+ AddType text/cache-manifest appcache manifest
+ AddType text/vtt vtt
+ AddType text/x-component htc
+ AddType text/x-vcard vcf
+
+
+
+# ------------------------------------------------------------------------------
+# | UTF-8 encoding |
+# ------------------------------------------------------------------------------
+
+# Use UTF-8 encoding for anything served as `text/html` or `text/plain`.
+AddDefaultCharset utf-8
+
+# Force UTF-8 for certain file formats.
+
+ AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml
+
+
+
+# ##############################################################################
+# # URL REWRITES #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Rewrite engine |
+# ------------------------------------------------------------------------------
+
+# Turning on the rewrite engine and enabling the `FollowSymLinks` option is
+# necessary for the following directives to work.
+
+# If your web host doesn't allow the `FollowSymlinks` option, you may need to
+# comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the
+# performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks
+
+# Also, some cloud hosting services require `RewriteBase` to be set:
+# http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site
+
+
+ Options +FollowSymlinks
+ # Options +SymLinksIfOwnerMatch
+ RewriteEngine On
+ # RewriteBase /
+
+
+# ------------------------------------------------------------------------------
+# | Suppressing / Forcing the "www." at the beginning of URLs |
+# ------------------------------------------------------------------------------
+
+# The same content should never be available under two different URLs especially
+# not with and without "www." at the beginning. This can cause SEO problems
+# (duplicate content), therefore, you should choose one of the alternatives and
+# redirect the other one.
+
+# By default option 1 (no "www.") is activated:
+# http://no-www.org/faq.php?q=class_b
+
+# If you'd prefer to use option 2, just comment out all the lines from option 1
+# and uncomment the ones from option 2.
+
+# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME!
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Option 1: rewrite www.example.com → example.com
+
+
+ RewriteCond %{HTTPS} !=on
+ RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
+ RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Option 2: rewrite example.com → www.example.com
+
+# Be aware that the following might not be a good idea if you use "real"
+# subdomains for certain parts of your website.
+
+#
+# RewriteCond %{HTTPS} !=on
+# RewriteCond %{HTTP_HOST} !^www\..+$ [NC]
+# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
+#
+
+
+# ##############################################################################
+# # SECURITY #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Content Security Policy (CSP) |
+# ------------------------------------------------------------------------------
+
+# You can mitigate the risk of cross-site scripting and other content-injection
+# attacks by setting a Content Security Policy which whitelists trusted sources
+# of content for your site.
+
+# The example header below allows ONLY scripts that are loaded from the current
+# site's origin (no inline scripts, no CDN, etc). This almost certainly won't
+# work as-is for your site!
+
+# To get all the details you'll need to craft a reasonable policy for your site,
+# read: http://html5rocks.com/en/tutorials/security/content-security-policy (or
+# see the specification: http://w3.org/TR/CSP).
+
+#
+# Header set Content-Security-Policy "script-src 'self'; object-src 'self'"
+#
+# Header unset Content-Security-Policy
+#
+#
+
+# ------------------------------------------------------------------------------
+# | File access |
+# ------------------------------------------------------------------------------
+
+# Block access to directories without a default document.
+# Usually you should leave this uncommented because you shouldn't allow anyone
+# to surf through every directory on your server (which may includes rather
+# private places like the CMS's directories).
+
+
+ Options -Indexes
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Block access to hidden files and directories.
+# This includes directories used by version control systems such as Git and SVN.
+
+
+ RewriteCond %{SCRIPT_FILENAME} -d [OR]
+ RewriteCond %{SCRIPT_FILENAME} -f
+ RewriteRule "(^|/)\." - [F]
+
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Block access to backup and source files.
+# These files may be left by some text editors and can pose a great security
+# danger when anyone has access to them.
+
+
+ Order allow,deny
+ Deny from all
+ Satisfy All
+
+
+# ------------------------------------------------------------------------------
+# | Secure Sockets Layer (SSL) |
+# ------------------------------------------------------------------------------
+
+# Rewrite secure requests properly to prevent SSL certificate warnings, e.g.:
+# prevent `https://www.example.com` when your certificate only allows
+# `https://secure.example.com`.
+
+#
+# RewriteCond %{SERVER_PORT} !^443
+# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L]
+#
+
+# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+# Force client-side SSL redirection.
+
+# If a user types "example.com" in his browser, the above rule will redirect him
+# to the secure version of the site. That still leaves a window of opportunity
+# (the initial HTTP connection) for an attacker to downgrade or redirect the
+# request. The following header ensures that browser will ONLY connect to your
+# server via HTTPS, regardless of what the users type in the address bar.
+# http://www.html5rocks.com/en/tutorials/security/transport-layer-security/
+
+#
+# Header set Strict-Transport-Security max-age=16070400;
+#
+
+# ------------------------------------------------------------------------------
+# | Server software information |
+# ------------------------------------------------------------------------------
+
+# Avoid displaying the exact Apache version number, the description of the
+# generic OS-type and the information about Apache's compiled-in modules.
+
+# ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`!
+
+# ServerTokens Prod
+
+
+# ##############################################################################
+# # WEB PERFORMANCE #
+# ##############################################################################
+
+# ------------------------------------------------------------------------------
+# | Compression |
+# ------------------------------------------------------------------------------
+
+
+
+ # Force compression for mangled headers.
+ # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping
+
+
+ SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
+ RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
+
+
+
+ # Compress all output labeled with one of the following MIME-types
+ # (for Apache versions below 2.3.7, you don't need to enable `mod_filter`
+ # and can remove the `` and `` lines
+ # as `AddOutputFilterByType` is still in the core directives).
+
+ AddOutputFilterByType DEFLATE application/atom+xml \
+ application/javascript \
+ application/json \
+ application/rss+xml \
+ application/vnd.ms-fontobject \
+ application/x-font-ttf \
+ application/x-web-app-manifest+json \
+ application/xhtml+xml \
+ application/xml \
+ font/opentype \
+ image/svg+xml \
+ image/x-icon \
+ text/css \
+ text/html \
+ text/plain \
+ text/x-component \
+ text/xml
+
+
+
+
+# ------------------------------------------------------------------------------
+# | Content transformations |
+# ------------------------------------------------------------------------------
+
+# Prevent some of the mobile network providers from modifying the content of
+# your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5.
+
+#
+# Header set Cache-Control "no-transform"
+#
+
+# ------------------------------------------------------------------------------
+# | ETag removal |
+# ------------------------------------------------------------------------------
+
+# Since we're sending far-future expires headers (see below), ETags can
+# be removed: http://developer.yahoo.com/performance/rules.html#etags.
+
+# `FileETag None` is not enough for every server.
+
+ Header unset ETag
+
+
+FileETag None
+
+# ------------------------------------------------------------------------------
+# | Expires headers (for better cache control) |
+# ------------------------------------------------------------------------------
+
+# The following expires headers are set pretty far in the future. If you don't
+# control versioning with filename-based cache busting, consider lowering the
+# cache time for resources like CSS and JS to something like 1 week.
+
+
+
+ ExpiresActive on
+ ExpiresDefault "access plus 1 month"
+
+ # CSS
+ ExpiresByType text/css "access plus 1 year"
+
+ # Data interchange
+ ExpiresByType application/json "access plus 0 seconds"
+ ExpiresByType application/xml "access plus 0 seconds"
+ ExpiresByType text/xml "access plus 0 seconds"
+
+ # Favicon (cannot be renamed!)
+ ExpiresByType image/x-icon "access plus 1 week"
+
+ # HTML components (HTCs)
+ ExpiresByType text/x-component "access plus 1 month"
+
+ # HTML
+ ExpiresByType text/html "access plus 0 seconds"
+
+ # JavaScript
+ ExpiresByType application/javascript "access plus 1 year"
+
+ # Manifest files
+ ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"
+ ExpiresByType text/cache-manifest "access plus 0 seconds"
+
+ # Media
+ ExpiresByType audio/ogg "access plus 1 month"
+ ExpiresByType image/gif "access plus 1 month"
+ ExpiresByType image/jpeg "access plus 1 month"
+ ExpiresByType image/png "access plus 1 month"
+ ExpiresByType video/mp4 "access plus 1 month"
+ ExpiresByType video/ogg "access plus 1 month"
+ ExpiresByType video/webm "access plus 1 month"
+
+ # Web feeds
+ ExpiresByType application/atom+xml "access plus 1 hour"
+ ExpiresByType application/rss+xml "access plus 1 hour"
+
+ # Web fonts
+ ExpiresByType application/font-woff "access plus 1 month"
+ ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
+ ExpiresByType application/x-font-ttf "access plus 1 month"
+ ExpiresByType font/opentype "access plus 1 month"
+ ExpiresByType image/svg+xml "access plus 1 month"
+
+
+
+# ------------------------------------------------------------------------------
+# | Filename-based cache busting |
+# ------------------------------------------------------------------------------
+
+# If you're not using a build process to manage your filename version revving,
+# you might want to consider enabling the following directives to route all
+# requests such as `/css/style.12345.css` to `/css/style.css`.
+
+# To understand why this is important and a better idea than `*.css?v231`, read:
+# http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring
+
+#
+# RewriteCond %{REQUEST_FILENAME} !-f
+# RewriteCond %{REQUEST_FILENAME} !-d
+# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L]
+#
+
+# ------------------------------------------------------------------------------
+# | File concatenation |
+# ------------------------------------------------------------------------------
+
+# Allow concatenation from within specific CSS and JS files, e.g.:
+# Inside of `script.combined.js` you could have
+#
+#
+# and they would be included into this single file.
+
+#
+#
+# Options +Includes
+# AddOutputFilterByType INCLUDES application/javascript application/json
+# SetOutputFilter INCLUDES
+#
+#
+# Options +Includes
+# AddOutputFilterByType INCLUDES text/css
+# SetOutputFilter INCLUDES
+#
+#
+
+# ------------------------------------------------------------------------------
+# | Persistent connections |
+# ------------------------------------------------------------------------------
+
+# Allow multiple requests to be sent over the same TCP connection:
+# http://httpd.apache.org/docs/current/en/mod/core.html#keepalive.
+
+# Enable if you serve a lot of static content but, be aware of the
+# possible disadvantages!
+
+#
+# Header set Connection Keep-Alive
+#
diff --git a/client/.jshintrc b/client/.jshintrc
new file mode 100644
index 0000000..52c6a6d
--- /dev/null
+++ b/client/.jshintrc
@@ -0,0 +1,38 @@
+{
+ "node": true,
+ "browser": true,
+ "esnext": true,
+ "bitwise": true,
+ "camelcase": true,
+ "curly": true,
+ "eqeqeq": true,
+ "immed": true,
+ "indent": 2,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "quotmark": "single",
+ "regexp": true,
+ "undef": true,
+ "unused": true,
+ "strict": true,
+ "trailing": true,
+ "smarttabs": true,
+ "globals": {
+ "jQuery": true,
+ "angular": true,
+ "console": true,
+ "$": true,
+ "_": true,
+ "moment": true,
+ "describe": true,
+ "beforeEach": true,
+ "module": true,
+ "inject": true,
+ "it": true,
+ "expect": true,
+ "browser": true,
+ "element": true,
+ "by": true
+ }
+}
diff --git a/client/app/account/account.js b/client/app/account/account.js
new file mode 100644
index 0000000..defbb45
--- /dev/null
+++ b/client/app/account/account.js
@@ -0,0 +1,22 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .config(function ($stateProvider) {
+ $stateProvider
+ .state('login', {
+ url: '/login',
+ templateUrl: 'app/account/login/login.html',
+ controller: 'LoginCtrl'
+ })
+ .state('signup', {
+ url: '/signup',
+ templateUrl: 'app/account/signup/signup.html',
+ controller: 'SignupCtrl'
+ })
+ .state('settings', {
+ url: '/settings',
+ templateUrl: 'app/account/settings/settings.html',
+ controller: 'SettingsCtrl',
+ authenticate: true
+ });
+ });
\ No newline at end of file
diff --git a/client/app/account/login/login.controller.js b/client/app/account/login/login.controller.js
new file mode 100644
index 0000000..7742b60
--- /dev/null
+++ b/client/app/account/login/login.controller.js
@@ -0,0 +1,26 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .controller('LoginCtrl', function ($scope, Auth, $location) {
+ $scope.user = {};
+ $scope.errors = {};
+
+ $scope.login = function(form) {
+ $scope.submitted = true;
+
+ if(form.$valid) {
+ Auth.login({
+ email: $scope.user.email,
+ password: $scope.user.password
+ })
+ .then( function() {
+ // Logged in, redirect to home
+ $location.path('/');
+ })
+ .catch( function(err) {
+ $scope.errors.other = err.message;
+ });
+ }
+ };
+
+ });
diff --git a/client/app/account/login/login.jade b/client/app/account/login/login.jade
new file mode 100644
index 0000000..9f5a6a0
--- /dev/null
+++ b/client/app/account/login/login.jade
@@ -0,0 +1,40 @@
+div(ng-include='"components/navbar/navbar.html"')
+.container
+ .row
+ .col-sm-12
+ h1 Login
+ p
+ | Accounts are reset on server restart from
+ code server/config/seed.js
+ | . Default account is
+ code test@test.com
+ | /
+ code test
+ p
+ | Admin account is
+ code admin@admin.com
+ | /
+ code admin
+
+ .col-sm-12
+ form.form(name='form', ng-submit='login(form)', novalidate='')
+ .form-group
+ label Email
+ input.form-control(type='text', name='email', ng-model='user.email')
+ .form-group
+ label Password
+ input.form-control(type='password', name='password', ng-model='user.password')
+
+ .form-group.has-error
+ p.help-block(ng-show='form.email.$error.required && form.password.$error.required && submitted')
+ | Please enter your email and password.
+ p.help-block {{ errors.other }}
+
+ div
+ button.btn.btn-inverse.btn-lg.btn-login(type='submit')
+ | Login
+ = ' '
+ a.btn.btn-default.btn-lg.btn-register(href='/signup')
+ | Register
+
+ hr
diff --git a/client/app/account/login/login.styl b/client/app/account/login/login.styl
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/client/app/account/login/login.styl
@@ -0,0 +1 @@
+
diff --git a/client/app/account/settings/settings.controller.js b/client/app/account/settings/settings.controller.js
new file mode 100644
index 0000000..8187024
--- /dev/null
+++ b/client/app/account/settings/settings.controller.js
@@ -0,0 +1,21 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .controller('SettingsCtrl', function ($scope, User, Auth) {
+ $scope.errors = {};
+
+ $scope.changePassword = function(form) {
+ $scope.submitted = true;
+ if(form.$valid) {
+ Auth.changePassword( $scope.user.oldPassword, $scope.user.newPassword )
+ .then( function() {
+ $scope.message = 'Password successfully changed.';
+ })
+ .catch( function() {
+ form.password.$setValidity('mongoose', false);
+ $scope.errors.other = 'Incorrect password';
+ $scope.message = '';
+ });
+ }
+ };
+ });
diff --git a/client/app/account/settings/settings.jade b/client/app/account/settings/settings.jade
new file mode 100644
index 0000000..2dc55d4
--- /dev/null
+++ b/client/app/account/settings/settings.jade
@@ -0,0 +1,21 @@
+div(ng-include='"components/navbar/navbar.html"')
+.container
+ .row
+ .col-sm-12
+ h1 Change Password
+ .col-sm-12
+ form.form(name='form', ng-submit='changePassword(form)', novalidate='')
+ .form-group
+ label Current Password
+ input.form-control(type='password', name='password', ng-model='user.oldPassword', mongoose-error='')
+ p.help-block(ng-show='form.password.$error.mongoose')
+ | {{ errors.other }}
+ .form-group
+ label New Password
+ input.form-control(type='password', name='newPassword', ng-model='user.newPassword', ng-minlength='3', required='')
+ p.help-block(ng-show='(form.newPassword.$error.minlength || form.newPassword.$error.required) && (form.newPassword.$dirty || submitted)')
+ | Password must be at least 3 characters.
+
+ p.help-block {{ message }}
+
+ button.btn.btn-lg.btn-primary(type='submit') Save changes
diff --git a/client/app/account/signup/signup.controller.js b/client/app/account/signup/signup.controller.js
new file mode 100644
index 0000000..2097c0e
--- /dev/null
+++ b/client/app/account/signup/signup.controller.js
@@ -0,0 +1,34 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .controller('SignupCtrl', function ($scope, Auth, $location) {
+ $scope.user = {};
+ $scope.errors = {};
+
+ $scope.register = function(form) {
+ $scope.submitted = true;
+
+ if(form.$valid) {
+ Auth.createUser({
+ name: $scope.user.name,
+ email: $scope.user.email,
+ password: $scope.user.password
+ })
+ .then( function() {
+ // Account created, redirect to home
+ $location.path('/');
+ })
+ .catch( function(err) {
+ err = err.data;
+ $scope.errors = {};
+
+ // Update validity of form fields that match the mongoose errors
+ angular.forEach(err.errors, function(error, field) {
+ form[field].$setValidity('mongoose', false);
+ $scope.errors[field] = error.message;
+ });
+ });
+ }
+ };
+
+ });
diff --git a/client/app/account/signup/signup.jade b/client/app/account/signup/signup.jade
new file mode 100644
index 0000000..d216832
--- /dev/null
+++ b/client/app/account/signup/signup.jade
@@ -0,0 +1,43 @@
+div(ng-include='"components/navbar/navbar.html"')
+.container
+ .row
+ .col-sm-12
+ h1 Sign up
+ .col-sm-12
+ form.form(name='form', ng-submit='register(form)', novalidate='')
+ .form-group(ng-class='{ "has-success": form.name.$valid && submitted,\
+ "has-error": form.name.$invalid && submitted }')
+ label Name
+ input.form-control(type='text', name='name', ng-model='user.name', required='')
+ p.help-block(ng-show='form.name.$error.required && submitted')
+ | A name is required
+
+ .form-group(ng-class='{ "has-success": form.email.$valid && submitted,\
+ "has-error": form.email.$invalid && submitted }')
+ label Email
+ input.form-control(type='email', name='email', ng-model='user.email', required='', mongoose-error='')
+ p.help-block(ng-show='form.email.$error.email && submitted')
+ | Doesn't look like a valid email.
+ p.help-block(ng-show='form.email.$error.required && submitted')
+ | What's your email address?
+ p.help-block(ng-show='form.email.$error.mongoose')
+ | {{ errors.email }}
+
+ .form-group(ng-class='{ "has-success": form.password.$valid && submitted,\
+ "has-error": form.password.$invalid && submitted }')
+ label Password
+ input.form-control(type='password', name='password', ng-model='user.password', ng-minlength='3', required='', mongoose-error='')
+ p.help-block(ng-show='(form.password.$error.minlength || form.password.$error.required) && submitted')
+ | Password must be at least 3 characters.
+ p.help-block(ng-show='form.password.$error.mongoose')
+ | {{ errors.password }}
+
+ div
+ button.btn.btn-inverse.btn-lg.btn-login(type='submit')
+ | Sign up
+ = ' '
+ a.btn.btn-default.btn-lg.btn-register(href='/login')
+ | Login
+
+
+ hr
diff --git a/client/app/admin/admin.controller.js b/client/app/admin/admin.controller.js
new file mode 100644
index 0000000..b2d24af
--- /dev/null
+++ b/client/app/admin/admin.controller.js
@@ -0,0 +1,17 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .controller('AdminCtrl', function ($scope, $http, Auth, User) {
+
+ // Use the User $resource to fetch all users
+ $scope.users = User.query();
+
+ $scope.delete = function(user) {
+ User.remove({ id: user._id });
+ angular.forEach($scope.users, function(u, i) {
+ if (u === user) {
+ $scope.users.splice(i, 1);
+ }
+ });
+ };
+ });
diff --git a/client/app/admin/admin.jade b/client/app/admin/admin.jade
new file mode 100644
index 0000000..fd80a0b
--- /dev/null
+++ b/client/app/admin/admin.jade
@@ -0,0 +1,11 @@
+div(ng-include='"components/navbar/navbar.html"')
+.container
+ p
+ | The delete user and user index api routes are restricted to users with the 'admin' role.
+ ul.list-group
+ li.list-group-item(ng-repeat='user in users')
+ strong {{user.name}}
+ br
+ span.text-muted {{user.email}}
+ a.trash(ng-click='delete(user)')
+ span.glyphicon.glyphicon-trash.pull-right
\ No newline at end of file
diff --git a/client/app/admin/admin.js b/client/app/admin/admin.js
new file mode 100644
index 0000000..d82e625
--- /dev/null
+++ b/client/app/admin/admin.js
@@ -0,0 +1,11 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .config(function ($stateProvider) {
+ $stateProvider
+ .state('admin', {
+ url: '/admin',
+ templateUrl: 'app/admin/admin.html',
+ controller: 'AdminCtrl'
+ });
+ });
\ No newline at end of file
diff --git a/client/app/admin/admin.styl b/client/app/admin/admin.styl
new file mode 100644
index 0000000..d57e50d
--- /dev/null
+++ b/client/app/admin/admin.styl
@@ -0,0 +1,2 @@
+.trash
+ color rgb(209, 91, 71)
\ No newline at end of file
diff --git a/client/app/app.js b/client/app/app.js
new file mode 100644
index 0000000..6b2c7a0
--- /dev/null
+++ b/client/app/app.js
@@ -0,0 +1,53 @@
+'use strict';
+
+angular.module('manticoreApp', [
+ 'ngCookies',
+ 'ngResource',
+ 'ngSanitize',
+ 'ui.router',
+ 'ui.bootstrap'
+])
+ .config(function ($stateProvider, $urlRouterProvider, $locationProvider, $httpProvider) {
+ $urlRouterProvider
+ .otherwise('/');
+
+ $locationProvider.html5Mode(true);
+ $httpProvider.interceptors.push('authInterceptor');
+ })
+
+ .factory('authInterceptor', function ($rootScope, $q, $cookieStore, $location) {
+ return {
+ // Add authorization token to headers
+ request: function (config) {
+ config.headers = config.headers || {};
+ if ($cookieStore.get('token')) {
+ config.headers.Authorization = 'Bearer ' + $cookieStore.get('token');
+ }
+ return config;
+ },
+
+ // Intercept 401s and redirect you to login
+ responseError: function(response) {
+ if(response.status === 401) {
+ $location.path('/login');
+ // remove any stale tokens
+ $cookieStore.remove('token');
+ return $q.reject(response);
+ }
+ else {
+ return $q.reject(response);
+ }
+ }
+ };
+ })
+
+ .run(function ($rootScope, $location, Auth) {
+ // Redirect to login if route requires auth and you're not logged in
+ $rootScope.$on('$stateChangeStart', function (event, next) {
+ Auth.isLoggedInAsync(function(loggedIn) {
+ if (next.authenticate && !loggedIn) {
+ $location.path('/login');
+ }
+ });
+ });
+ });
\ No newline at end of file
diff --git a/client/app/app.styl b/client/app/app.styl
new file mode 100644
index 0000000..26eee00
--- /dev/null
+++ b/client/app/app.styl
@@ -0,0 +1,46 @@
+@import "font-awesome/css/font-awesome.css"
+@import "bootstrap/dist/css/bootstrap.css"
+
+//
+// Bootstrap Fonts
+//
+
+@font-face
+ font-family: 'Glyphicons Halflings'
+ src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot')
+ src: url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
+ url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.woff') format('woff'),
+ url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
+ url('../bower_components/bootstrap/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
+
+//
+// Font Awesome Fonts
+//
+
+@font-face
+ font-family: 'FontAwesome'
+ src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?v=4.1.0')
+ src: url('../bower_components/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'),
+ url('../bower_components/font-awesome/fonts/fontawesome-webfont.woff?v=4.1.0') format('woff'),
+ url('../bower_components/font-awesome/fonts/fontawesome-webfont.ttf?v=4.1.0') format('truetype'),
+ url('../bower_components/font-awesome/fonts/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg');
+ font-weight: normal
+ font-style: normal
+
+//
+// App-wide Styles
+//
+
+.browsehappy
+ background #ccc
+ color #000
+ margin 0.2em 0
+ padding 0.2em 0
+
+// Component styles are injected through grunt
+// injector
+@import 'account/login/login.styl';
+@import 'admin/admin.styl';
+@import 'main/main.styl';
+@import 'modal/modal.styl';
+// endinjector
\ No newline at end of file
diff --git a/client/app/main/main.controller.js b/client/app/main/main.controller.js
new file mode 100644
index 0000000..2c1ca57
--- /dev/null
+++ b/client/app/main/main.controller.js
@@ -0,0 +1,23 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .controller('MainCtrl', function ($scope, $http, Auth) {
+ $scope.documents = [];
+ $scope.isLoggedIn = Auth.isLoggedIn;
+
+ $http.get('/api/documents').success(function(documents) {
+ $scope.documents = documents;
+ });
+
+ $scope.addDocument = function() {
+ if($scope.newDocument === '') {
+ return;
+ }
+ $http.post('/api/documents', { name: $scope.newDocument });
+ $scope.newDocument = '';
+ };
+
+ $scope.deleteDocument = function(document) {
+ $http.delete('/api/documents/' + document._id);
+ };
+ });
diff --git a/client/app/main/main.controller.spec.js b/client/app/main/main.controller.spec.js
new file mode 100644
index 0000000..65a1e3e
--- /dev/null
+++ b/client/app/main/main.controller.spec.js
@@ -0,0 +1,28 @@
+'use strict';
+
+describe('Controller: MainCtrl', function () {
+
+ // load the controller's module
+ beforeEach(module('manticoreApp'));
+
+ var MainCtrl,
+ scope,
+ $httpBackend;
+
+ // Initialize the controller and a mock scope
+ beforeEach(inject(function (_$httpBackend_, $controller, $rootScope) {
+ $httpBackend = _$httpBackend_;
+ $httpBackend.expectGET('/api/documents')
+ .respond(['HTML5 Boilerplate', 'AngularJS', 'Karma', 'Express']);
+
+ scope = $rootScope.$new();
+ MainCtrl = $controller('MainCtrl', {
+ $scope: scope
+ });
+ }));
+
+ it('should attach a list of documents to the scope', function () {
+ $httpBackend.flush();
+ expect(scope.documents.length).toBe(4);
+ });
+});
diff --git a/client/app/main/main.jade b/client/app/main/main.jade
new file mode 100644
index 0000000..9097617
--- /dev/null
+++ b/client/app/main/main.jade
@@ -0,0 +1,23 @@
+div(ng-include='"components/navbar/navbar.html"')
+
+header#banner.hero-unit(ng-hide='isLoggedIn() ')
+ .container
+ h1 Manticore
+ p.lead Realtime collaboration for rich office documents.
+ img(src='assets/images/manticore.jpg', alt='Manticore')
+
+.container(ng-hide='!isLoggedIn()')
+ .row
+ .col-lg-12
+ h1.page-header Documents
+ ul.nav.nav-tabs.nav-stacked.col-md-4.col-lg-4.col-sm-6(ng-repeat='document in documents')
+ li
+ a(href='#', tooltip='{{document.info}}')
+ | {{document.name}}
+
+//- footer.footer
+ .container
+ p
+ | Manticore
+ = ' | '
+ a(href='https://github.com/adityab/Manticure/issues?state=open') Issues
diff --git a/client/app/main/main.js b/client/app/main/main.js
new file mode 100644
index 0000000..392014e
--- /dev/null
+++ b/client/app/main/main.js
@@ -0,0 +1,11 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .config(function ($stateProvider) {
+ $stateProvider
+ .state('main', {
+ url: '/',
+ templateUrl: 'app/main/main.html',
+ controller: 'MainCtrl'
+ });
+ });
\ No newline at end of file
diff --git a/client/app/main/main.styl b/client/app/main/main.styl
new file mode 100644
index 0000000..7ace56d
--- /dev/null
+++ b/client/app/main/main.styl
@@ -0,0 +1,26 @@
+.document-form
+ margin 20px 0
+
+#banner
+ border-bottom none
+ margin-top -20px
+
+#banner h1
+ font-size 60px
+ letter-spacing -1px
+ line-height 1
+
+.hero-unit
+ color #000
+ padding 30px 15px
+ position relative
+ text-align center
+ text-shadow 0 1px 0 rgba(0, 0, 0, 0.1)
+ img
+ max-width: 100%;
+
+.footer
+ border-top 1px solid #E5E5E5
+ margin-top 70px
+ padding 30px 0
+ text-align center
diff --git a/client/assets/images/manticore.jpg b/client/assets/images/manticore.jpg
new file mode 100644
index 0000000..090bc06
Binary files /dev/null and b/client/assets/images/manticore.jpg differ
diff --git a/client/components/auth/auth.service.js b/client/components/auth/auth.service.js
new file mode 100644
index 0000000..54ef5e3
--- /dev/null
+++ b/client/components/auth/auth.service.js
@@ -0,0 +1,146 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .factory('Auth', function Auth($location, $rootScope, $http, User, $cookieStore, $q) {
+ var currentUser = {};
+ if($cookieStore.get('token')) {
+ currentUser = User.get();
+ }
+
+ return {
+
+ /**
+ * Authenticate user and save token
+ *
+ * @param {Object} user - login info
+ * @param {Function} callback - optional
+ * @return {Promise}
+ */
+ login: function(user, callback) {
+ var cb = callback || angular.noop;
+ var deferred = $q.defer();
+
+ $http.post('/auth/local', {
+ email: user.email,
+ password: user.password
+ }).
+ success(function(data) {
+ $cookieStore.put('token', data.token);
+ currentUser = User.get();
+ deferred.resolve(data);
+ return cb();
+ }).
+ error(function(err) {
+ this.logout();
+ deferred.reject(err);
+ return cb(err);
+ }.bind(this));
+
+ return deferred.promise;
+ },
+
+ /**
+ * Delete access token and user info
+ *
+ * @param {Function}
+ */
+ logout: function() {
+ $cookieStore.remove('token');
+ currentUser = {};
+ },
+
+ /**
+ * Create a new user
+ *
+ * @param {Object} user - user info
+ * @param {Function} callback - optional
+ * @return {Promise}
+ */
+ createUser: function(user, callback) {
+ var cb = callback || angular.noop;
+
+ return User.save(user,
+ function(data) {
+ $cookieStore.put('token', data.token);
+ currentUser = User.get();
+ return cb(user);
+ },
+ function(err) {
+ this.logout();
+ return cb(err);
+ }.bind(this)).$promise;
+ },
+
+ /**
+ * Change password
+ *
+ * @param {String} oldPassword
+ * @param {String} newPassword
+ * @param {Function} callback - optional
+ * @return {Promise}
+ */
+ changePassword: function(oldPassword, newPassword, callback) {
+ var cb = callback || angular.noop;
+
+ return User.changePassword({ id: currentUser._id }, {
+ oldPassword: oldPassword,
+ newPassword: newPassword
+ }, function(user) {
+ return cb(user);
+ }, function(err) {
+ return cb(err);
+ }).$promise;
+ },
+
+ /**
+ * Gets all available info on authenticated user
+ *
+ * @return {Object} user
+ */
+ getCurrentUser: function() {
+ return currentUser;
+ },
+
+ /**
+ * Check if a user is logged in
+ *
+ * @return {Boolean}
+ */
+ isLoggedIn: function() {
+ return currentUser.hasOwnProperty('role');
+ },
+
+ /**
+ * Waits for currentUser to resolve before checking if user is logged in
+ */
+ isLoggedInAsync: function(cb) {
+ if(currentUser.hasOwnProperty('$promise')) {
+ currentUser.$promise.then(function() {
+ cb(true);
+ }).catch(function() {
+ cb(false);
+ });
+ } else if(currentUser.hasOwnProperty('role')) {
+ cb(true);
+ } else {
+ cb(false);
+ }
+ },
+
+ /**
+ * Check if a user is an admin
+ *
+ * @return {Boolean}
+ */
+ isAdmin: function() {
+ return currentUser.role === 'admin';
+ },
+
+ /**
+ * Get auth token
+ */
+ getToken: function() {
+ return $cookieStore.get('token');
+ }
+ };
+ });
diff --git a/client/components/auth/user.service.js b/client/components/auth/user.service.js
new file mode 100644
index 0000000..e3e5ffe
--- /dev/null
+++ b/client/components/auth/user.service.js
@@ -0,0 +1,22 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .factory('User', function ($resource) {
+ return $resource('/api/users/:id/:controller', {
+ id: '@_id'
+ },
+ {
+ changePassword: {
+ method: 'PUT',
+ params: {
+ controller:'password'
+ }
+ },
+ get: {
+ method: 'GET',
+ params: {
+ id:'me'
+ }
+ }
+ });
+ });
diff --git a/client/components/modal/modal.jade b/client/components/modal/modal.jade
new file mode 100644
index 0000000..71b4321
--- /dev/null
+++ b/client/components/modal/modal.jade
@@ -0,0 +1,8 @@
+.modal-header
+ button.close(ng-if='modal.dismissable', type='button', ng-click='$dismiss()') ×
+ h4.modal-title(ng-if='modal.title', ng-bind='modal.title')
+.modal-body
+ p(ng-if='modal.text', ng-bind='modal.text')
+ div(ng-if='modal.html', ng-bind-html='modal.html')
+.modal-footer
+ button.btn(ng-repeat='button in modal.buttons', ng-class='button.classes', ng-click='button.click($event)', ng-bind='button.text')
diff --git a/client/components/modal/modal.service.js b/client/components/modal/modal.service.js
new file mode 100644
index 0000000..036abe0
--- /dev/null
+++ b/client/components/modal/modal.service.js
@@ -0,0 +1,77 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .factory('Modal', function ($rootScope, $modal) {
+ /**
+ * Opens a modal
+ * @param {Object} scope - an object to be merged with modal's scope
+ * @param {String} modalClass - (optional) class(es) to be applied to the modal
+ * @return {Object} - the instance $modal.open() returns
+ */
+ function openModal(scope, modalClass) {
+ var modalScope = $rootScope.$new();
+ scope = scope || {};
+ modalClass = modalClass || 'modal-default';
+
+ angular.extend(modalScope, scope);
+
+ return $modal.open({
+ templateUrl: 'components/modal/modal.html',
+ windowClass: modalClass,
+ scope: modalScope
+ });
+ }
+
+ // Public API here
+ return {
+
+ /* Confirmation modals */
+ confirm: {
+
+ /**
+ * Create a function to open a delete confirmation modal (ex. ng-click='myModalFn(name, arg1, arg2...)')
+ * @param {Function} del - callback, ran when delete is confirmed
+ * @return {Function} - the function to open the modal (ex. myModalFn)
+ */
+ delete: function(del) {
+ del = del || angular.noop;
+
+ /**
+ * Open a delete confirmation modal
+ * @param {String} name - name or info to show on modal
+ * @param {All} - any additional args are passed staight to del callback
+ */
+ return function() {
+ var args = Array.prototype.slice.call(arguments),
+ name = args.shift(),
+ deleteModal;
+
+ deleteModal = openModal({
+ modal: {
+ dismissable: true,
+ title: 'Confirm Delete',
+ html: '
Are you sure you want to delete ' + name + ' ?
',
+ buttons: [{
+ classes: 'btn-danger',
+ text: 'Delete',
+ click: function(e) {
+ deleteModal.close(e);
+ }
+ }, {
+ classes: 'btn-default',
+ text: 'Cancel',
+ click: function(e) {
+ deleteModal.dismiss(e);
+ }
+ }]
+ }
+ }, 'modal-danger');
+
+ deleteModal.result.then(function(event) {
+ del.apply(event, args);
+ });
+ };
+ }
+ }
+ };
+ });
diff --git a/client/components/modal/modal.styl b/client/components/modal/modal.styl
new file mode 100644
index 0000000..d394ee0
--- /dev/null
+++ b/client/components/modal/modal.styl
@@ -0,0 +1,23 @@
+.modal-primary
+.modal-info
+.modal-success
+.modal-warning
+.modal-danger
+ .modal-header
+ color #fff
+ border-radius 5px 5px 0 0
+
+.modal-primary .modal-header
+ background #428bca
+
+.modal-info .modal-header
+ background #5bc0de
+
+.modal-success .modal-header
+ background #5cb85c
+
+.modal-warning .modal-header
+ background #f0ad4e
+
+.modal-danger .modal-header
+ background #d9534f
diff --git a/client/components/mongoose-error/mongoose-error.directive.js b/client/components/mongoose-error/mongoose-error.directive.js
new file mode 100644
index 0000000..77babaf
--- /dev/null
+++ b/client/components/mongoose-error/mongoose-error.directive.js
@@ -0,0 +1,17 @@
+'use strict';
+
+/**
+ * Removes server error when user updates input
+ */
+angular.module('manticoreApp')
+ .directive('mongooseError', function () {
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function(scope, element, attrs, ngModel) {
+ element.on('keydown', function() {
+ return ngModel.$setValidity('mongoose', true);
+ });
+ }
+ };
+ });
\ No newline at end of file
diff --git a/client/components/navbar/navbar.controller.js b/client/components/navbar/navbar.controller.js
new file mode 100644
index 0000000..349b267
--- /dev/null
+++ b/client/components/navbar/navbar.controller.js
@@ -0,0 +1,23 @@
+'use strict';
+
+angular.module('manticoreApp')
+ .controller('NavbarCtrl', function ($scope, $location, Auth) {
+ $scope.menu = [{
+ 'title': 'Home',
+ 'link': '/'
+ }];
+
+ $scope.isCollapsed = true;
+ $scope.isLoggedIn = Auth.isLoggedIn;
+ $scope.isAdmin = Auth.isAdmin;
+ $scope.getCurrentUser = Auth.getCurrentUser;
+
+ $scope.logout = function() {
+ Auth.logout();
+ $location.path('/login');
+ };
+
+ $scope.isActive = function(route) {
+ return route === $location.path();
+ };
+ });
\ No newline at end of file
diff --git a/client/components/navbar/navbar.jade b/client/components/navbar/navbar.jade
new file mode 100644
index 0000000..f39c338
--- /dev/null
+++ b/client/components/navbar/navbar.jade
@@ -0,0 +1,34 @@
+div.navbar.navbar-default.navbar-static-top(ng-controller='NavbarCtrl')
+ div.container
+ div.navbar-header
+ button.navbar-toggle(type='button', ng-click='isCollapsed = !isCollapsed')
+ span.sr-only Toggle navigation
+ span.icon-bar
+ span.icon-bar
+ span.icon-bar
+ a.navbar-brand(href='/') manticore
+
+ div#navbar-main.navbar-collapse.collapse(collapse='isCollapsed')
+ ul.nav.navbar-nav
+ li(ng-repeat='item in menu', ng-class='{active: isActive(item.link)}')
+ a(ng-href='{{item.link}}') {{item.title}}
+
+ li(ng-show='isAdmin()', ng-class='{active: isActive("/admin")}')
+ a(href='/admin') Admin
+
+ ul.nav.navbar-nav.navbar-right
+ li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/signup")}')
+ a(href='/signup') Sign up
+
+ li(ng-hide='isLoggedIn()', ng-class='{active: isActive("/login")}')
+ a(href='/login') Login
+
+ li(ng-show='isLoggedIn()')
+ p.navbar-text Hello {{ getCurrentUser().name }}
+
+ li(ng-show='isLoggedIn()', ng-class='{active: isActive("/settings")}')
+ a(href='/settings')
+ span.glyphicon.glyphicon-cog
+
+ li(ng-show='isLoggedIn()', ng-class='{active: isActive("/logout")}')
+ a(href='', ng-click='logout()') Logout
\ No newline at end of file
diff --git a/client/favicon.ico b/client/favicon.ico
new file mode 100644
index 0000000..8a163fb
Binary files /dev/null and b/client/favicon.ico differ
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..14786c6
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/robots.txt b/client/robots.txt
new file mode 100644
index 0000000..9417495
--- /dev/null
+++ b/client/robots.txt
@@ -0,0 +1,3 @@
+# robotstxt.org
+
+User-agent: *
diff --git a/e2e/main/main.po.js b/e2e/main/main.po.js
new file mode 100644
index 0000000..6718608
--- /dev/null
+++ b/e2e/main/main.po.js
@@ -0,0 +1,15 @@
+/**
+ * This file uses the Page Object pattern to define the main page for tests
+ * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ
+ */
+
+'use strict';
+
+var MainPage = function() {
+ this.heroEl = element(by.css('.hero-unit'));
+ this.h1El = this.heroEl.element(by.css('h1'));
+ this.imgEl = this.heroEl.element(by.css('img'));
+};
+
+module.exports = new MainPage();
+
diff --git a/e2e/main/main.spec.js b/e2e/main/main.spec.js
new file mode 100644
index 0000000..61745a8
--- /dev/null
+++ b/e2e/main/main.spec.js
@@ -0,0 +1,16 @@
+'use strict';
+
+describe('Main View', function() {
+ var page;
+
+ beforeEach(function() {
+ browser.get('/');
+ page = require('./main.po');
+ });
+
+ it('should include jumbotron with correct data', function() {
+ expect(page.h1El.getText()).toBe('\'Allo, \'Allo!');
+ expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/);
+ expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman');
+ });
+});
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..d698526
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,80 @@
+// Karma configuration
+// http://karma-runner.github.io/0.10/config/configuration-file.html
+
+module.exports = function(config) {
+ config.set({
+ // base path, that will be used to resolve files and exclude
+ basePath: '',
+
+ // testing framework to use (jasmine/mocha/qunit/...)
+ frameworks: ['jasmine'],
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'client/bower_components/jquery/dist/jquery.js',
+ 'client/bower_components/angular/angular.js',
+ 'client/bower_components/angular-mocks/angular-mocks.js',
+ 'client/bower_components/angular-resource/angular-resource.js',
+ 'client/bower_components/angular-cookies/angular-cookies.js',
+ 'client/bower_components/angular-sanitize/angular-sanitize.js',
+ 'client/bower_components/angular-route/angular-route.js',
+ 'client/bower_components/angular-bootstrap/ui-bootstrap-tpls.js',
+ 'client/bower_components/lodash/dist/lodash.compat.js',
+ 'client/bower_components/angular-ui-router/release/angular-ui-router.js',
+ 'client/app/app.js',
+ 'client/app/app.coffee',
+ 'client/app/**/*.js',
+ 'client/app/**/*.coffee',
+ 'client/components/**/*.js',
+ 'client/components/**/*.coffee',
+ 'client/app/**/*.jade',
+ 'client/components/**/*.jade',
+ 'client/app/**/*.html',
+ 'client/components/**/*.html'
+ ],
+
+ preprocessors: {
+ '**/*.jade': 'ng-jade2js',
+ '**/*.html': 'html2js',
+ '**/*.coffee': 'coffee',
+ },
+
+ ngHtml2JsPreprocessor: {
+ stripPrefix: 'client/'
+ },
+
+ ngJade2JsPreprocessor: {
+ stripPrefix: 'client/'
+ },
+
+ // list of files / patterns to exclude
+ exclude: [],
+
+ // web server port
+ port: 8080,
+
+ // level of logging
+ // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+
+ // Start these browsers, currently available:
+ // - Chrome
+ // - ChromeCanary
+ // - Firefox
+ // - Opera
+ // - Safari (only Mac)
+ // - PhantomJS
+ // - IE (only Windows)
+ browsers: ['PhantomJS'],
+
+
+ // Continuous Integration mode
+ // if true, it capture browsers, run tests and exit
+ singleRun: false
+ });
+};
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2403f90
--- /dev/null
+++ b/package.json
@@ -0,0 +1,90 @@
+{
+ "name": "manticore",
+ "version": "0.0.0",
+ "main": "server/app.js",
+ "dependencies": {
+ "express": "~4.0.0",
+ "morgan": "~1.0.0",
+ "body-parser": "~1.5.0",
+ "method-override": "~1.0.0",
+ "serve-favicon": "~2.0.1",
+ "cookie-parser": "~1.0.1",
+ "express-session": "~1.0.2",
+ "errorhandler": "~1.0.0",
+ "compression": "~1.0.1",
+ "lodash": "~2.4.1",
+ "jade": "~1.2.0",
+ "mongoose": "~3.8.8",
+ "jsonwebtoken": "^0.3.0",
+ "express-jwt": "^0.1.3",
+ "passport": "~0.2.0",
+ "passport-local": "~0.1.6",
+ "composable-middleware": "^0.3.0",
+ "connect-mongo": "^0.4.1"
+ },
+ "devDependencies": {
+ "grunt": "~0.4.4",
+ "grunt-autoprefixer": "~0.7.2",
+ "grunt-wiredep": "~1.8.0",
+ "grunt-concurrent": "~0.5.0",
+ "grunt-contrib-clean": "~0.5.0",
+ "grunt-contrib-concat": "~0.4.0",
+ "grunt-contrib-copy": "~0.5.0",
+ "grunt-contrib-cssmin": "~0.9.0",
+ "grunt-contrib-htmlmin": "~0.2.0",
+ "grunt-contrib-imagemin": "~0.7.1",
+ "grunt-contrib-jshint": "~0.10.0",
+ "grunt-contrib-uglify": "~0.4.0",
+ "grunt-contrib-watch": "~0.6.1",
+ "grunt-contrib-jade": "^0.11.0",
+ "grunt-google-cdn": "~0.4.0",
+ "grunt-newer": "~0.7.0",
+ "grunt-ng-annotate": "^0.2.3",
+ "grunt-rev": "~0.1.0",
+ "grunt-svgmin": "~0.4.0",
+ "grunt-usemin": "~2.1.1",
+ "grunt-env": "~0.4.1",
+ "grunt-node-inspector": "~0.1.5",
+ "grunt-nodemon": "~0.2.0",
+ "grunt-angular-templates": "^0.5.4",
+ "grunt-dom-munger": "^3.4.0",
+ "grunt-protractor-runner": "^1.1.0",
+ "grunt-asset-injector": "^0.1.0",
+ "grunt-karma": "~0.8.2",
+ "grunt-build-control": "DaftMonk/grunt-build-control",
+ "grunt-mocha-test": "~0.10.2",
+ "grunt-contrib-stylus": "latest",
+ "jit-grunt": "^0.5.0",
+ "time-grunt": "~0.3.1",
+ "grunt-express-server": "~0.4.17",
+ "grunt-open": "~0.2.3",
+ "open": "~0.0.4",
+ "jshint-stylish": "~0.1.5",
+ "connect-livereload": "~0.4.0",
+ "karma-ng-scenario": "~0.1.0",
+ "karma-firefox-launcher": "~0.1.3",
+ "karma-script-launcher": "~0.1.0",
+ "karma-html2js-preprocessor": "~0.1.0",
+ "karma-ng-jade2js-preprocessor": "^0.1.2",
+ "karma-jasmine": "~0.1.5",
+ "karma-chrome-launcher": "~0.1.3",
+ "requirejs": "~2.1.11",
+ "karma-requirejs": "~0.2.1",
+ "karma-coffee-preprocessor": "~0.2.1",
+ "karma-jade-preprocessor": "0.0.11",
+ "karma-phantomjs-launcher": "~0.1.4",
+ "karma": "~0.12.9",
+ "karma-ng-html2js-preprocessor": "~0.1.0",
+ "supertest": "~0.11.0",
+ "should": "~3.3.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "scripts": {
+ "start": "node server/app.js",
+ "test": "grunt test",
+ "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update"
+ },
+ "private": true
+}
diff --git a/protractor.conf.js b/protractor.conf.js
new file mode 100644
index 0000000..cb66c67
--- /dev/null
+++ b/protractor.conf.js
@@ -0,0 +1,50 @@
+// Protractor configuration
+// https://github.com/angular/protractor/blob/master/referenceConf.js
+
+'use strict';
+
+exports.config = {
+ // The timeout for each script run on the browser. This should be longer
+ // than the maximum time your application needs to stabilize between tasks.
+ allScriptsTimeout: 110000,
+
+ // A base URL for your application under test. Calls to protractor.get()
+ // with relative paths will be prepended with this.
+ baseUrl: 'http://localhost:' + (process.env.PORT || '9000'),
+
+ // If true, only chromedriver will be started, not a standalone selenium.
+ // Tests for browsers other than chrome will not run.
+ chromeOnly: true,
+
+ // list of files / patterns to load in the browser
+ specs: [
+ 'e2e/**/*.spec.js'
+ ],
+
+ // Patterns to exclude.
+ exclude: [],
+
+ // ----- Capabilities to be passed to the webdriver instance ----
+ //
+ // For a full list of available capabilities, see
+ // https://code.google.com/p/selenium/wiki/DesiredCapabilities
+ // and
+ // https://code.google.com/p/selenium/source/browse/javascript/webdriver/capabilities.js
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+
+ // ----- The test framework -----
+ //
+ // Jasmine and Cucumber are fully supported as a test and assertion framework.
+ // Mocha has limited beta support. You will need to include your own
+ // assertion framework if working with mocha.
+ framework: 'jasmine',
+
+ // ----- Options to be passed to minijasminenode -----
+ //
+ // See the full list at https://github.com/juliemr/minijasminenode
+ jasmineNodeOpts: {
+ defaultTimeoutInterval: 30000
+ }
+};
diff --git a/server/.jshintrc b/server/.jshintrc
new file mode 100644
index 0000000..d7b958e
--- /dev/null
+++ b/server/.jshintrc
@@ -0,0 +1,15 @@
+{
+ "node": true,
+ "esnext": true,
+ "bitwise": true,
+ "eqeqeq": true,
+ "immed": true,
+ "latedef": "nofunc",
+ "newcap": true,
+ "noarg": true,
+ "regexp": true,
+ "undef": true,
+ "smarttabs": true,
+ "asi": true,
+ "debug": true
+}
diff --git a/server/.jshintrc-spec b/server/.jshintrc-spec
new file mode 100644
index 0000000..b6b55cb
--- /dev/null
+++ b/server/.jshintrc-spec
@@ -0,0 +1,11 @@
+{
+ "extends": ".jshintrc",
+ "globals": {
+ "describe": true,
+ "it": true,
+ "before": true,
+ "beforeEach": true,
+ "after": true,
+ "afterEach": true
+ }
+}
diff --git a/server/api/document/document.controller.js b/server/api/document/document.controller.js
new file mode 100644
index 0000000..7029351
--- /dev/null
+++ b/server/api/document/document.controller.js
@@ -0,0 +1,68 @@
+/**
+ * Using Rails-like standard naming convention for endpoints.
+ * GET /documents -> index
+ * POST /documents -> create
+ * GET /documents/:id -> show
+ * PUT /documents/:id -> update
+ * DELETE /documents/:id -> destroy
+ */
+
+'use strict';
+
+var _ = require('lodash');
+var Document = require('./document.model');
+
+// Get list of documents
+exports.index = function(req, res) {
+ Document.find(function (err, documents) {
+ if(err) { return handleError(res, err); }
+ return res.json(200, documents);
+ });
+};
+
+// Get a single document
+exports.show = function(req, res) {
+ Document.findById(req.params.id, function (err, document) {
+ if(err) { return handleError(res, err); }
+ if(!document) { return res.send(404); }
+ return res.json(document);
+ });
+};
+
+// Creates a new document in the DB.
+exports.create = function(req, res) {
+ Document.create(req.body, function(err, document) {
+ if(err) { return handleError(res, err); }
+ return res.json(201, document);
+ });
+};
+
+// Updates an existing document in the DB.
+exports.update = function(req, res) {
+ if(req.body._id) { delete req.body._id; }
+ Document.findById(req.params.id, function (err, document) {
+ if (err) { return handleError(res, err); }
+ if(!document) { return res.send(404); }
+ var updated = _.merge(document, req.body);
+ updated.save(function (err) {
+ if (err) { return handleError(res, err); }
+ return res.json(200, document);
+ });
+ });
+};
+
+// Deletes a document from the DB.
+exports.destroy = function(req, res) {
+ Document.findById(req.params.id, function (err, document) {
+ if(err) { return handleError(res, err); }
+ if(!document) { return res.send(404); }
+ document.remove(function(err) {
+ if(err) { return handleError(res, err); }
+ return res.send(204);
+ });
+ });
+};
+
+function handleError(res, err) {
+ return res.send(500, err);
+}
diff --git a/server/api/document/document.model.js b/server/api/document/document.model.js
new file mode 100644
index 0000000..a35e88b
--- /dev/null
+++ b/server/api/document/document.model.js
@@ -0,0 +1,14 @@
+'use strict';
+
+var mongoose = require('mongoose'),
+ Schema = mongoose.Schema;
+
+var DocumentSchema = new Schema({
+ name: String,
+ created: { type: Date, default: Date.now, required: true },
+ updated: Date,
+ active: { type: Boolean, default: false, required: true },
+ editors: { type: [{type: Schema.Types.ObjectId, ref: 'User'}], default: [] }
+});
+
+module.exports = mongoose.model('Document', DocumentSchema);
diff --git a/server/api/document/document.spec.js b/server/api/document/document.spec.js
new file mode 100644
index 0000000..ee73d2c
--- /dev/null
+++ b/server/api/document/document.spec.js
@@ -0,0 +1,20 @@
+'use strict';
+
+var should = require('should');
+var app = require('../../app');
+var request = require('supertest');
+
+describe('GET /api/documents', function() {
+
+ it('should respond with JSON array', function(done) {
+ request(app)
+ .get('/api/documents')
+ .expect(200)
+ .expect('Content-Type', /json/)
+ .end(function(err, res) {
+ if (err) return done(err);
+ res.body.should.be.instanceof(Array);
+ done();
+ });
+ });
+});
diff --git a/server/api/document/index.js b/server/api/document/index.js
new file mode 100644
index 0000000..50357f7
--- /dev/null
+++ b/server/api/document/index.js
@@ -0,0 +1,15 @@
+'use strict';
+
+var express = require('express');
+var controller = require('./document.controller');
+
+var router = express.Router();
+
+router.get('/', controller.index);
+router.get('/:id', controller.show);
+router.post('/', controller.create);
+router.put('/:id', controller.update);
+router.patch('/:id', controller.update);
+router.delete('/:id', controller.destroy);
+
+module.exports = router;
diff --git a/server/api/user/index.js b/server/api/user/index.js
new file mode 100644
index 0000000..48567e4
--- /dev/null
+++ b/server/api/user/index.js
@@ -0,0 +1,17 @@
+'use strict';
+
+var express = require('express');
+var controller = require('./user.controller');
+var config = require('../../config/environment');
+var auth = require('../../auth/auth.service');
+
+var router = express.Router();
+
+router.get('/', auth.hasRole('admin'), controller.index);
+router.delete('/:id', auth.hasRole('admin'), controller.destroy);
+router.get('/me', auth.isAuthenticated(), controller.me);
+router.put('/:id/password', auth.isAuthenticated(), controller.changePassword);
+router.get('/:id', auth.isAuthenticated(), controller.show);
+router.post('/', controller.create);
+
+module.exports = router;
diff --git a/server/api/user/user.controller.js b/server/api/user/user.controller.js
new file mode 100644
index 0000000..f4cd10c
--- /dev/null
+++ b/server/api/user/user.controller.js
@@ -0,0 +1,101 @@
+'use strict';
+
+var User = require('./user.model');
+var passport = require('passport');
+var config = require('../../config/environment');
+var jwt = require('jsonwebtoken');
+
+var validationError = function(res, err) {
+ return res.json(422, err);
+};
+
+/**
+ * Get list of users
+ * restriction: 'admin'
+ */
+exports.index = function(req, res) {
+ User.find({}, '-salt -hashedPassword', function (err, users) {
+ if(err) return res.send(500, err);
+ res.json(200, users);
+ });
+};
+
+/**
+ * Creates a new user
+ */
+exports.create = function (req, res, next) {
+ var newUser = new User(req.body);
+ newUser.provider = 'local';
+ newUser.role = 'user';
+ newUser.save(function(err, user) {
+ if (err) return validationError(res, err);
+ var token = jwt.sign({_id: user._id }, config.secrets.session, { expiresInMinutes: 60*5 });
+ res.json({ token: token });
+ });
+};
+
+/**
+ * Get a single user
+ */
+exports.show = function (req, res, next) {
+ var userId = req.params.id;
+
+ User.findById(userId, function (err, user) {
+ if (err) return next(err);
+ if (!user) return res.send(401);
+ res.json(user.profile);
+ });
+};
+
+/**
+ * Deletes a user
+ * restriction: 'admin'
+ */
+exports.destroy = function(req, res) {
+ User.findByIdAndRemove(req.params.id, function(err, user) {
+ if(err) return res.send(500, err);
+ return res.send(204);
+ });
+};
+
+/**
+ * Change a users password
+ */
+exports.changePassword = function(req, res, next) {
+ var userId = req.user._id;
+ var oldPass = String(req.body.oldPassword);
+ var newPass = String(req.body.newPassword);
+
+ User.findById(userId, function (err, user) {
+ if(user.authenticate(oldPass)) {
+ user.password = newPass;
+ user.save(function(err) {
+ if (err) return validationError(res, err);
+ res.send(200);
+ });
+ } else {
+ res.send(403);
+ }
+ });
+};
+
+/**
+ * Get my info
+ */
+exports.me = function(req, res, next) {
+ var userId = req.user._id;
+ User.findOne({
+ _id: userId
+ }, '-salt -hashedPassword', function(err, user) { // don't ever give out the password or salt
+ if (err) return next(err);
+ if (!user) return res.json(401);
+ res.json(user);
+ });
+};
+
+/**
+ * Authentication callback
+ */
+exports.authCallback = function(req, res, next) {
+ res.redirect('/');
+};
diff --git a/server/api/user/user.model.js b/server/api/user/user.model.js
new file mode 100644
index 0000000..f82ad18
--- /dev/null
+++ b/server/api/user/user.model.js
@@ -0,0 +1,142 @@
+'use strict';
+
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
+var crypto = require('crypto');
+
+var UserSchema = new Schema({
+ name: String,
+ email: { type: String, lowercase: true },
+ role: {
+ type: String,
+ default: 'user'
+ },
+ hashedPassword: String,
+ provider: String,
+ salt: String
+});
+
+/**
+ * Virtuals
+ */
+UserSchema
+ .virtual('password')
+ .set(function(password) {
+ this._password = password;
+ this.salt = this.makeSalt();
+ this.hashedPassword = this.encryptPassword(password);
+ })
+ .get(function() {
+ return this._password;
+ });
+
+// Public profile information
+UserSchema
+ .virtual('profile')
+ .get(function() {
+ return {
+ 'name': this.name,
+ 'role': this.role
+ };
+ });
+
+// Non-sensitive info we'll be putting in the token
+UserSchema
+ .virtual('token')
+ .get(function() {
+ return {
+ '_id': this._id,
+ 'role': this.role
+ };
+ });
+
+/**
+ * Validations
+ */
+
+// Validate empty email
+UserSchema
+ .path('email')
+ .validate(function(email) {
+ return email.length;
+ }, 'Email cannot be blank');
+
+// Validate empty password
+UserSchema
+ .path('hashedPassword')
+ .validate(function(hashedPassword) {
+ return hashedPassword.length;
+ }, 'Password cannot be blank');
+
+// Validate email is not taken
+UserSchema
+ .path('email')
+ .validate(function(value, respond) {
+ var self = this;
+ this.constructor.findOne({email: value}, function(err, user) {
+ if(err) throw err;
+ if(user) {
+ if(self.id === user.id) return respond(true);
+ return respond(false);
+ }
+ respond(true);
+ });
+}, 'The specified email address is already in use.');
+
+var validatePresenceOf = function(value) {
+ return value && value.length;
+};
+
+/**
+ * Pre-save hook
+ */
+UserSchema
+ .pre('save', function(next) {
+ if (!this.isNew) return next();
+
+ if (!validatePresenceOf(this.hashedPassword))
+ next(new Error('Invalid password'));
+ else
+ next();
+ });
+
+/**
+ * Methods
+ */
+UserSchema.methods = {
+ /**
+ * Authenticate - check if the passwords are the same
+ *
+ * @param {String} plainText
+ * @return {Boolean}
+ * @api public
+ */
+ authenticate: function(plainText) {
+ return this.encryptPassword(plainText) === this.hashedPassword;
+ },
+
+ /**
+ * Make salt
+ *
+ * @return {String}
+ * @api public
+ */
+ makeSalt: function() {
+ return crypto.randomBytes(16).toString('base64');
+ },
+
+ /**
+ * Encrypt password
+ *
+ * @param {String} password
+ * @return {String}
+ * @api public
+ */
+ encryptPassword: function(password) {
+ if (!password || !this.salt) return '';
+ var salt = new Buffer(this.salt, 'base64');
+ return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64');
+ }
+};
+
+module.exports = mongoose.model('User', UserSchema);
diff --git a/server/api/user/user.model.spec.js b/server/api/user/user.model.spec.js
new file mode 100644
index 0000000..257c95b
--- /dev/null
+++ b/server/api/user/user.model.spec.js
@@ -0,0 +1,60 @@
+'use strict';
+
+var should = require('should');
+var app = require('../../app');
+var User = require('./user.model');
+
+var user = new User({
+ provider: 'local',
+ name: 'Fake User',
+ email: 'test@test.com',
+ password: 'password'
+});
+
+describe('User Model', function() {
+ before(function(done) {
+ // Clear users before testing
+ User.remove().exec().then(function() {
+ done();
+ });
+ });
+
+ afterEach(function(done) {
+ User.remove().exec().then(function() {
+ done();
+ });
+ });
+
+ it('should begin with no users', function(done) {
+ User.find({}, function(err, users) {
+ users.should.have.length(0);
+ done();
+ });
+ });
+
+ it('should fail when saving a duplicate user', function(done) {
+ user.save(function() {
+ var userDup = new User(user);
+ userDup.save(function(err) {
+ should.exist(err);
+ done();
+ });
+ });
+ });
+
+ it('should fail when saving without an email', function(done) {
+ user.email = '';
+ user.save(function(err) {
+ should.exist(err);
+ done();
+ });
+ });
+
+ it("should authenticate user if password is valid", function() {
+ return user.authenticate('password').should.be.true;
+ });
+
+ it("should not authenticate user if password is invalid", function() {
+ return user.authenticate('blah').should.not.be.true;
+ });
+});
diff --git a/server/app.js b/server/app.js
new file mode 100644
index 0000000..82acdba
--- /dev/null
+++ b/server/app.js
@@ -0,0 +1,32 @@
+/**
+ * Main application file
+ */
+
+'use strict';
+
+// Set default node environment to development
+process.env.NODE_ENV = process.env.NODE_ENV || 'development';
+
+var express = require('express');
+var mongoose = require('mongoose');
+var config = require('./config/environment');
+
+// Connect to database
+mongoose.connect(config.mongo.uri, config.mongo.options);
+
+// Populate DB with sample data
+if(config.seedDB) { require('./config/seed'); }
+
+// Setup server
+var app = express();
+var server = require('http').createServer(app);
+require('./config/express')(app);
+require('./routes')(app);
+
+// Start server
+server.listen(config.port, config.ip, function () {
+ console.log('Express server listening on %d, in %s mode', config.port, app.get('env'));
+});
+
+// Expose app
+exports = module.exports = app;
\ No newline at end of file
diff --git a/server/auth/auth.service.js b/server/auth/auth.service.js
new file mode 100644
index 0000000..38ec343
--- /dev/null
+++ b/server/auth/auth.service.js
@@ -0,0 +1,76 @@
+'use strict';
+
+var mongoose = require('mongoose');
+var passport = require('passport');
+var config = require('../config/environment');
+var jwt = require('jsonwebtoken');
+var expressJwt = require('express-jwt');
+var compose = require('composable-middleware');
+var User = require('../api/user/user.model');
+var validateJwt = expressJwt({ secret: config.secrets.session });
+
+/**
+ * Attaches the user object to the request if authenticated
+ * Otherwise returns 403
+ */
+function isAuthenticated() {
+ return compose()
+ // Validate jwt
+ .use(function(req, res, next) {
+ // allow access_token to be passed through query parameter as well
+ if(req.query && req.query.hasOwnProperty('access_token')) {
+ req.headers.authorization = 'Bearer ' + req.query.access_token;
+ }
+ validateJwt(req, res, next);
+ })
+ // Attach user to request
+ .use(function(req, res, next) {
+ User.findById(req.user._id, function (err, user) {
+ if (err) return next(err);
+ if (!user) return res.send(401);
+
+ req.user = user;
+ next();
+ });
+ });
+}
+
+/**
+ * Checks if the user role meets the minimum requirements of the route
+ */
+function hasRole(roleRequired) {
+ if (!roleRequired) throw new Error('Required role needs to be set');
+
+ return compose()
+ .use(isAuthenticated())
+ .use(function meetsRequirements(req, res, next) {
+ if (config.userRoles.indexOf(req.user.role) >= config.userRoles.indexOf(roleRequired)) {
+ next();
+ }
+ else {
+ res.send(403);
+ }
+ });
+}
+
+/**
+ * Returns a jwt token signed by the app secret
+ */
+function signToken(id) {
+ return jwt.sign({ _id: id }, config.secrets.session, { expiresInMinutes: 60*5 });
+}
+
+/**
+ * Set token cookie directly for oAuth strategies
+ */
+function setTokenCookie(req, res) {
+ if (!req.user) return res.json(404, { message: 'Something went wrong, please try again.'});
+ var token = signToken(req.user._id, req.user.role);
+ res.cookie('token', JSON.stringify(token));
+ res.redirect('/');
+}
+
+exports.isAuthenticated = isAuthenticated;
+exports.hasRole = hasRole;
+exports.signToken = signToken;
+exports.setTokenCookie = setTokenCookie;
\ No newline at end of file
diff --git a/server/auth/index.js b/server/auth/index.js
new file mode 100644
index 0000000..0b79cbc
--- /dev/null
+++ b/server/auth/index.js
@@ -0,0 +1,15 @@
+'use strict';
+
+var express = require('express');
+var passport = require('passport');
+var config = require('../config/environment');
+var User = require('../api/user/user.model');
+
+// Passport Configuration
+require('./local/passport').setup(User, config);
+
+var router = express.Router();
+
+router.use('/local', require('./local'));
+
+module.exports = router;
\ No newline at end of file
diff --git a/server/auth/local/index.js b/server/auth/local/index.js
new file mode 100644
index 0000000..8bf88a0
--- /dev/null
+++ b/server/auth/local/index.js
@@ -0,0 +1,20 @@
+'use strict';
+
+var express = require('express');
+var passport = require('passport');
+var auth = require('../auth.service');
+
+var router = express.Router();
+
+router.post('/', function(req, res, next) {
+ passport.authenticate('local', function (err, user, info) {
+ var error = err || info;
+ if (error) return res.json(401, error);
+ if (!user) return res.json(404, {message: 'Something went wrong, please try again.'});
+
+ var token = auth.signToken(user._id, user.role);
+ res.json({token: token});
+ })(req, res, next)
+});
+
+module.exports = router;
\ No newline at end of file
diff --git a/server/auth/local/passport.js b/server/auth/local/passport.js
new file mode 100644
index 0000000..ac82b42
--- /dev/null
+++ b/server/auth/local/passport.js
@@ -0,0 +1,25 @@
+var passport = require('passport');
+var LocalStrategy = require('passport-local').Strategy;
+
+exports.setup = function (User, config) {
+ passport.use(new LocalStrategy({
+ usernameField: 'email',
+ passwordField: 'password' // this is the virtual field on the model
+ },
+ function(email, password, done) {
+ User.findOne({
+ email: email.toLowerCase()
+ }, function(err, user) {
+ if (err) return done(err);
+
+ if (!user) {
+ return done(null, false, { message: 'This email is not registered.' });
+ }
+ if (!user.authenticate(password)) {
+ return done(null, false, { message: 'This password is not correct.' });
+ }
+ return done(null, user);
+ });
+ }
+ ));
+};
\ No newline at end of file
diff --git a/server/components/errors/index.js b/server/components/errors/index.js
new file mode 100644
index 0000000..4c5a57c
--- /dev/null
+++ b/server/components/errors/index.js
@@ -0,0 +1,20 @@
+/**
+ * Error responses
+ */
+
+'use strict';
+
+module.exports[404] = function pageNotFound(req, res) {
+ var viewFilePath = '404';
+ var statusCode = 404;
+ var result = {
+ status: statusCode
+ };
+
+ res.status(result.status);
+ res.render(viewFilePath, function (err) {
+ if (err) { return res.json(result, result.status); }
+
+ res.render(viewFilePath);
+ });
+};
diff --git a/server/config/environment/development.js b/server/config/environment/development.js
new file mode 100644
index 0000000..5332b1b
--- /dev/null
+++ b/server/config/environment/development.js
@@ -0,0 +1,12 @@
+'use strict';
+
+// Development specific configuration
+// ==================================
+module.exports = {
+ // MongoDB connection options
+ mongo: {
+ uri: 'mongodb://localhost/manticore-dev'
+ },
+
+ seedDB: true
+};
diff --git a/server/config/environment/index.js b/server/config/environment/index.js
new file mode 100644
index 0000000..9c47094
--- /dev/null
+++ b/server/config/environment/index.js
@@ -0,0 +1,50 @@
+'use strict';
+
+var path = require('path');
+var _ = require('lodash');
+
+function requiredProcessEnv(name) {
+ if(!process.env[name]) {
+ throw new Error('You must set the ' + name + ' environment variable');
+ }
+ return process.env[name];
+}
+
+// All configurations will extend these options
+// ============================================
+var all = {
+ env: process.env.NODE_ENV,
+
+ // Root path of server
+ root: path.normalize(__dirname + '/../../..'),
+
+ // Server port
+ port: process.env.PORT || 9000,
+
+ // Should we populate the DB with sample data?
+ seedDB: false,
+
+ // Secret for session, you will want to change this and make it an environment variable
+ secrets: {
+ session: 'manticore-secret'
+ },
+
+ // List of user roles
+ userRoles: ['guest', 'user', 'admin'],
+
+ // MongoDB connection options
+ mongo: {
+ options: {
+ db: {
+ safe: true
+ }
+ }
+ },
+
+};
+
+// Export the config object based on the NODE_ENV
+// ==============================================
+module.exports = _.merge(
+ all,
+ require('./' + process.env.NODE_ENV + '.js') || {});
\ No newline at end of file
diff --git a/server/config/environment/production.js b/server/config/environment/production.js
new file mode 100644
index 0000000..896f810
--- /dev/null
+++ b/server/config/environment/production.js
@@ -0,0 +1,23 @@
+'use strict';
+
+// Production specific configuration
+// =================================
+module.exports = {
+ // Server IP
+ ip: process.env.OPENSHIFT_NODEJS_IP ||
+ process.env.IP ||
+ undefined,
+
+ // Server port
+ port: process.env.OPENSHIFT_NODEJS_PORT ||
+ process.env.PORT ||
+ 8080,
+
+ // MongoDB connection options
+ mongo: {
+ uri: process.env.MONGOLAB_URI ||
+ process.env.MONGOHQ_URL ||
+ process.env.OPENSHIFT_MONGODB_DB_URL+process.env.OPENSHIFT_APP_NAME ||
+ 'mongodb://localhost/manticore'
+ }
+};
\ No newline at end of file
diff --git a/server/config/environment/test.js b/server/config/environment/test.js
new file mode 100644
index 0000000..21969ea
--- /dev/null
+++ b/server/config/environment/test.js
@@ -0,0 +1,10 @@
+'use strict';
+
+// Test specific configuration
+// ===========================
+module.exports = {
+ // MongoDB connection options
+ mongo: {
+ uri: 'mongodb://localhost/manticore-test'
+ }
+};
\ No newline at end of file
diff --git a/server/config/express.js b/server/config/express.js
new file mode 100644
index 0000000..4129eca
--- /dev/null
+++ b/server/config/express.js
@@ -0,0 +1,45 @@
+/**
+ * Express configuration
+ */
+
+'use strict';
+
+var express = require('express');
+var favicon = require('serve-favicon');
+var morgan = require('morgan');
+var compression = require('compression');
+var bodyParser = require('body-parser');
+var methodOverride = require('method-override');
+var cookieParser = require('cookie-parser');
+var errorHandler = require('errorhandler');
+var path = require('path');
+var config = require('./environment');
+var passport = require('passport');
+
+module.exports = function(app) {
+ var env = app.get('env');
+
+ app.set('views', config.root + '/server/views');
+ app.set('view engine', 'jade');
+ app.use(compression());
+ app.use(bodyParser.urlencoded({ extended: false }));
+ app.use(bodyParser.json());
+ app.use(methodOverride());
+ app.use(cookieParser());
+ app.use(passport.initialize());
+ if ('production' === env) {
+ app.use(favicon(path.join(config.root, 'public', 'favicon.ico')));
+ app.use(express.static(path.join(config.root, 'public')));
+ app.set('appPath', config.root + '/public');
+ app.use(morgan('dev'));
+ }
+
+ if ('development' === env || 'test' === env) {
+ app.use(require('connect-livereload')());
+ app.use(express.static(path.join(config.root, '.tmp')));
+ app.use(express.static(path.join(config.root, 'client')));
+ app.set('appPath', 'client');
+ app.use(morgan('dev'));
+ app.use(errorHandler()); // Error handler - has to be last
+ }
+};
\ No newline at end of file
diff --git a/server/config/local.env.sample.js b/server/config/local.env.sample.js
new file mode 100644
index 0000000..982ca82
--- /dev/null
+++ b/server/config/local.env.sample.js
@@ -0,0 +1,14 @@
+'use strict';
+
+// Use local.env.js for environment variables that grunt will set when the server starts locally.
+// Use for your api keys, secrets, etc. This file should not be tracked by git.
+//
+// You will need to set these on the server you deploy to.
+
+module.exports = {
+ DOMAIN: 'http://localhost:9000',
+ SESSION_SECRET: 'manticore-secret',
+
+ // Control debug level for modules using visionmedia/debug
+ DEBUG: ''
+};
diff --git a/server/config/seed.js b/server/config/seed.js
new file mode 100644
index 0000000..614d8f9
--- /dev/null
+++ b/server/config/seed.js
@@ -0,0 +1,42 @@
+/**
+ * Populate DB with sample data on server start
+ * to disable, edit config/environment/index.js, and set `seedDB: false`
+ */
+
+'use strict';
+
+var Document = require('../api/document/document.model');
+var User = require('../api/user/user.model');
+
+Document.find({}).remove(function() {
+ Document.create({
+ name: 'Trip Budget'
+ }, {
+ name: 'Vacation Spots'
+ }, {
+ name: 'Voucher Codes'
+ }, {
+ name: 'Backpacks'
+ }, function () {
+ console.log('finished populating documents')
+ }
+ );
+});
+
+User.find({}).remove(function() {
+ User.create({
+ provider: 'local',
+ name: 'Test User',
+ email: 'test@test.com',
+ password: 'test'
+ }, {
+ provider: 'local',
+ role: 'admin',
+ name: 'Admin',
+ email: 'admin@admin.com',
+ password: 'admin'
+ }, function() {
+ console.log('finished populating users');
+ }
+ );
+});
diff --git a/server/routes.js b/server/routes.js
new file mode 100644
index 0000000..fd38254
--- /dev/null
+++ b/server/routes.js
@@ -0,0 +1,26 @@
+/**
+ * Main application routes
+ */
+
+'use strict';
+
+var errors = require('./components/errors');
+
+module.exports = function(app) {
+
+ // Insert routes below
+ app.use('/api/documents', require('./api/document'));
+ app.use('/api/users', require('./api/user'));
+
+ app.use('/auth', require('./auth'));
+
+ // All undefined asset or api routes should return a 404
+ app.route('/:url(api|auth|components|app|bower_components|assets)/*')
+ .get(errors[404]);
+
+ // All other routes should redirect to the index.html
+ app.route('/*')
+ .get(function(req, res) {
+ res.sendfile(app.get('appPath') + '/index.html');
+ });
+};
diff --git a/server/views/404.jade b/server/views/404.jade
new file mode 100644
index 0000000..b5735b4
--- /dev/null
+++ b/server/views/404.jade
@@ -0,0 +1,133 @@
+doctype html
+html(lang='en')
+head
+ meta(charset='utf-8')
+ title Page Not Found :(
+ style.
+ ::-moz-selection {
+ background: #b3d4fc;
+ text-shadow: none;
+ }
+ ::selection {
+ background: #b3d4fc;
+ text-shadow: none;
+ }
+ html {
+ padding: 30px 10px;
+ font-size: 20px;
+ line-height: 1.4;
+ color: #737373;
+ background: #f0f0f0;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+ }
+ html,
+ input {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ }
+ body {
+ max-width: 500px;
+ _width: 500px;
+ padding: 30px 20px 50px;
+ border: 1px solid #b3b3b3;
+ border-radius: 4px;
+ margin: 0 auto;
+ box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
+ background: #fcfcfc;
+ }
+ h1 {
+ margin: 0 10px;
+ font-size: 50px;
+ text-align: center;
+ }
+ h1 span {
+ color: #bbb;
+ }
+ h3 {
+ margin: 1.5em 0 0.5em;
+ }
+ p {
+ margin: 1em 0;
+ }
+ ul {
+ padding: 0 0 0 40px;
+ margin: 1em 0;
+ }
+ .container {
+ max-width: 380px;
+ _width: 380px;
+ margin: 0 auto;
+ }
+ /* google search */
+ #goog-fixurl ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+ #goog-fixurl form {
+ margin: 0;
+ }
+ #goog-wm-qt,
+ #goog-wm-sb {
+ border: 1px solid #bbb;
+ font-size: 16px;
+ line-height: normal;
+ vertical-align: top;
+ color: #444;
+ border-radius: 2px;
+ }
+ #goog-wm-qt {
+ width: 220px;
+ height: 20px;
+ padding: 5px;
+ margin: 5px 10px 0 0;
+ box-shadow: inset 0 1px 1px #ccc;
+ }
+ #goog-wm-sb {
+ display: inline-block;
+ height: 32px;
+ padding: 0 10px;
+ margin: 5px 0 0;
+ white-space: nowrap;
+ cursor: pointer;
+ background-color: #f5f5f5;
+ background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ *overflow: visible;
+ *display: inline;
+ *zoom: 1;
+ }
+ #goog-wm-sb:hover,
+ #goog-wm-sb:focus {
+ border-color: #aaa;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
+ background-color: #f8f8f8;
+ }
+ #goog-wm-qt:hover,
+ #goog-wm-qt:focus {
+ border-color: #105cb6;
+ outline: 0;
+ color: #222;
+ }
+ input::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+ }
+ body
+ .container
+ h1
+ | Not found
+ span :(
+ p Sorry, but the page you were trying to view does not exist.
+ p It looks like this was the result of either:
+ ul
+ li a mistyped address
+ li an out-of-date link
+ script.
+ var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
+ script(src='//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js')