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')