Repository: gi-no/paizaqa Branch: master Commit: c8bc49534afa Files: 133 Total size: 202.3 KB Directory structure: gitextract_m_s_vier/ ├── .bowerrc ├── .buildignore ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jscsrc ├── .travis.yml ├── .yo-rc.json ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── client/ │ ├── .htaccess │ ├── .jshintrc │ ├── app/ │ │ ├── account/ │ │ │ ├── account.js │ │ │ ├── login/ │ │ │ │ ├── login.controller.js │ │ │ │ └── login.html │ │ │ ├── settings/ │ │ │ │ ├── settings.controller.js │ │ │ │ └── settings.html │ │ │ └── signup/ │ │ │ ├── signup.controller.js │ │ │ └── signup.html │ │ ├── admin/ │ │ │ ├── admin.controller.js │ │ │ ├── admin.html │ │ │ ├── admin.module.js │ │ │ ├── admin.router.js │ │ │ └── admin.scss │ │ ├── app.constant.js │ │ ├── app.js │ │ ├── app.scss │ │ ├── fromNow/ │ │ │ ├── fromNow.filter.js │ │ │ └── fromNow.filter.spec.js │ │ ├── questionsCreate/ │ │ │ ├── questionsCreate.controller.js │ │ │ ├── questionsCreate.controller.spec.js │ │ │ ├── questionsCreate.html │ │ │ ├── questionsCreate.js │ │ │ └── questionsCreate.scss │ │ ├── questionsIndex/ │ │ │ ├── questionsIndex.controller.js │ │ │ ├── questionsIndex.controller.spec.js │ │ │ ├── questionsIndex.html │ │ │ ├── questionsIndex.js │ │ │ └── questionsIndex.scss │ │ └── questionsShow/ │ │ ├── questionsShow.controller.js │ │ ├── questionsShow.controller.spec.js │ │ ├── questionsShow.html │ │ ├── questionsShow.js │ │ └── questionsShow.scss │ ├── components/ │ │ ├── auth/ │ │ │ ├── auth.module.js │ │ │ ├── auth.service.js │ │ │ ├── interceptor.service.js │ │ │ ├── router.decorator.js │ │ │ └── user.service.js │ │ ├── footer/ │ │ │ ├── footer.directive.js │ │ │ ├── footer.html │ │ │ └── footer.scss │ │ ├── modal/ │ │ │ ├── modal.html │ │ │ ├── modal.scss │ │ │ └── modal.service.js │ │ ├── mongoose-error/ │ │ │ └── mongoose-error.directive.js │ │ ├── navbar/ │ │ │ ├── navbar.controller.js │ │ │ ├── navbar.directive.js │ │ │ └── navbar.html │ │ ├── oauth-buttons/ │ │ │ ├── oauth-buttons.controller.js │ │ │ ├── oauth-buttons.controller.spec.js │ │ │ ├── oauth-buttons.directive.js │ │ │ ├── oauth-buttons.directive.spec.js │ │ │ ├── oauth-buttons.html │ │ │ └── oauth-buttons.scss │ │ ├── socket/ │ │ │ ├── socket.mock.js │ │ │ └── socket.service.js │ │ ├── ui-router/ │ │ │ └── ui-router.mock.js │ │ └── util/ │ │ ├── util.module.js │ │ └── util.service.js │ ├── index.html │ └── robots.txt ├── e2e/ │ ├── account/ │ │ ├── login/ │ │ │ ├── login.po.js │ │ │ └── login.spec.js │ │ ├── logout/ │ │ │ └── logout.spec.js │ │ └── signup/ │ │ ├── signup.po.js │ │ └── signup.spec.js │ ├── components/ │ │ ├── navbar/ │ │ │ └── navbar.po.js │ │ └── oauth-buttons/ │ │ └── oauth-buttons.po.js │ └── main/ │ ├── main.po.js │ └── main.spec.js ├── karma.conf.js ├── mocha.conf.js ├── package.json ├── protractor.conf.js └── server/ ├── .jshintrc ├── .jshintrc-spec ├── api/ │ ├── question/ │ │ ├── index.js │ │ ├── question.controller.js │ │ ├── question.events.js │ │ ├── question.integration.js │ │ ├── question.model.js │ │ └── question.socket.js │ ├── thing/ │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── thing.controller.js │ │ ├── thing.events.js │ │ ├── thing.integration.js │ │ ├── thing.model.js │ │ └── thing.socket.js │ └── user/ │ ├── index.js │ ├── index.spec.js │ ├── user.controller.js │ ├── user.events.js │ ├── user.integration.js │ ├── user.model.js │ └── user.model.spec.js ├── app.js ├── auth/ │ ├── auth.service.js │ ├── facebook/ │ │ ├── index.js │ │ └── passport.js │ ├── google/ │ │ ├── index.js │ │ └── passport.js │ ├── index.js │ ├── local/ │ │ ├── index.js │ │ └── passport.js │ └── twitter/ │ ├── index.js │ └── passport.js ├── components/ │ └── errors/ │ └── index.js ├── config/ │ ├── environment/ │ │ ├── development.js │ │ ├── index.js │ │ ├── production.js │ │ ├── shared.js │ │ └── test.js │ ├── express.js │ ├── local.env.sample.js │ ├── seed.js │ └── socketio.js ├── index.js ├── routes.js └── views/ └── 404.html ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bowerrc ================================================ { "directory": "client/bower_components" } ================================================ FILE: .buildignore ================================================ ================================================ FILE: .editorconfig ================================================ # 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 ================================================ FILE: .gitattributes ================================================ * text=auto # These files are text and should be normalized (Convert crlf => lf) *.php text *.css text *.js text *.htm text *.html text *.xml text *.txt text *.ini text *.inc text .htaccess text # Denote all files that are truly binary and should not be modified. # (binary is a macro for -text -diff) *.png binary *.jpg binary *.jpeg binary *.gif binary *.ico binary *.mov binary *.mp4 binary *.mp3 binary *.flv binary *.fla binary *.swf binary *.gz binary *.zip binary *.7z binary *.ttf binary # Documents *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain ================================================ FILE: .gitignore ================================================ node_modules public .tmp .sass-cache .idea client/bower_components dist /server/config/local.env.js npm-debug.log coverage .cache .config ================================================ FILE: .jscsrc ================================================ { "excludeFiles": [ "client/app/app.constant.js" ], "esnext": true, "maximumLineLength": { "value": 100, "allowComments": true, "allowRegex": true }, "disallowMixedSpacesAndTabs": true, "disallowMultipleLineStrings": true, "disallowNewlineBeforeBlockStatements": true, "disallowSpaceAfterObjectKeys": true, "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], "disallowSpaceBeforeBinaryOperators": [","], "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], "disallowSpacesInAnonymousFunctionExpression": { "beforeOpeningRoundBrace": true }, "disallowSpacesInFunctionDeclaration": { "beforeOpeningRoundBrace": true }, "disallowSpacesInNamedFunctionExpression": { "beforeOpeningRoundBrace": true }, "disallowSpacesInsideArrayBrackets": true, "disallowSpacesInsideParentheses": true, "disallowTrailingComma": true, "disallowTrailingWhitespace": true, "requireCommaBeforeLineBreak": true, "requireLineFeedAtFileEnd": true, "requireSpaceAfterBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], "requireSpaceBeforeBinaryOperators": ["?", ":", "+", "-", "/", "*", "%", "==", "===", "!=", "!==", ">", ">=", "<", "<=", "&&", "||"], "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], "requireSpaceBeforeBlockStatements": true, "requireSpacesInConditionalExpression": { "afterTest": true, "beforeConsequent": true, "afterConsequent": true, "beforeAlternate": true }, "requireSpacesInFunction": { "beforeOpeningCurlyBrace": true }, "validateLineBreaks": "LF", "validateParameterSeparator": ", " } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 4.2.3 matrix: fast_finish: true allow_failures: - node_js: 5.1.1 before_script: - npm install -g bower grunt-cli - gem install sass - bower install services: mongodb ================================================ FILE: .yo-rc.json ================================================ { "generator-angular-fullstack": { "generatorVersion": "3.3.0", "endpointDirectory": "server/api/", "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", "insertModels": true, "registerModelsFile": "server/sqldb/index.js", "modelsNeedle": "// Insert models below", "filters": { "js": true, "babel": true, "html": true, "sass": true, "uirouter": true, "bootstrap": true, "uibootstrap": true, "socketio": true, "auth": true, "models": true, "mongooseModels": true, "mongoose": true, "oauth": true, "googleAuth": true, "facebookAuth": true, "twitterAuth": true, "grunt": true, "jasmine": true, "mocha": false, "should": false, "expect": false } }, "generator-ng-component": { "routeDirectory": "client/app/", "directiveDirectory": "client/app/", "filterDirectory": "client/app/", "serviceDirectory": "client/app/", "basePath": "client", "moduleName": "", "filters": [ "uirouter", "jasmine", "uirouter" ], "extensions": [ "babel", "js", "html", "scss" ], "directiveSimpleTemplates": "", "directiveComplexTemplates": "", "filterTemplates": "", "serviceTemplates": "", "factoryTemplates": "", "controllerTemplates": "", "decoratorTemplates": "", "providerTemplates": "", "routeTemplates": "" } } ================================================ FILE: Gruntfile.js ================================================ // Generated on 2016-03-08 using generator-angular-fullstack 3.3.0 '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', buildcontrol: 'grunt-build-control', istanbul_check_coverage: 'grunt-mocha-istanbul', ngconstant: 'grunt-ng-constant' }); // 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', server: 'server', dist: 'dist' }, express: { options: { port: process.env.PORT || 9000 }, dev: { options: { script: '<%= yeoman.server %>', debug: true } }, prod: { options: { script: '<%= yeoman.dist %>/<%= yeoman.server %>' } } }, open: { server: { url: 'http://localhost:<%= express.options.port %>' } }, watch: { babel: { files: ['<%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js'], tasks: ['newer:babel:client'] }, ngconstant: { files: ['<%= yeoman.server %>/config/environment/shared.js'], tasks: ['ngconstant'] }, injectJS: { files: [ '<%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js', '!<%= yeoman.client %>/app/app.js' ], tasks: ['injector:scripts'] }, injectCss: { files: ['<%= yeoman.client %>/{app,components}/**/*.css'], tasks: ['injector:css'] }, mochaTest: { files: ['<%= yeoman.server %>/**/*.{spec,integration}.js'], tasks: ['env:test', 'mochaTest'] }, jsTest: { files: ['<%= yeoman.client %>/{app,components}/**/*.{spec,mock}.js'], tasks: ['newer:jshint:all', 'wiredep:test', 'karma'] }, injectSass: { files: ['<%= yeoman.client %>/{app,components}/**/*.{scss,sass}'], tasks: ['injector:sass'] }, sass: { files: ['<%= yeoman.client %>/{app,components}/**/*.{scss,sass}'], tasks: ['sass', 'postcss'] }, gruntfile: { files: ['Gruntfile.js'] }, livereload: { files: [ '{.tmp,<%= yeoman.client %>}/{app,components}/**/*.{css,html}', '{.tmp,<%= yeoman.client %>}/{app,components}/**/!(*.spec|*.mock).js', '<%= yeoman.client %>/assets/images/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}' ], options: { livereload: true } }, express: { files: ['<%= yeoman.server %>/**/*.{js,json}'], tasks: ['express:dev', 'wait'], options: { livereload: true, spawn: false //Without this option specified express won't be reloaded } }, bower: { files: ['bower.json'], tasks: ['wiredep'] }, }, // 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: '<%= yeoman.server %>/.jshintrc' }, src: ['<%= yeoman.server %>/**/!(*.spec|*.integration).js'] }, serverTest: { options: { jshintrc: '<%= yeoman.server %>/.jshintrc-spec' }, src: ['<%= yeoman.server %>/**/*.{spec,integration}.js'] }, all: ['<%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock|app.constant).js'], test: { src: ['<%= yeoman.client %>/{app,components}/**/*.{spec,mock}.js'] } }, jscs: { options: { config: ".jscsrc" }, main: { files: { src: [ '<%= yeoman.client %>/app/**/*.js', '<%= yeoman.server %>/**/*.js' ] } } }, // Empties folders to start fresh clean: { dist: { files: [{ dot: true, src: [ '.tmp', '<%= yeoman.dist %>/!(.git*|.openshift|Procfile)**' ] }] }, server: '.tmp' }, // Add vendor prefixed styles postcss: { options: { map: true, processors: [ require('autoprefixer')({browsers: ['last 2 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: '<%= yeoman.server %>', 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 and karma.conf.js wiredep: { options: { exclude: [ /bootstrap.js/, '/json3/', '/es5-shim/', /font-awesome\.css/, /bootstrap\.css/, /bootstrap-sass-official/, /bootstrap-social\.css/ ] }, client: { src: '<%= yeoman.client %>/index.html', ignorePath: '<%= yeoman.client %>/', }, test: { src: './karma.conf.js', devDependencies: true } }, // Renames files for browser caching purposes filerev: { dist: { src: [ '<%= yeoman.dist %>/<%= yeoman.client %>/!(bower_components){,*/}*.{js,css}', '<%= yeoman.dist %>/<%= yeoman.client %>/assets/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', '<%= yeoman.dist %>/<%= yeoman.client %>/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 %>/<%= yeoman.client %>' } }, // Performs rewrites based on rev and the useminPrepare configuration usemin: { html: ['<%= yeoman.dist %>/<%= yeoman.client %>/{,!(bower_components)/**/}*.html'], css: ['<%= yeoman.dist %>/<%= yeoman.client %>/!(bower_components){,*/}*.css'], js: ['<%= yeoman.dist %>/<%= yeoman.client %>/!(bower_components){,*/}*.js'], options: { assetsDirs: [ '<%= yeoman.dist %>/<%= yeoman.client %>', '<%= yeoman.dist %>/<%= yeoman.client %>/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,svg}', dest: '<%= yeoman.dist %>/<%= yeoman.client %>/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' }] } }, // Dynamically generate angular constant `appConfig` from // `server/config/environment/shared.js` ngconstant: { options: { name: 'paizaqaApp.constants', dest: '<%= yeoman.client %>/app/app.constant.js', deps: [], wrap: true, configPath: '<%= yeoman.server %>/config/environment/shared' }, app: { constants: function() { return { appConfig: require('./' + grunt.config.get('ngconstant.options.configPath')) }; } } }, // Package all the html partials into a single javascript payload ngtemplates: { options: { // This should be the name of your apps angular module module: 'paizaqaApp', 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 %>/<%= yeoman.client %>/*.html'] } }, // Copies remaining files to places other tasks can use copy: { dist: { files: [{ expand: true, dot: true, cwd: '<%= yeoman.client %>', dest: '<%= yeoman.dist %>/<%= yeoman.client %>', src: [ '*.{ico,png,txt}', '.htaccess', 'bower_components/**/*', 'assets/images/{,*/}*.{webp}', 'assets/fonts/**/*', 'index.html' ] }, { expand: true, cwd: '.tmp/images', dest: '<%= yeoman.dist %>/<%= yeoman.client %>/assets/images', src: ['generated/*'] }, { expand: true, dest: '<%= yeoman.dist %>', src: [ 'package.json', '<%= yeoman.server %>/**/*', '!<%= yeoman.server %>/config/local.env.sample.js' ] }] }, styles: { expand: true, cwd: '<%= yeoman.client %>', dest: '.tmp/', src: ['{app,components}/**/*.css'] } }, buildcontrol: { options: { dir: '<%= yeoman.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: { pre: [ 'injector:sass', 'ngconstant' ], server: [ 'newer:babel:client', 'sass', ], test: [ 'newer:babel:client', 'sass', ], debug: { tasks: [ 'nodemon', 'node-inspector' ], options: { logConcurrentOutput: true } }, dist: [ 'newer:babel:client', 'sass', 'imagemin' ] }, // Test settings karma: { unit: { configFile: 'karma.conf.js', singleRun: true } }, mochaTest: { options: { reporter: 'spec', require: 'mocha.conf.js', timeout: 5000 // set default mocha spec timeout }, unit: { src: ['<%= yeoman.server %>/**/*.spec.js'] }, integration: { src: ['<%= yeoman.server %>/**/*.integration.js'] } }, mocha_istanbul: { unit: { options: { excludes: ['**/*.{spec,mock,integration}.js'], reporter: 'spec', require: ['mocha.conf.js'], mask: '**/*.spec.js', coverageFolder: 'coverage/server/unit' }, src: '<%= yeoman.server %>' }, integration: { options: { excludes: ['**/*.{spec,mock,integration}.js'], reporter: 'spec', require: ['mocha.conf.js'], mask: '**/*.integration.js', coverageFolder: 'coverage/server/integration' }, src: '<%= yeoman.server %>' } }, istanbul_check_coverage: { default: { options: { coverageFolder: 'coverage/**', check: { lines: 80, statements: 80, branches: 80, functions: 80 } } } }, protractor: { options: { configFile: 'protractor.conf.js' }, chrome: { options: { args: { browser: 'chrome' } } } }, env: { test: { NODE_ENV: 'test' }, prod: { NODE_ENV: 'production' }, all: localConfig }, // Compiles ES6 to JavaScript using Babel babel: { options: { sourceMap: true, optional: [ 'es7.classProperties' ] }, client: { files: [{ expand: true, cwd: '<%= yeoman.client %>', src: ['{app,components}/**/!(*.spec).js'], dest: '.tmp' }] }, server: { options: { optional: ['runtime'] }, files: [{ expand: true, cwd: '<%= yeoman.server %>', src: ['**/*.js'], dest: '<%= yeoman.dist %>/<%= yeoman.server %>' }] } }, // Compiles Sass to CSS sass: { server: { options: { compass: false }, files: { '.tmp/app/app.css' : '<%= yeoman.client %>/app/app.scss' } } }, injector: { options: {}, // Inject application script files into index.html (doesn't include bower) scripts: { options: { transform: function(filePath) { var yoClient = grunt.config.get('yeoman.client'); filePath = filePath.replace('/' + yoClient + '/', ''); filePath = filePath.replace('/.tmp/', ''); return ''; }, sort: function(a, b) { var module = /\.module\.js$/; var aMod = module.test(a); var bMod = module.test(b); // inject *.module.js first return (aMod === bMod) ? 0 : (aMod ? -1 : 1); }, starttag: '', endtag: '' }, files: { '<%= yeoman.client %>/index.html': [ [ '<%= yeoman.client %>/{app,components}/**/!(*.spec|*.mock).js', '!{.tmp,<%= yeoman.client %>}/app/app.{js,ts}' ] ] } }, // Inject component scss into app.scss sass: { options: { transform: function(filePath) { var yoClient = grunt.config.get('yeoman.client'); filePath = filePath.replace('/' + yoClient + '/app/', ''); filePath = filePath.replace('/' + yoClient + '/components/', '../components/'); return '@import \'' + filePath + '\';'; }, starttag: '// injector', endtag: '// endinjector' }, files: { '<%= yeoman.client %>/app/app.scss': [ '<%= yeoman.client %>/{app,components}/**/*.{scss,sass}', '!<%= yeoman.client %>/app/app.{scss,sass}' ] } }, // Inject component css into index.html css: { options: { transform: function(filePath) { var yoClient = grunt.config.get('yeoman.client'); filePath = filePath.replace('/' + yoClient + '/', ''); 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', 'concurrent:pre', 'concurrent:server', 'injector', 'wiredep:client', 'postcss', 'concurrent:debug' ]); } grunt.task.run([ 'clean:server', 'env:all', 'concurrent:pre', 'concurrent:server', 'injector', 'wiredep:client', 'postcss', '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, option) { if (target === 'server') { return grunt.task.run([ 'env:all', 'env:test', 'mochaTest:unit', 'mochaTest:integration' ]); } else if (target === 'client') { return grunt.task.run([ 'clean:server', 'env:all', 'concurrent:pre', 'concurrent:test', 'injector', 'postcss', 'wiredep:test', 'karma' ]); } else if (target === 'e2e') { if (option === 'prod') { return grunt.task.run([ 'build', 'env:all', 'env:prod', 'express:prod', 'protractor' ]); } else { return grunt.task.run([ 'clean:server', 'env:all', 'env:test', 'concurrent:pre', 'concurrent:test', 'injector', 'wiredep:client', 'postcss', 'express:dev', 'protractor' ]); } } else if (target === 'coverage') { if (option === 'unit') { return grunt.task.run([ 'env:all', 'env:test', 'mocha_istanbul:unit' ]); } else if (option === 'integration') { return grunt.task.run([ 'env:all', 'env:test', 'mocha_istanbul:integration' ]); } else if (option === 'check') { return grunt.task.run([ 'istanbul_check_coverage' ]); } else { return grunt.task.run([ 'env:all', 'env:test', 'mocha_istanbul', 'istanbul_check_coverage' ]); } } else grunt.task.run([ 'test:server', 'test:client' ]); }); grunt.registerTask('build', [ 'clean:dist', 'concurrent:pre', 'concurrent:dist', 'injector', 'wiredep:client', 'useminPrepare', 'postcss', 'ngtemplates', 'concat', 'ngAnnotate', 'copy:dist', 'babel:server', 'cdnify', 'cssmin', 'uglify', 'filerev', 'usemin' ]); grunt.registerTask('default', [ 'newer:jshint', 'test', 'build' ]); }; ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 Gino, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # PaizaQA PaizaQA is a Open Source QA service(like StackOverflow) using MEAN stack. This project was generated with the [Angular Full-Stack Generator](https://github.com/DaftMonk/generator-angular-fullstack) version 3.3.0. ## Blog article The following blog article explains how to build the QA service using MEAN stack. English: [Building a QA web service in an hour - MEAN stack development(3)](http://engineering.paiza.io/entry/2016/03/10/115345) Japanese: [Webサービスを作りたい人に最適、たった1時間でJSベースのQAサイトを作る方法 - MEANスタック開発(3)](http://paiza.hatenablog.com/entry/meanstack_howto_3) ## Demo [http://paizaqa.herokuapp.com](http://paizaqa.herokuapp.com) ## Getting Started ### Prerequisites - [Git](https://git-scm.com/) - [Node.js and npm](nodejs.org) Node ^4.2.3, npm ^2.14.7 - [Bower](bower.io) (`npm install --global bower`) - [Ruby](https://www.ruby-lang.org) and then `gem install sass` - [Grunt](http://gruntjs.com/) (`npm install --global grunt-cli`) - [MongoDB](https://www.mongodb.org/) - Keep a running daemon with `mongod` ### Developing 1. Run `npm install` to install server dependencies. 2. Run `bower install` to install front-end dependencies. 3. Run `mongod` in a separate shell to keep an instance of the MongoDB Daemon running 4. Run `grunt serve` to start the development server. It should automatically open the client in your browser when ready. ## Build & development Run `grunt build` for building and `grunt serve` for preview. ## Testing Running `npm test` will run the unit tests with karma. ================================================ FILE: bower.json ================================================ { "name": "paizaqa", "version": "0.0.0", "dependencies": { "angular": "~1.4.0", "json3": "~3.3.1", "es5-shim": "~3.0.1", "bootstrap-sass-official": "~3.1.1", "bootstrap": "~3.1.1", "bootstrap-social": "~4.9.1", "angular-resource": "~1.4.0", "angular-cookies": "~1.4.0", "angular-sanitize": "~1.4.0", "angular-bootstrap": "~0.13.0", "font-awesome": ">=4.1.0", "lodash": "~2.4.1", "angular-socket-io": "~0.7.0", "angular-ui-router": "~0.2.15", "angular-validation-match": "~1.5.2", "angular-pagedown": "^0.4.4", "ng-tags-input": "^3.0.0", "angular-messages": "^1.5.0", "moment": "momentjs#^2.12.0", "ngInfiniteScroll": "^1.2.2" }, "devDependencies": { "angular-mocks": "~1.4.0" }, "overrides": { "pagedown": { "main": [ "Markdown.Converter.js", "Markdown.Sanitizer.js", "Markdown.Extra.js", "Markdown.Editor.js", "wmd-buttons.png" ] } } } ================================================ FILE: client/.htaccess ================================================ # 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 # ================================================ FILE: client/.jshintrc ================================================ { "node": true, "browser": true, "esnext": true, "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "immed": true, "latedef": true, "newcap": true, "noarg": true, "quotmark": "single", "undef": true, "unused": true, "strict": true, "trailing": true, "smarttabs": true, "ignoreDelimiters": [ { "start": "start-non-standard", "end": "end-non-standard" } ], "globals": { "jQuery": true, "angular": true, "console": true, "$": true, "_": true, "moment": true, "jasmine": true, "describe": true, "beforeEach": true, "module": true, "inject": true, "it": true, "expect": true, "browser": true, "element": true, "by": true } } ================================================ FILE: client/app/account/account.js ================================================ 'use strict'; angular.module('paizaqaApp') .config(function($stateProvider) { $stateProvider .state('login', { url: '/login', templateUrl: 'app/account/login/login.html', controller: 'LoginController', controllerAs: 'vm' }) .state('logout', { url: '/logout?referrer', referrer: 'main', template: '', controller: function($state, Auth) { var referrer = $state.params.referrer || $state.current.referrer || 'main'; Auth.logout(); $state.go(referrer); } }) .state('signup', { url: '/signup', templateUrl: 'app/account/signup/signup.html', controller: 'SignupController', controllerAs: 'vm' }) .state('settings', { url: '/settings', templateUrl: 'app/account/settings/settings.html', controller: 'SettingsController', controllerAs: 'vm', authenticate: true }); }) .run(function($rootScope) { $rootScope.$on('$stateChangeStart', function(event, next, nextParams, current) { if (next.name === 'logout' && current && current.name && !current.authenticate) { next.referrer = current.name; } }); }); ================================================ FILE: client/app/account/login/login.controller.js ================================================ 'use strict'; class LoginController { constructor(Auth, $state) { this.user = {}; this.errors = {}; this.submitted = false; this.Auth = Auth; this.$state = $state; } login(form) { this.submitted = true; if (form.$valid) { this.Auth.login({ email: this.user.email, password: this.user.password }) .then(() => { // Logged in, redirect to home this.$state.go('main'); }) .catch(err => { this.errors.other = err.message; }); } } } angular.module('paizaqaApp') .controller('LoginController', LoginController); ================================================ FILE: client/app/account/login/login.html ================================================

Login

Accounts are reset on server restart from server/config/seed.js. Default account is test@example.com / test

Admin account is admin@example.com / admin

Please enter your email and password.

Please enter a valid email.

{{ vm.errors.other }}

Register


================================================ FILE: client/app/account/settings/settings.controller.js ================================================ 'use strict'; class SettingsController { constructor(Auth) { this.errors = {}; this.submitted = false; this.Auth = Auth; } changePassword(form) { this.submitted = true; if (form.$valid) { this.Auth.changePassword(this.user.oldPassword, this.user.newPassword) .then(() => { this.message = 'Password successfully changed.'; }) .catch(() => { form.password.$setValidity('mongoose', false); this.errors.other = 'Incorrect password'; this.message = ''; }); } } } angular.module('paizaqaApp') .controller('SettingsController', SettingsController); ================================================ FILE: client/app/account/settings/settings.html ================================================

Change Password

{{ vm.errors.other }}

Password must be at least 3 characters.

Passwords must match.

{{ vm.message }}

================================================ FILE: client/app/account/signup/signup.controller.js ================================================ 'use strict'; class SignupController { //start-non-standard user = {}; errors = {}; submitted = false; //end-non-standard constructor(Auth, $state) { this.Auth = Auth; this.$state = $state; } register(form) { this.submitted = true; if (form.$valid) { this.Auth.createUser({ name: this.user.name, email: this.user.email, password: this.user.password }) .then(() => { // Account created, redirect to home this.$state.go('main'); }) .catch(err => { err = err.data; this.errors = {}; // Update validity of form fields that match the mongoose errors angular.forEach(err.errors, (error, field) => { form[field].$setValidity('mongoose', false); this.errors[field] = error.message; }); }); } } } angular.module('paizaqaApp') .controller('SignupController', SignupController); ================================================ FILE: client/app/account/signup/signup.html ================================================

Sign up

A name is required

Doesn't look like a valid email.

What's your email address?

{{ vm.errors.email }}

Password must be at least 3 characters.

{{ vm.errors.password }}

Passwords must match.



================================================ FILE: client/app/admin/admin.controller.js ================================================ 'use strict'; (function() { class AdminController { constructor(User) { // Use the User $resource to fetch all users this.users = User.query(); } delete(user) { user.$remove(); this.users.splice(this.users.indexOf(user), 1); } } angular.module('paizaqaApp.admin') .controller('AdminController', AdminController); })(); ================================================ FILE: client/app/admin/admin.html ================================================

The delete user and user index api routes are restricted to users with the 'admin' role.

================================================ FILE: client/app/admin/admin.module.js ================================================ 'use strict'; angular.module('paizaqaApp.admin', [ 'paizaqaApp.auth', 'ui.router' ]); ================================================ FILE: client/app/admin/admin.router.js ================================================ 'use strict'; angular.module('paizaqaApp.admin') .config(function($stateProvider) { $stateProvider .state('admin', { url: '/admin', templateUrl: 'app/admin/admin.html', controller: 'AdminController', controllerAs: 'admin', authenticate: 'admin' }); }); ================================================ FILE: client/app/admin/admin.scss ================================================ .trash { color:rgb(209, 91, 71); } .user-list { li { display: flex; border: none; border-bottom: 1px lightgray solid; margin-bottom: 0; &:last-child { border-bottom: none; } .user-info { flex-grow: 1; } .trash { display: flex; align-items: center; text-decoration: none; } } } ================================================ FILE: client/app/app.constant.js ================================================ (function(angular, undefined) { 'use strict'; angular.module('paizaqaApp.constants', []) .constant('appConfig', {userRoles:['guest','user','admin']}) ; })(angular); ================================================ FILE: client/app/app.js ================================================ 'use strict'; angular.module('paizaqaApp', [ 'paizaqaApp.auth', 'paizaqaApp.admin', 'paizaqaApp.constants', 'ngCookies', 'ngResource', 'ngSanitize', 'btford.socket-io', 'ui.router', 'ui.bootstrap', 'validation.match', 'ui.pagedown', 'ngTagsInput', 'ngMessages', 'infinite-scroll', ]) .config(function($urlRouterProvider, $locationProvider) { $urlRouterProvider .otherwise('/'); $locationProvider.html5Mode(true); }); ================================================ FILE: client/app/app.scss ================================================ $icon-font-path: "../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/"; @import '../bower_components/bootstrap-sass-official/vendor/assets/stylesheets/bootstrap'; @import '../bower_components/bootstrap-social/bootstrap-social.scss'; $fa-font-path: "../bower_components/font-awesome/fonts"; @import '../bower_components/font-awesome/scss/font-awesome'; /** * App-wide Styles */ .browserupgrade { margin: 0.2em 0; background: #ccc; color: #000; padding: 0.2em 0; } // Component styles are injected through grunt // injector @import 'admin/admin.scss'; @import 'questionsCreate/questionsCreate.scss'; @import 'questionsIndex/questionsIndex.scss'; @import 'questionsShow/questionsShow.scss'; @import '../components/footer/footer.scss'; @import '../components/modal/modal.scss'; @import '../components/oauth-buttons/oauth-buttons.scss'; // endinjector ================================================ FILE: client/app/fromNow/fromNow.filter.js ================================================ 'use strict'; angular.module('paizaqaApp') .filter('fromNow', function () { return function (input) { return moment(input).locale(window.navigator.language).fromNow(); }; }); ================================================ FILE: client/app/fromNow/fromNow.filter.spec.js ================================================ 'use strict'; describe('Filter: fromNow', function () { // load the filter's module beforeEach(module('paizaqaApp')); // initialize a new instance of the filter before each test var fromNow; beforeEach(inject(function ($filter) { fromNow = $filter('fromNow'); })); it('return "a few seconds ago" for now', function () { expect(fromNow(Date.now())).toBe('a few seconds ago'); }); }); ================================================ FILE: client/app/questionsCreate/questionsCreate.controller.js ================================================ 'use strict'; angular.module('paizaqaApp') .controller('QuestionsCreateCtrl', function ($scope, $http, $location, Auth) { if(! Auth.isLoggedIn()){ $location.path('/login'); $location.replace(); return; } $scope.submit = function() { $http.post('/api/questions', $scope.question).success(function(){ $location.path('/'); }); }; }); ================================================ FILE: client/app/questionsCreate/questionsCreate.controller.spec.js ================================================ 'use strict'; describe('Controller: QuestionsCreateCtrl', function () { // load the controller's module beforeEach(module('paizaqaApp')); var QuestionsCreateCtrl, scope; // Initialize the controller and a mock scope beforeEach(inject(function ($controller, $rootScope) { scope = $rootScope.$new(); QuestionsCreateCtrl = $controller('QuestionsCreateCtrl', { $scope: scope }); })); it('should ...', function () { expect(1).toEqual(1); }); }); ================================================ FILE: client/app/questionsCreate/questionsCreate.html ================================================

Title:

Required OK

Question:

Required OK

Tags:

================================================ FILE: client/app/questionsCreate/questionsCreate.js ================================================ 'use strict'; angular.module('paizaqaApp') .config(function ($stateProvider) { $stateProvider .state('questionsCreate', { url: '/questions/create', templateUrl: 'app/questionsCreate/questionsCreate.html', controller: 'QuestionsCreateCtrl' }); }); ================================================ FILE: client/app/questionsCreate/questionsCreate.scss ================================================ ================================================ FILE: client/app/questionsIndex/questionsIndex.controller.js ================================================ 'use strict'; angular.module('paizaqaApp') .controller('QuestionsIndexCtrl', function ($scope, $http, $location, Auth, query) { var keyword = $location.search().keyword; if(keyword){ query = _.merge(query, {$text: {$search: keyword}}); } $scope.busy = true; $scope.noMoreData = false; $http.get('/api/questions', {params: {query: query}}).success(function(questions) { $scope.questions = questions; if($scope.questions.length < 20){ $scope.noMoreData = true; } $scope.busy = false; }); $scope.nextPage = function(){ if($scope.busy){ return; } $scope.busy = true; var lastId = $scope.questions[$scope.questions.length-1]._id; var pageQuery = _.merge(query, {_id: {$lt: lastId}}); $http.get('/api/questions', {params: {query: pageQuery}}).success(function(questions){ $scope.questions = $scope.questions.concat(questions); $scope.busy = false; if(questions.length === 0){ $scope.noMoreData = true; } }); }; $scope.isStar = function(obj){ return Auth.isLoggedIn() && obj && obj.stars && obj.stars.indexOf(Auth.getCurrentUser()._id)!==-1; }; }); ================================================ FILE: client/app/questionsIndex/questionsIndex.controller.spec.js ================================================ 'use strict'; describe('Controller: QuestionsIndexCtrl', function () { // load the controller's module beforeEach(module('paizaqaApp')); var QuestionsIndexCtrl, scope; // Initialize the controller and a mock scope beforeEach(inject(function ($controller, $rootScope) { scope = $rootScope.$new(); QuestionsIndexCtrl = $controller('QuestionsIndexCtrl', { $scope: scope, query: {}, }); })); it('should ...', function () { expect(1).toEqual(1); }); }); ================================================ FILE: client/app/questionsIndex/questionsIndex.html ================================================

Ask Question
Stars Answers Question
{{question.stars.length}}
{{question.answers.length}}
{{question.title}}
by {{question.user.name}} - {{question.createdAt|fromNow}}
{{tag.text}}  
Loading data...
================================================ FILE: client/app/questionsIndex/questionsIndex.js ================================================ 'use strict'; angular.module('paizaqaApp') .config(function ($stateProvider) { $stateProvider .state('main', { url: '/?keyword', templateUrl: 'app/questionsIndex/questionsIndex.html', controller: 'QuestionsIndexCtrl', resolve: { query: function(){return {};} }, }) .state('starredQuestionsIndex', { url: '/users/:userId/starred', templateUrl: 'app/questionsIndex/questionsIndex.html', controller: 'QuestionsIndexCtrl', resolve: { query: function($stateParams){ return { $or: [ {'stars': $stateParams.userId}, {'answers.stars': $stateParams.userId}, {'comments.stars': $stateParams.userId}, {'answers.comments.stars': $stateParams.userId}, ] }; } }, }) .state('userQuestionsIndex', { url: '/users/:userId', templateUrl: 'app/questionsIndex/questionsIndex.html', controller: 'QuestionsIndexCtrl', resolve: { query: function($stateParams){ return {user: $stateParams.userId}; } }, }); }); ================================================ FILE: client/app/questionsIndex/questionsIndex.scss ================================================ #banner { border-bottom: none; margin-top: -20px; } #banner h1 { font-size: 60px; line-height: 1; letter-spacing: -1px; } .hero-unit { position: relative; padding: 30px 15px; color: #F5F5F5; text-align: center; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); background: #4393B9; } ================================================ FILE: client/app/questionsShow/questionsShow.controller.js ================================================ 'use strict'; angular.module('paizaqaApp') .controller('QuestionsShowCtrl', function ($scope, $http, $stateParams, Auth, $location) { var loadQuestions = function(){ $http.get('/api/questions/' + $stateParams.id).success(function(question) { $scope.question = question; }); }; loadQuestions(); $scope.newAnswer = {}; $scope.submitAnswer = function() { $http.post('/api/questions/' + $stateParams.id + '/answers', $scope.newAnswer).success(function(){ loadQuestions(); $scope.newAnswer = {}; }); }; $scope.deleteQuestion = function() { $http.delete('/api/questions/' + $stateParams.id).success(function(){ $location.path('/'); }); }; $scope.deleteAnswer = function(answer) { $http.delete('/api/questions/' + $stateParams.id + '/answers/' + answer._id).success(function(){ loadQuestions(); }); }; $scope.updateQuestion = function() { $http.put('/api/questions/' + $stateParams.id, $scope.question).success(function(){ loadQuestions(); }); }; $scope.updateAnswer = function(answer) { $http.put('/api/questions/' + $stateParams.id + '/answers/' + answer._id, answer).success(function(){ loadQuestions(); }); }; $scope.isOwner = function(obj){ return Auth.isLoggedIn() && obj && obj.user && obj.user._id === Auth.getCurrentUser()._id; }; $scope.newComment = {}; $scope.submitComment = function() { $http.post('/api/questions/' + $stateParams.id + '/comments', $scope.newComment).success(function(){ loadQuestions(); $scope.newComment = {}; $scope.editNewComment = false; }); }; $scope.submitAnswerComment = function(answer) { $http.post('/api/questions/' + $stateParams.id + '/answers/' + answer._id + '/comments', answer.newAnswerComment).success(function(){ loadQuestions(); }); }; $scope.deleteComment = function(comment) { $http.delete('/api/questions/' + $stateParams.id + '/comments/' + comment._id).success(function(){ loadQuestions(); }); }; $scope.deleteAnswerComment = function(answer, answerComment) { $http.delete('/api/questions/' + $stateParams.id + '/answers/' + answer._id + '/comments/' + answerComment._id).success(function(){ loadQuestions(); }); }; $scope.updateComment = function(comment) { $http.put('/api/questions/' + $stateParams.id + '/comments/' + comment._id, comment).success(function(){ loadQuestions(); }); }; $scope.updateAnswerComment = function(answer, answerComment) { $http.put('/api/questions/' + $stateParams.id + '/answers/' + answer._id + '/comments/' + answerComment._id, answerComment).success(function(){ loadQuestions(); }); }; $scope.isStar = function(obj){ return Auth.isLoggedIn() && obj && obj.stars && obj.stars.indexOf(Auth.getCurrentUser()._id)!==-1; }; $scope.star = function(subpath) { $http.put('/api/questions/' + $scope.question._id + subpath + '/star').success(function(){ loadQuestions(); }); }; $scope.unstar = function(subpath) { $http.delete('/api/questions/' + $scope.question._id + subpath + '/star').success(function(){ loadQuestions(); }); }; }); ================================================ FILE: client/app/questionsShow/questionsShow.controller.spec.js ================================================ 'use strict'; describe('Controller: QuestionsShowCtrl', function () { // load the controller's module beforeEach(module('paizaqaApp')); var QuestionsShowCtrl, scope; // Initialize the controller and a mock scope beforeEach(inject(function ($controller, $rootScope) { scope = $rootScope.$new(); QuestionsShowCtrl = $controller('QuestionsShowCtrl', { $scope: scope }); })); it('should ...', function () { expect(1).toEqual(1); }); }); ================================================ FILE: client/app/questionsShow/questionsShow.html ================================================

{{question.stars.length}}

{{question.title}}

{{tag.text}}

Edit
by {{question.user.name}} - {{question.createdAt|fromNow}}
 


{{comment.stars.length}}
Edit
by {{comment.user.name}} - {{comment.createdAt|fromNow}}

add a comment
 

{{question.answers.length}} Answers



{{answer.stars.length}}
Edit
by {{answer.user.name}} - {{answer.createdAt|fromNow}}


{{comment.stars.length}}
Edit
by {{comment.user.name}} - {{comment.createdAt|fromNow}}

add a comment


Your answer

================================================ FILE: client/app/questionsShow/questionsShow.js ================================================ 'use strict'; angular.module('paizaqaApp') .config(function ($stateProvider) { $stateProvider .state('questionsShow', { url: '/questions/show/:id', templateUrl: 'app/questionsShow/questionsShow.html', controller: 'QuestionsShowCtrl' }); }); ================================================ FILE: client/app/questionsShow/questionsShow.scss ================================================ #question-show-container .comment{ hr { margin: 0; }; p { margin: 0; }; margin-left: 100px; }; #question-show-container .answer{ margin-left: 50px; }; .comment-wmd-input { height: 50px; width: 100%; background-color: Gainsboro; border: 1px solid DarkGray; }; ================================================ FILE: client/components/auth/auth.module.js ================================================ 'use strict'; angular.module('paizaqaApp.auth', [ 'paizaqaApp.constants', 'paizaqaApp.util', 'ngCookies', 'ui.router' ]) .config(function($httpProvider) { $httpProvider.interceptors.push('authInterceptor'); }); ================================================ FILE: client/components/auth/auth.service.js ================================================ 'use strict'; (function() { function AuthService($location, $http, $cookies, $q, appConfig, Util, User) { var safeCb = Util.safeCb; var currentUser = {}; var userRoles = appConfig.userRoles || []; if ($cookies.get('token') && $location.path() !== '/logout') { currentUser = User.get(); } var Auth = { /** * Authenticate user and save token * * @param {Object} user - login info * @param {Function} callback - optional, function(error, user) * @return {Promise} */ login({email, password}, callback) { return $http.post('/auth/local', { email: email, password: password }) .then(res => { $cookies.put('token', res.data.token); currentUser = User.get(); return currentUser.$promise; }) .then(user => { safeCb(callback)(null, user); return user; }) .catch(err => { Auth.logout(); safeCb(callback)(err.data); return $q.reject(err.data); }); }, /** * Delete access token and user info */ logout() { $cookies.remove('token'); currentUser = {}; }, /** * Create a new user * * @param {Object} user - user info * @param {Function} callback - optional, function(error, user) * @return {Promise} */ createUser(user, callback) { return User.save(user, function(data) { $cookies.put('token', data.token); currentUser = User.get(); return safeCb(callback)(null, user); }, function(err) { Auth.logout(); return safeCb(callback)(err); }).$promise; }, /** * Change password * * @param {String} oldPassword * @param {String} newPassword * @param {Function} callback - optional, function(error, user) * @return {Promise} */ changePassword(oldPassword, newPassword, callback) { return User.changePassword({ id: currentUser._id }, { oldPassword: oldPassword, newPassword: newPassword }, function() { return safeCb(callback)(null); }, function(err) { return safeCb(callback)(err); }).$promise; }, /** * Gets all available info on a user * (synchronous|asynchronous) * * @param {Function|*} callback - optional, funciton(user) * @return {Object|Promise} */ getCurrentUser(callback) { if (arguments.length === 0) { return currentUser; } var value = (currentUser.hasOwnProperty('$promise')) ? currentUser.$promise : currentUser; return $q.when(value) .then(user => { safeCb(callback)(user); return user; }, () => { safeCb(callback)({}); return {}; }); }, /** * Check if a user is logged in * (synchronous|asynchronous) * * @param {Function|*} callback - optional, function(is) * @return {Bool|Promise} */ isLoggedIn(callback) { if (arguments.length === 0) { return currentUser.hasOwnProperty('role'); } return Auth.getCurrentUser(null) .then(user => { var is = user.hasOwnProperty('role'); safeCb(callback)(is); return is; }); }, /** * Check if a user has a specified role or higher * (synchronous|asynchronous) * * @param {String} role - the role to check against * @param {Function|*} callback - optional, function(has) * @return {Bool|Promise} */ hasRole(role, callback) { var hasRole = function(r, h) { return userRoles.indexOf(r) >= userRoles.indexOf(h); }; if (arguments.length < 2) { return hasRole(currentUser.role, role); } return Auth.getCurrentUser(null) .then(user => { var has = (user.hasOwnProperty('role')) ? hasRole(user.role, role) : false; safeCb(callback)(has); return has; }); }, /** * Check if a user is an admin * (synchronous|asynchronous) * * @param {Function|*} callback - optional, function(is) * @return {Bool|Promise} */ isAdmin() { return Auth.hasRole .apply(Auth, [].concat.apply(['admin'], arguments)); }, /** * Get auth token * * @return {String} - a token string used for authenticating */ getToken() { return $cookies.get('token'); } }; return Auth; } angular.module('paizaqaApp.auth') .factory('Auth', AuthService); })(); ================================================ FILE: client/components/auth/interceptor.service.js ================================================ 'use strict'; (function() { function authInterceptor($rootScope, $q, $cookies, $injector, Util) { var state; return { // Add authorization token to headers request(config) { config.headers = config.headers || {}; if ($cookies.get('token') && Util.isSameOrigin(config.url)) { config.headers.Authorization = 'Bearer ' + $cookies.get('token'); } return config; }, // Intercept 401s and redirect you to login responseError(response) { if (response.status === 401) { (state || (state = $injector.get('$state'))).go('login'); // remove any stale tokens $cookies.remove('token'); } return $q.reject(response); } }; } angular.module('paizaqaApp.auth') .factory('authInterceptor', authInterceptor); })(); ================================================ FILE: client/components/auth/router.decorator.js ================================================ 'use strict'; (function() { angular.module('paizaqaApp.auth') .run(function($rootScope, $state, Auth) { // Redirect to login if route requires auth and the user is not logged in, or doesn't have required role $rootScope.$on('$stateChangeStart', function(event, next) { if (!next.authenticate) { return; } if (typeof next.authenticate === 'string') { Auth.hasRole(next.authenticate, _.noop).then(has => { if (has) { return; } event.preventDefault(); return Auth.isLoggedIn(_.noop).then(is => { $state.go(is ? 'main' : 'login'); }); }); } else { Auth.isLoggedIn(_.noop).then(is => { if (is) { return; } event.preventDefault(); $state.go('main'); }); } }); }); })(); ================================================ FILE: client/components/auth/user.service.js ================================================ 'use strict'; (function() { function UserResource($resource) { return $resource('/api/users/:id/:controller', { id: '@_id' }, { changePassword: { method: 'PUT', params: { controller: 'password' } }, get: { method: 'GET', params: { id: 'me' } } }); } angular.module('paizaqaApp.auth') .factory('User', UserResource); })(); ================================================ FILE: client/components/footer/footer.directive.js ================================================ 'use strict'; angular.module('paizaqaApp') .directive('footer', function() { return { templateUrl: 'components/footer/footer.html', restrict: 'E', link: function(scope, element) { element.addClass('footer'); } }; }); ================================================ FILE: client/components/footer/footer.html ================================================

Angular Fullstack v3.3.0 | @tyhenkel | Issues

================================================ FILE: client/components/footer/footer.scss ================================================ footer.footer { text-align: center; padding: 30px 0; margin-top: 70px; border-top: 1px solid #E5E5E5; } ================================================ FILE: client/components/modal/modal.html ================================================ ================================================ FILE: client/components/modal/modal.scss ================================================ .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: $brand-primary; } .modal-info .modal-header { background: $brand-info; } .modal-success .modal-header { background: $brand-success; } .modal-warning .modal-header { background: $brand-warning; } .modal-danger .modal-header { background: $brand-danger; } ================================================ FILE: client/components/modal/modal.service.js ================================================ 'use strict'; angular.module('paizaqaApp') .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 = 'modal-default') { var modalScope = $rootScope.$new(); 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(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 straight 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); }); }; } } }; }); ================================================ FILE: client/components/mongoose-error/mongoose-error.directive.js ================================================ 'use strict'; /** * Removes server error when user updates input */ angular.module('paizaqaApp') .directive('mongooseError', function() { return { restrict: 'A', require: 'ngModel', link: function(scope, element, attrs, ngModel) { element.on('keydown', () => ngModel.$setValidity('mongoose', true)); } }; }); ================================================ FILE: client/components/navbar/navbar.controller.js ================================================ 'use strict'; class NavbarController { //start-non-standard isCollapsed = true; //end-non-standard constructor(Auth, $state) { this.menu = [ { 'title': 'All', 'link': function(){return '/';}, 'show': function(){return true;}, }, { 'title': 'Mine', 'link': function(){return '/users/' + Auth.getCurrentUser()._id;}, 'show': Auth.isLoggedIn, }, { 'title': 'Starred', 'link': function(){return '/users/' + Auth.getCurrentUser()._id + '/starred';}, 'show': Auth.isLoggedIn, }, ]; this.isLoggedIn = Auth.isLoggedIn; this.isAdmin = Auth.isAdmin; this.getCurrentUser = Auth.getCurrentUser; this.search = function(keyword) { $state.go('main', {keyword: keyword}, {reload: true}); }; } } angular.module('paizaqaApp') .controller('NavbarController', NavbarController); ================================================ FILE: client/components/navbar/navbar.directive.js ================================================ 'use strict'; angular.module('paizaqaApp') .directive('navbar', () => ({ templateUrl: 'components/navbar/navbar.html', restrict: 'E', controller: 'NavbarController', controllerAs: 'nav' })); ================================================ FILE: client/components/navbar/navbar.html ================================================ ================================================ FILE: client/components/oauth-buttons/oauth-buttons.controller.js ================================================ 'use strict'; angular.module('paizaqaApp') .controller('OauthButtonsCtrl', function($window) { this.loginOauth = function(provider) { $window.location.href = '/auth/' + provider; }; }); ================================================ FILE: client/components/oauth-buttons/oauth-buttons.controller.spec.js ================================================ 'use strict'; describe('Controller: OauthButtonsCtrl', function() { // load the controller's module beforeEach(module('paizaqaApp')); var OauthButtonsCtrl, $window; // Initialize the controller and a mock $window beforeEach(inject(function($controller) { $window = { location: {} }; OauthButtonsCtrl = $controller('OauthButtonsCtrl', { $window: $window }); })); it('should attach loginOauth', function() { expect(OauthButtonsCtrl.loginOauth).toEqual(jasmine.any(Function)); }); }); ================================================ FILE: client/components/oauth-buttons/oauth-buttons.directive.js ================================================ 'use strict'; angular.module('paizaqaApp') .directive('oauthButtons', function() { return { templateUrl: 'components/oauth-buttons/oauth-buttons.html', restrict: 'EA', controller: 'OauthButtonsCtrl', controllerAs: 'OauthButtons', scope: { classes: '@' } }; }); ================================================ FILE: client/components/oauth-buttons/oauth-buttons.directive.spec.js ================================================ 'use strict'; describe('Directive: oauthButtons', function() { // load the directive's module and view beforeEach(module('paizaqaApp')); beforeEach(module('components/oauth-buttons/oauth-buttons.html')); var element, parentScope, elementScope; var compileDirective = function(template) { inject(function($compile) { element = angular.element(template); element = $compile(element)(parentScope); parentScope.$digest(); elementScope = element.isolateScope(); }); }; beforeEach(inject(function($rootScope) { parentScope = $rootScope.$new(); })); it('should contain anchor buttons', function() { compileDirective(''); expect(element.find('a.btn.btn-social').length).toBeGreaterThan(0); }); it('should evaluate and bind the classes attribute to scope.classes', function() { parentScope.scopedClass = 'scopedClass1'; compileDirective(''); expect(elementScope.classes).toEqual('testClass1 scopedClass1'); }); it('should bind scope.classes to class names on the anchor buttons', function() { compileDirective(''); // Add classes elementScope.classes = 'testClass1 testClass2'; elementScope.$digest(); expect(element.find('a.btn.btn-social.testClass1.testClass2').length).toBeGreaterThan(0); // Remove classes elementScope.classes = ''; elementScope.$digest(); expect(element.find('a.btn.btn-social.testClass1.testClass2').length).toEqual(0); }); }); ================================================ FILE: client/components/oauth-buttons/oauth-buttons.html ================================================ Connect with Facebook Connect with Google+ Connect with Twitter ================================================ FILE: client/components/oauth-buttons/oauth-buttons.scss ================================================ ================================================ FILE: client/components/socket/socket.mock.js ================================================ 'use strict'; angular.module('socketMock', []) .factory('socket', function() { return { socket: { connect: function() {}, on: function() {}, emit: function() {}, receive: function() {} }, syncUpdates: function() {}, unsyncUpdates: function() {} }; }); ================================================ FILE: client/components/socket/socket.service.js ================================================ /* global io */ 'use strict'; angular.module('paizaqaApp') .factory('socket', function(socketFactory) { // socket.io now auto-configures its connection when we ommit a connection url var ioSocket = io('', { // Send auth token on connection, you will need to DI the Auth service above // 'query': 'token=' + Auth.getToken() path: '/socket.io-client' }); var socket = socketFactory({ ioSocket }); return { socket, /** * Register listeners to sync an array with updates on a model * * Takes the array we want to sync, the model name that socket updates are sent from, * and an optional callback function after new items are updated. * * @param {String} modelName * @param {Array} array * @param {Function} cb */ syncUpdates(modelName, array, cb) { cb = cb || angular.noop; /** * Syncs item creation/updates on 'model:save' */ socket.on(modelName + ':save', function (item) { var oldItem = _.find(array, {_id: item._id}); var index = array.indexOf(oldItem); var event = 'created'; // replace oldItem if it exists // otherwise just add item to the collection if (oldItem) { array.splice(index, 1, item); event = 'updated'; } else { array.push(item); } cb(event, item, array); }); /** * Syncs removed items on 'model:remove' */ socket.on(modelName + ':remove', function (item) { var event = 'deleted'; _.remove(array, {_id: item._id}); cb(event, item, array); }); }, /** * Removes listeners for a models updates on the socket * * @param modelName */ unsyncUpdates(modelName) { socket.removeAllListeners(modelName + ':save'); socket.removeAllListeners(modelName + ':remove'); } }; }); ================================================ FILE: client/components/ui-router/ui-router.mock.js ================================================ 'use strict'; angular.module('stateMock', []); angular.module('stateMock').service('$state', function($q) { this.expectedTransitions = []; this.transitionTo = function(stateName) { if (this.expectedTransitions.length > 0) { var expectedState = this.expectedTransitions.shift(); if (expectedState !== stateName) { throw Error('Expected transition to state: ' + expectedState + ' but transitioned to ' + stateName); } } else { throw Error('No more transitions were expected! Tried to transition to ' + stateName); } console.log('Mock transition to: ' + stateName); var deferred = $q.defer(); var promise = deferred.promise; deferred.resolve(); return promise; }; this.go = this.transitionTo; this.expectTransitionTo = function(stateName) { this.expectedTransitions.push(stateName); }; this.ensureAllTransitionsHappened = function() { if (this.expectedTransitions.length > 0) { throw Error('Not all transitions happened!'); } }; }); ================================================ FILE: client/components/util/util.module.js ================================================ 'use strict'; angular.module('paizaqaApp.util', []); ================================================ FILE: client/components/util/util.service.js ================================================ 'use strict'; (function() { /** * The Util service is for thin, globally reusable, utility functions */ function UtilService($window) { var Util = { /** * Return a callback or noop function * * @param {Function|*} cb - a 'potential' function * @return {Function} */ safeCb(cb) { return (angular.isFunction(cb)) ? cb : angular.noop; }, /** * Parse a given url with the use of an anchor element * * @param {String} url - the url to parse * @return {Object} - the parsed url, anchor element */ urlParse(url) { var a = document.createElement('a'); a.href = url; // Special treatment for IE, see http://stackoverflow.com/a/13405933 for details if (a.host === '') { a.href = a.href; } return a; }, /** * Test whether or not a given url is same origin * * @param {String} url - url to test * @param {String|String[]} [origins] - additional origins to test against * @return {Boolean} - true if url is same origin */ isSameOrigin(url, origins) { url = Util.urlParse(url); origins = (origins && [].concat(origins)) || []; origins = origins.map(Util.urlParse); origins.push($window.location); origins = origins.filter(function(o) { return url.hostname === o.hostname && url.port === o.port && url.protocol === o.protocol; }); return (origins.length >= 1); } }; return Util; } angular.module('paizaqaApp.util') .factory('Util', UtilService); })(); ================================================ FILE: client/index.html ================================================
Fork me on GitHub ================================================ FILE: client/robots.txt ================================================ # robotstxt.org User-agent: * ================================================ FILE: e2e/account/login/login.po.js ================================================ /** * 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 LoginPage = function() { var form = this.form = element(by.css('.form')); form.email = form.element(by.model('vm.user.email')); form.password = form.element(by.model('vm.user.password')); form.submit = form.element(by.css('.btn-login')); form.oauthButtons = require('../../components/oauth-buttons/oauth-buttons.po').oauthButtons; this.login = function(data) { for (var prop in data) { var formElem = form[prop]; if (data.hasOwnProperty(prop) && formElem && typeof formElem.sendKeys === 'function') { formElem.sendKeys(data[prop]); } } return form.submit.click(); }; }; module.exports = new LoginPage(); ================================================ FILE: e2e/account/login/login.spec.js ================================================ 'use strict'; var config = browser.params; var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); describe('Login View', function() { var page; var loadPage = function() { browser.get(config.baseUrl + '/login'); page = require('./login.po'); }; var testUser = { name: 'Test User', email: 'test@example.com', password: 'test' }; beforeEach(function(done) { UserModel.removeAsync() .then(function() { return UserModel.createAsync(testUser); }) .then(loadPage) .finally(function() { browser.wait(function() { //console.log('waiting for angular...'); return browser.executeScript('return !!window.angular'); }, 5000).then(done); }); }); it('should include login form with correct inputs and submit button', function() { expect(page.form.email.getAttribute('type')).toBe('email'); expect(page.form.email.getAttribute('name')).toBe('email'); expect(page.form.password.getAttribute('type')).toBe('password'); expect(page.form.password.getAttribute('name')).toBe('password'); expect(page.form.submit.getAttribute('type')).toBe('submit'); expect(page.form.submit.getText()).toBe('Login'); }); it('should include oauth buttons with correct classes applied', function() { expect(page.form.oauthButtons.facebook.getText()).toBe('Connect with Facebook'); expect(page.form.oauthButtons.facebook.getAttribute('class')).toMatch('btn-block'); expect(page.form.oauthButtons.google.getText()).toBe('Connect with Google+'); expect(page.form.oauthButtons.google.getAttribute('class')).toMatch('btn-block'); expect(page.form.oauthButtons.twitter.getText()).toBe('Connect with Twitter'); expect(page.form.oauthButtons.twitter.getAttribute('class')).toMatch('btn-block'); }); describe('with local auth', function() { it('should login a user and redirecting to "/"', function() { page.login(testUser); var navbar = require('../../components/navbar/navbar.po'); expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/'); expect(navbar.navbarAccountGreeting.getText()).toBe('Hello ' + testUser.name); }); it('should indicate login failures', function() { page.login({ email: testUser.email, password: 'badPassword' }); expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/login'); var helpBlock = page.form.element(by.css('.form-group.has-error .help-block.ng-binding')); expect(helpBlock.getText()).toBe('This password is not correct.'); }); }); }); ================================================ FILE: e2e/account/logout/logout.spec.js ================================================ 'use strict'; var config = browser.params; var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); describe('Logout View', function() { var login = function(user) { browser.get(config.baseUrl + '/login'); require('../login/login.po').login(user); }; var testUser = { name: 'Test User', email: 'test@example.com', password: 'test' }; beforeEach(function(done) { UserModel.removeAsync() .then(function() { return UserModel.createAsync(testUser); }) .then(function() { return login(testUser); }) .finally(function() { browser.wait(function() { return browser.executeScript('return !!window.angular'); }, 5000).then(done); }); }); describe('with local auth', function() { it('should logout a user and redirecting to "/"', function() { var navbar = require('../../components/navbar/navbar.po'); expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/'); expect(navbar.navbarAccountGreeting.getText()).toBe('Hello ' + testUser.name); browser.get(config.baseUrl + '/logout'); navbar = require('../../components/navbar/navbar.po'); expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/'); expect(navbar.navbarAccountGreeting.isDisplayed()).toBe(false); }); }); }); ================================================ FILE: e2e/account/signup/signup.po.js ================================================ /** * 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 SignupPage = function() { var form = this.form = element(by.css('.form')); form.name = form.element(by.model('vm.user.name')); form.email = form.element(by.model('vm.user.email')); form.password = form.element(by.model('vm.user.password')); form.confirmPassword = form.element(by.model('vm.user.confirmPassword')); form.submit = form.element(by.css('.btn-register')); form.oauthButtons = require('../../components/oauth-buttons/oauth-buttons.po').oauthButtons; this.signup = function(data) { for (var prop in data) { var formElem = form[prop]; if (data.hasOwnProperty(prop) && formElem && typeof formElem.sendKeys === 'function') { formElem.sendKeys(data[prop]); } } return form.submit.click(); }; }; module.exports = new SignupPage(); ================================================ FILE: e2e/account/signup/signup.spec.js ================================================ 'use strict'; var config = browser.params; var UserModel = require(config.serverConfig.root + '/server/api/user/user.model'); describe('Signup View', function() { var page; var loadPage = function() { browser.manage().deleteAllCookies(); browser.get(config.baseUrl + '/signup'); page = require('./signup.po'); }; var testUser = { name: 'Test', email: 'test@example.com', password: 'test', confirmPassword: 'test' }; beforeEach(function(done) { loadPage(); browser.wait(function() { return browser.executeScript('return !!window.angular'); }, 5000).then(done); }); it('should include signup form with correct inputs and submit button', function() { expect(page.form.name.getAttribute('type')).toBe('text'); expect(page.form.name.getAttribute('name')).toBe('name'); expect(page.form.email.getAttribute('type')).toBe('email'); expect(page.form.email.getAttribute('name')).toBe('email'); expect(page.form.password.getAttribute('type')).toBe('password'); expect(page.form.password.getAttribute('name')).toBe('password'); expect(page.form.confirmPassword.getAttribute('type')).toBe('password'); expect(page.form.confirmPassword.getAttribute('name')).toBe('confirmPassword'); expect(page.form.submit.getAttribute('type')).toBe('submit'); expect(page.form.submit.getText()).toBe('Sign up'); }); it('should include oauth buttons with correct classes applied', function() { expect(page.form.oauthButtons.facebook.getText()).toBe('Connect with Facebook'); expect(page.form.oauthButtons.facebook.getAttribute('class')).toMatch('btn-block'); expect(page.form.oauthButtons.google.getText()).toBe('Connect with Google+'); expect(page.form.oauthButtons.google.getAttribute('class')).toMatch('btn-block'); expect(page.form.oauthButtons.twitter.getText()).toBe('Connect with Twitter'); expect(page.form.oauthButtons.twitter.getAttribute('class')).toMatch('btn-block'); }); describe('with local auth', function() { beforeAll(function(done) { UserModel.removeAsync().then(done); }); it('should signup a new user, log them in, and redirecting to "/"', function() { page.signup(testUser); var navbar = require('../../components/navbar/navbar.po'); expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/'); expect(navbar.navbarAccountGreeting.getText()).toBe('Hello ' + testUser.name); }); it('should indicate signup failures', function() { page.signup(testUser); expect(browser.getCurrentUrl()).toBe(config.baseUrl + '/signup'); expect(page.form.email.getAttribute('class')).toContain('ng-invalid-mongoose'); var helpBlock = page.form.element(by.css('.form-group.has-error .help-block.ng-binding')); expect(helpBlock.getText()).toBe('The specified email address is already in use.'); }); }); }); ================================================ FILE: e2e/components/navbar/navbar.po.js ================================================ /** * 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 NavbarComponent = function() { this.navbar = element(by.css('.navbar')); this.navbarHeader = this.navbar.element(by.css('.navbar-header')); this.navbarNav = this.navbar.element(by.css('#navbar-main .nav.navbar-nav:not(.navbar-right)')); this.navbarAccount = this.navbar.element(by.css('#navbar-main .nav.navbar-nav.navbar-right')); this.navbarAccountGreeting = this.navbarAccount.element(by.binding('getCurrentUser().name')); }; module.exports = new NavbarComponent(); ================================================ FILE: e2e/components/oauth-buttons/oauth-buttons.po.js ================================================ /** * 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 OauthButtons = function() { var oauthButtons = this.oauthButtons = element(by.css('oauth-buttons')); oauthButtons.facebook = oauthButtons.element(by.css('.btn.btn-social.btn-facebook')); oauthButtons.google = oauthButtons.element(by.css('.btn.btn-social.btn-google')); oauthButtons.twitter = oauthButtons.element(by.css('.btn.btn-social.btn-twitter')); }; module.exports = new OauthButtons(); ================================================ FILE: e2e/main/main.po.js ================================================ /** * 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(); ================================================ FILE: e2e/main/main.spec.js ================================================ 'use strict'; var config = browser.params; describe('Main View', function() { var page; beforeEach(function() { browser.get(config.baseUrl + '/'); 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(/yeoman.png$/); expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman'); }); }); ================================================ FILE: karma.conf.js ================================================ // 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: [ // bower:js 'client/bower_components/jquery/dist/jquery.js', 'client/bower_components/angular/angular.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-bootstrap/ui-bootstrap-tpls.js', 'client/bower_components/lodash/dist/lodash.compat.js', 'client/bower_components/angular-socket-io/socket.js', 'client/bower_components/angular-ui-router/release/angular-ui-router.js', 'client/bower_components/angular-validation-match/dist/angular-validation-match.min.js', 'client/bower_components/pagedown/Markdown.Converter.js', 'client/bower_components/pagedown/Markdown.Sanitizer.js', 'client/bower_components/pagedown/Markdown.Extra.js', 'client/bower_components/pagedown/Markdown.Editor.js', 'client/bower_components/angular-pagedown/angular-pagedown.js', 'client/bower_components/ng-tags-input/ng-tags-input.min.js', 'client/bower_components/angular-messages/angular-messages.js', 'client/bower_components/moment/moment.js', 'client/bower_components/ngInfiniteScroll/build/ng-infinite-scroll.js', 'client/bower_components/angular-mocks/angular-mocks.js', // endbower 'node_modules/socket.io-client/socket.io.js', 'client/app/app.js', 'client/{app,components}/**/*.module.js', 'client/{app,components}/**/*.js', 'client/{app,components}/**/*.html' ], preprocessors: { '**/*.html': 'ng-html2js', 'client/{app,components}/**/*.js': 'babel' }, ngHtml2JsPreprocessor: { stripPrefix: 'client/' }, babelPreprocessor: { options: { sourceMap: 'inline', optional: [ 'es7.classProperties' ] }, filename: function (file) { return file.originalPath.replace(/\.js$/, '.es5.js'); }, sourceFileName: function (file) { return file.originalPath; } }, // 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, // reporter types: // - dots // - progress (default) // - spec (karma-spec-reporter) // - junit // - growl // - coverage reporters: ['spec'], // 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 }); }; ================================================ FILE: mocha.conf.js ================================================ 'use strict'; // Register the Babel require hook require('babel-core/register'); var chai = require('chai'); // Load Chai assertions global.expect = chai.expect; global.assert = chai.assert; chai.should(); // Load Sinon global.sinon = require('sinon'); // Initialize Chai plugins chai.use(require('sinon-chai')); chai.use(require('chai-as-promised')); chai.use(require('chai-things')) ================================================ FILE: package.json ================================================ { "name": "paizaqa", "version": "0.0.0", "main": "server/app.js", "dependencies": { "babel-runtime": "^5.8.20", "bluebird": "^2.9.34", "body-parser": "^1.13.3", "composable-middleware": "^0.3.0", "compression": "^1.5.2", "connect-mongo": "^0.8.1", "cookie-parser": "^1.3.5", "ejs": "^2.3.3", "errorhandler": "^1.4.2", "express": "^4.13.3", "express-jwt": "^3.0.0", "express-session": "^1.11.3", "jsonwebtoken": "^5.0.0", "lodash": "^3.10.1", "lusca": "^1.3.0", "method-override": "^2.3.5", "mongoose": "^4.1.2", "morgan": "~1.6.1", "passport": "~0.3.0", "passport-facebook": "^2.0.0", "passport-google-oauth": "~0.2.0", "passport-local": "^1.0.0", "passport-twitter": "^1.0.3", "serve-favicon": "^2.3.0", "socket.io": "^1.3.5", "socket.io-client": "^1.3.5", "socketio-jwt": "^4.2.0", "tiny-segmenter": "r7kamura/tiny-segmenter" }, "devDependencies": { "autoprefixer": "^6.0.0", "babel-core": "^5.6.4", "grunt": "~0.4.5", "grunt-wiredep": "^2.0.0", "grunt-concurrent": "^2.0.1", "grunt-contrib-clean": "~0.7.0", "grunt-contrib-concat": "^0.5.1", "grunt-contrib-copy": "^0.8.0", "grunt-contrib-cssmin": "~0.14.0", "grunt-contrib-imagemin": "~1.0.0", "grunt-contrib-jshint": "~0.11.2", "grunt-contrib-uglify": "~0.11.0", "grunt-contrib-watch": "~0.6.1", "grunt-babel": "~5.0.0", "grunt-google-cdn": "~0.4.0", "grunt-jscs": "^2.1.0", "grunt-newer": "^1.1.1", "grunt-ng-annotate": "^1.0.1", "grunt-ng-constant": "^1.1.0", "grunt-filerev": "^2.3.1", "grunt-usemin": "^3.0.0", "grunt-env": "~0.4.1", "grunt-node-inspector": "^0.4.1", "grunt-nodemon": "^0.4.0", "grunt-angular-templates": "^0.5.4", "grunt-dom-munger": "^3.4.0", "grunt-protractor-runner": "^2.0.0", "grunt-injector": "^0.6.0", "grunt-karma": "~0.12.0", "grunt-build-control": "^0.6.0", "grunt-contrib-sass": "^0.9.0", "jit-grunt": "^0.9.1", "grunt-express-server": "^0.5.1", "grunt-postcss": "~0.7.1", "grunt-open": "~0.2.3", "time-grunt": "^1.2.1", "grunt-mocha-test": "~0.12.7", "grunt-mocha-istanbul": "^3.0.1", "open": "~0.0.4", "jshint-stylish": "~2.1.0", "connect-livereload": "^0.5.3", "istanbul": "~0.4.1", "chai": "^3.2.0", "sinon": "^1.16.1", "chai-as-promised": "^5.1.0", "chai-things": "^0.2.0", "karma": "~0.13.3", "karma-ng-scenario": "~0.1.0", "karma-firefox-launcher": "~0.1.6", "karma-script-launcher": "~0.1.0", "karma-chrome-launcher": "~0.2.0", "karma-requirejs": "~0.2.2", "karma-jade-preprocessor": "0.0.11", "karma-phantomjs-launcher": "~0.2.0", "karma-ng-html2js-preprocessor": "~0.2.0", "karma-spec-reporter": "~0.0.20", "sinon-chai": "^2.8.0", "mocha": "^2.2.5", "jasmine-core": "^2.3.4", "karma-jasmine": "~0.3.0", "jasmine-spec-reporter": "^2.4.0", "karma-babel-preprocessor": "^5.2.1", "requirejs": "~2.1.11", "phantomjs": "^1.9.18", "proxyquire": "^1.0.1", "supertest": "^1.1.0" }, "engines": { "node": "^4.2.3", "npm": "^2.14.7" }, "scripts": { "start": "node server", "test": "grunt test", "update-webdriver": "node node_modules/grunt-protractor-runner/node_modules/protractor/bin/webdriver-manager update" }, "private": true } ================================================ FILE: protractor.conf.js ================================================ // Protractor configuration // https://github.com/angular/protractor/blob/master/referenceConf.js 'use strict'; var 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'), // Credientials for Saucelabs sauceUser: process.env.SAUCE_USERNAME, sauceKey: process.env.SAUCE_ACCESS_KEY, // 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', 'name': 'Fullstack E2E', 'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER, 'build': process.env.TRAVIS_BUILD_NUMBER }, // ----- 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: 'jasmine2', // ----- Options to be passed to minijasminenode ----- // // See the full list at https://github.com/jasmine/jasmine-npm jasmineNodeOpts: { defaultTimeoutInterval: 30000, print: function() {} // for jasmine-spec-reporter }, // Prepare environment for tests params: { serverConfig: require('./server/config/environment') }, onPrepare: function() { require('babel-core/register'); var SpecReporter = require('jasmine-spec-reporter'); // add jasmine spec reporter jasmine.getEnv().addReporter(new SpecReporter({displayStacktrace: true})); var serverConfig = config.params.serverConfig; // Setup mongo for tests var mongoose = require('mongoose'); mongoose.connect(serverConfig.mongo.uri, serverConfig.mongo.options); // Connect to database } }; config.params.baseUrl = config.baseUrl; exports.config = config; ================================================ FILE: server/.jshintrc ================================================ { "expr": true, "node": true, "esnext": true, "bitwise": true, "eqeqeq": true, "immed": true, "latedef": "nofunc", "newcap": true, "noarg": true, "undef": true, "smarttabs": true, "asi": true, "debug": true } ================================================ FILE: server/.jshintrc-spec ================================================ { "extends": ".jshintrc", "globals": { "jasmine": true, "describe": true, "it": true, "before": true, "beforeEach": true, "after": true, "afterEach": true, "expect": true, "assert": true, "sinon": true } } ================================================ FILE: server/api/question/index.js ================================================ 'use strict'; var express = require('express'); var controller = require('./question.controller'); var router = express.Router(); var auth = require('../../auth/auth.service'); router.get('/', controller.index); router.get('/:id', controller.show); router.post('/', auth.isAuthenticated(), controller.create); router.put('/:id', auth.isAuthenticated(), controller.update); router.patch('/:id', auth.isAuthenticated(), controller.update); router.delete('/:id', auth.isAuthenticated(), controller.destroy); router.post('/:id/answers', auth.isAuthenticated(), controller.createAnswer); router.put('/:id/answers/:answerId', auth.isAuthenticated(), controller.updateAnswer); router.delete('/:id/answers/:answerId', auth.isAuthenticated(), controller.destroyAnswer); router.post('/:id/comments', auth.isAuthenticated(), controller.createComment); router.put('/:id/comments/:commentId', auth.isAuthenticated(), controller.updateComment); router.delete('/:id/comments/:commentId', auth.isAuthenticated(), controller.destroyComment); router.post('/:id/answers/:answerId/comments', auth.isAuthenticated(), controller.createAnswerComment); router.put('/:id/answers/:answerId/comments/:commentId', auth.isAuthenticated(), controller.updateAnswerComment); router.delete('/:id/answers/:answerId/comments/:commentId', auth.isAuthenticated(), controller.destroyAnswerComment); router.put('/:id/star', auth.isAuthenticated(), controller.star); router.delete('/:id/star', auth.isAuthenticated(), controller.unstar); router.put('/:id/answers/:answerId/star', auth.isAuthenticated(), controller.starAnswer); router.delete('/:id/answers/:answerId/star', auth.isAuthenticated(), controller.unstarAnswer); router.put('/:id/comments/:commentId/star', auth.isAuthenticated(), controller.starComment); router.delete('/:id/comments/:commentId/star', auth.isAuthenticated(), controller.unstarComment); router.put('/:id/answers/:answerId/comments/:commentId/star', auth.isAuthenticated(), controller.starAnswerComment); router.delete('/:id/answers/:answerId/comments/:commentId/star', auth.isAuthenticated(), controller.unstarAnswerComment); module.exports = router; ================================================ FILE: server/api/question/question.controller.js ================================================ /** * Using Rails-like standard naming convention for endpoints. * GET /api/questions -> index * POST /api/questions -> create * GET /api/questions/:id -> show * PUT /api/questions/:id -> update * DELETE /api/questions/:id -> destroy */ 'use strict'; import _ from 'lodash'; import Question from './question.model'; function respondWithResult(res, statusCode) { statusCode = statusCode || 200; return function(entity) { if (entity) { res.status(statusCode).json(entity); } }; } function saveUpdates(updates) { return function(entity) { var updated = _.merge(entity, updates); return updated.saveAsync() .spread(updated => { return updated; }); }; } function removeEntity(res) { return function(entity) { if (entity) { return entity.removeAsync() .then(() => { res.status(204).end(); }); } }; } function handleEntityNotFound(res) { return function(entity) { if (!entity) { res.status(404).end(); return null; } return entity; }; } function handleError(res, statusCode) { statusCode = statusCode || 500; return function(err) { res.status(statusCode).send(err); }; } function handleUnauthorized(req, res) { return function(entity) { if (!entity) {return null;} if(entity.user._id.toString() !== req.user._id.toString()){ res.send(403).end(); return null; } return entity; } } // Gets a list of Questions export function index(req, res) { var query = req.query.query && JSON.parse(req.query.query); Question.find(query).sort({createdAt: -1}).limit(20).execAsync() .then(respondWithResult(res)) .catch(handleError(res)); } // Gets a single Question from the DB export function show(req, res) { Question.findByIdAsync(req.params.id) .then(handleEntityNotFound(res)) .then(respondWithResult(res)) .catch(handleError(res)); } // Creates a new Question in the DB export function create(req, res) { req.body.user = req.user; Question.createAsync(req.body) .then(respondWithResult(res, 201)) .catch(handleError(res)); } // Updates an existing Question in the DB export function update(req, res) { if (req.body._id) { delete req.body._id; } Question.findByIdAsync(req.params.id) .then(handleEntityNotFound(res)) .then(handleUnauthorized(req, res)) .then(saveUpdates(req.body)) .then(respondWithResult(res)) .catch(handleError(res)); } // Deletes a Question from the DB export function destroy(req, res) { Question.findByIdAsync(req.params.id) .then(handleEntityNotFound(res)) .then(handleUnauthorized(req, res)) .then(removeEntity(res)) .catch(handleError(res)); } export function createAnswer(req, res) { req.body.user = req.user; Question.update({_id: req.params.id}, {$push: {answers: req.body}}, function(err, num) { if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); }); } export function destroyAnswer(req, res) { Question.update({_id: req.params.id}, {$pull: {answers: {_id: req.params.answerId , 'user': req.user._id}}}, function(err, num) { if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); }); } export function updateAnswer(req, res) { Question.update({_id: req.params.id, 'answers._id': req.params.answerId}, {'answers.$.content': req.body.content, 'answers.$.user': req.user.id}, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); }); } /* comments APIs */ export function createComment(req, res) { req.body.user = req.user.id; Question.update({_id: req.params.id}, {$push: {comments: req.body}}, function(err, num){ if(err) {return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); }) } export function destroyComment(req, res) { Question.update({_id: req.params.id}, {$pull: {comments: {_id: req.params.commentId , 'user': req.user._id}}}, function(err, num) { if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); }); } export function updateComment(req, res) { Question.update({_id: req.params.id, 'comments._id': req.params.commentId}, {'comments.$.content': req.body.content, 'comments.$.user': req.user.id}, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); }); } /* answersComments APIs */ export function createAnswerComment(req, res) { req.body.user = req.user.id; Question.update({_id: req.params.id, 'answers._id': req.params.answerId}, {$push: {'answers.$.comments': req.body}}, function(err, num){ if(err) {return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); }) } export function destroyAnswerComment(req, res) { Question.update({_id: req.params.id, 'answers._id': req.params.answerId}, {$pull: {'answers.$.comments': {_id: req.params.commentId , 'user': req.user._id}}}, function(err, num) { if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); }); } export function updateAnswerComment(req, res) { Question.find({_id: req.params.id}).exec(function(err, questions){ if(err) { return handleError(res)(err); } if(questions.length === 0) { return res.send(404).end(); } var question = questions[0]; var found = false; for(var i=0; i < question.answers.length; i++){ if(question.answers[i]._id.toString() === req.params.answerId){ found = true; var conditions = {}; conditions._id = req.params.id; conditions['answers.' + i + '.comments._id'] = req.params.commentId; conditions['answers.' + i + '.comments.user'] = req.user._id; var doc = {}; doc['answers.' + i + '.comments.$.content'] = req.body.content; /*jshint -W083 */ Question.update(conditions, doc, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); Question.updateSearchText(req.params.id); return; }); } } if(!found){ return res.send(404).end(); } }); } /* star/unstar question */ export function star(req, res) { Question.update({_id: req.params.id}, {$push: {stars: req.user.id}}, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); }); } export function unstar(req, res) { Question.update({_id: req.params.id}, {$pull: {stars: req.user.id}}, function(err, num){ if(err) { return handleError(res, err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); }); } /* star/unstar answer */ export function starAnswer(req, res) { Question.update({_id: req.params.id, 'answers._id': req.params.answerId}, {$push: {'answers.$.stars': req.user.id}}, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); }); } export function unstarAnswer(req, res) { Question.update({_id: req.params.id, 'answers._id': req.params.answerId}, {$pull: {'answers.$.stars': req.user.id}}, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); }); } /* star/unstar question comment */ export function starComment(req, res) { Question.update({_id: req.params.id, 'comments._id': req.params.commentId}, {$push: {'comments.$.stars': req.user.id}}, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); }); } export function unstarComment(req, res) { Question.update({_id: req.params.id, 'comments._id': req.params.commentId}, {$pull: {'comments.$.stars': req.user.id}}, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); }); } /* star/unstar question answer comment */ var pushOrPullStarAnswerComment = function(op, req, res) { Question.find({_id: req.params.id}).exec(function(err, questions){ if(err) { return handleError(res)(err); } if(questions.length === 0) { return res.send(404).end(); } var question = questions[0]; var found = false; for(var i=0; i < question.answers.length; i++){ if(question.answers[i]._id.toString() === req.params.answerId){ found = true; var conditions = {}; conditions._id = req.params.id; conditions['answers.' + i + '.comments._id'] = req.params.commentId; var doc = {}; doc[op] = {}; doc[op]['answers.' + i + '.comments.$.stars'] = req.user.id; // Question.update({_id: req.params.id, 'answers.' + i + '.comments._id': req.params.commentId}, {op: {('answers.' + i + '.comments.$.stars'): req.user.id}}, function(err, num){ /*jshint -W083 */ Question.update(conditions, doc, function(err, num){ if(err) { return handleError(res)(err); } if(num === 0) { return res.send(404).end(); } exports.show(req, res); return; }); } } if(!found){ return res.send(404).end(); } }); }; export function starAnswerComment(req, res) { pushOrPullStarAnswerComment('$push', req, res); } export function unstarAnswerComment(req, res) { pushOrPullStarAnswerComment('$pull', req, res); } ================================================ FILE: server/api/question/question.events.js ================================================ /** * Question model events */ 'use strict'; import {EventEmitter} from 'events'; var Question = require('./question.model'); var QuestionEvents = new EventEmitter(); // Set max event listeners (0 == unlimited) QuestionEvents.setMaxListeners(0); // Model events var events = { 'save': 'save', 'remove': 'remove' }; // Register the event emitter to the model events for (var e in events) { var event = events[e]; Question.schema.post(e, emitEvent(event)); } function emitEvent(event) { return function(doc) { QuestionEvents.emit(event + ':' + doc._id, doc); QuestionEvents.emit(event, doc); } } export default QuestionEvents; ================================================ FILE: server/api/question/question.integration.js ================================================ 'use strict'; var app = require('../..'); import request from 'supertest'; var User = require('../user/user.model'); var newQuestion; describe('Question API:', function() { var user; before(function() { return User.removeAsync().then(function() { user = new User({ name: 'Fake User', email: 'test@test.com', password: 'password' }); return user.saveAsync(); }); }); var token; before(function(done) { request(app) .post('/auth/local') .send({ email: 'test@test.com', password: 'password' }) .expect(200) .expect('Content-Type', /json/) .end(function(err, res) { token = res.body.token; done(); }); }); describe('GET /api/questions', function() { var questions; beforeEach(function(done) { request(app) .get('/api/questions') .expect(200) .expect('Content-Type', /json/) .end((err, res) => { if (err) { return done(err); } questions = res.body; done(); }); }); it('should respond with JSON array', function() { questions.should.be.instanceOf(Array); }); }); describe('POST /api/questions', function() { beforeEach(function(done) { request(app) .post('/api/questions') .set('authorization', 'Bearer ' + token) .send({ title: 'New Question', content: 'This is the brand new question!!!' }) .expect(201) .expect('Content-Type', /json/) .end((err, res) => { if (err) { return done(err); } newQuestion = res.body; console.warn("newQuestion:test1",newQuestion); done(); }); }); it('should respond with the newly created question', function() { console.warn("newQuestion:test2",newQuestion); newQuestion.title.should.equal('New Question'); newQuestion.content.should.equal('This is the brand new question!!!'); }); }); describe('GET /api/questions/:id', function() { var question; beforeEach(function(done) { request(app) .get('/api/questions/' + newQuestion._id) .expect(200) .expect('Content-Type', /json/) .end((err, res) => { if (err) { return done(err); } question = res.body; done(); }); }); afterEach(function() { question = {}; }); it('should respond with the requested question', function() { question.title.should.equal('New Question'); question.content.should.equal('This is the brand new question!!!'); }); }); /* describe('PUT /api/questions/:id', function() { var updatedQuestion; beforeEach(function(done) { request(app) .put('/api/questions/' + newQuestion._id) .set('authorization', 'Bearer ' + token) .send({ title: 'Updated Question', content: 'This is the updated question!!!' }) .expect(200) .expect('Content-Type', /json/) .end(function(err, res) { if (err) { return done(err); } updatedQuestion = res.body; done(); }); }); afterEach(function() { updatedQuestion = {}; }); it('should respond with the updated question', function() { updatedQuestion.title.should.equal('Updated Question'); updatedQuestion.content.should.equal('This is the updated question!!!'); }); }); */ describe('DELETE /api/questions/:id', function() { it('should respond with 204 on successful removal', function(done) { request(app) .delete('/api/questions/' + newQuestion._id) .set('authorization', 'Bearer ' + token) .expect(204) .end((err, res) => { if (err) { return done(err); } done(); }); }); it('should respond with 404 when question does not exist', function(done) { request(app) .delete('/api/questions/' + newQuestion._id) .set('authorization', 'Bearer ' + token) .expect(404) .end((err, res) => { if (err) { return done(err); } done(); }); }); }); }); ================================================ FILE: server/api/question/question.model.js ================================================ 'use strict'; var mongoose = require('bluebird').promisifyAll(require('mongoose')); var QuestionSchema = new mongoose.Schema({ title: String, content: String, answers: [{ content: String, user: { type: mongoose.Schema.ObjectId, ref: 'User' }, createdAt: { type: Date, default: Date.now, }, comments: [{ content: String, stars: [{ type: mongoose.Schema.ObjectId, ref: 'User' }], user: { type: mongoose.Schema.ObjectId, ref: 'User' }, createdAt: { type: Date, default: Date.now, } }], stars: [{ type: mongoose.Schema.ObjectId, ref: 'User' }], }], tags: [{ text: String, }], user: { type: mongoose.Schema.ObjectId, ref: 'User' }, createdAt: { type: Date, default: Date.now }, comments: [{ content: String, stars: [{ type: mongoose.Schema.ObjectId, ref: 'User' }], user: { type: mongoose.Schema.ObjectId, ref: 'User' }, createdAt: { type: Date, default: Date.now, } }], stars: [{ type: mongoose.Schema.ObjectId, ref: 'User' }], searchText: String, }); QuestionSchema.pre('find', function(next){ this.populate('user', 'name'); this.populate('comments.user', 'name'); this.populate('answers.user', 'name'); this.populate('answers.comments.user', 'name'); next(); }); QuestionSchema.pre('findOne', function(next){ this.populate('user', 'name'); this.populate('comments.user', 'name'); this.populate('answers.user', 'name'); this.populate('answers.comments.user', 'name'); next(); }); QuestionSchema.index({ 'title': 'text', 'content': 'text', 'tags.text': 'text', 'answers.content': 'text', 'comments.content': 'text', 'answers.comments.content': 'text', 'searchText': 'text', }, {name: 'question_schema_index'}); var TinySegmenter = require('tiny-segmenter'); var getSearchText = function(question){ var tinySegmenter = new TinySegmenter(); var searchText = ""; searchText += tinySegmenter.segment(question.title).join(' ') + " "; searchText += tinySegmenter.segment(question.content).join(' ') + " "; question.answers.forEach(function(answer){ searchText += tinySegmenter.segment(answer.content).join(' ') + " "; answer.comments.forEach(function(comment){ searchText += tinySegmenter.segment(comment.content).join(' ') + " "; }); }); question.comments.forEach(function(comment){ searchText += tinySegmenter.segment(comment.content).join(' ') + " "; }); console.log("searchText", searchText); return searchText; }; QuestionSchema.statics.updateSearchText = function(id, cb){ this.findOne({_id: id}).exec(function(err, question){ if(err){ if(cb){cb(err);} return; } var searchText = getSearchText(question); this.update({_id: id}, {searchText: searchText}, function(err, num){ if(cb){cb(err);} }); }.bind(this)); }; QuestionSchema.pre('save', function(next){ this.searchText = getSearchText(this); next(); }); export default mongoose.model('Question', QuestionSchema); ================================================ FILE: server/api/question/question.socket.js ================================================ /** * Broadcast updates to client when the model changes */ 'use strict'; var QuestionEvents = require('./question.events'); // Model events to emit var events = ['save', 'remove']; export function register(socket) { // Bind model events to socket events for (var i = 0, eventsLength = events.length; i < eventsLength; i++) { var event = events[i]; var listener = createListener('question:' + event, socket); QuestionEvents.on(event, listener); socket.on('disconnect', removeListener(event, listener)); } } function createListener(event, socket) { return function(doc) { socket.emit(event, doc); }; } function removeListener(event, listener) { return function() { QuestionEvents.removeListener(event, listener); }; } ================================================ FILE: server/api/thing/index.js ================================================ 'use strict'; var express = require('express'); var controller = require('./thing.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; ================================================ FILE: server/api/thing/index.spec.js ================================================ 'use strict'; var proxyquire = require('proxyquire').noPreserveCache(); var thingCtrlStub = { index: 'thingCtrl.index', show: 'thingCtrl.show', create: 'thingCtrl.create', update: 'thingCtrl.update', destroy: 'thingCtrl.destroy' }; var routerStub = { get: sinon.spy(), put: sinon.spy(), patch: sinon.spy(), post: sinon.spy(), delete: sinon.spy() }; // require the index with our stubbed out modules var thingIndex = proxyquire('./index.js', { 'express': { Router: function() { return routerStub; } }, './thing.controller': thingCtrlStub }); describe('Thing API Router:', function() { it('should return an express router instance', function() { thingIndex.should.equal(routerStub); }); describe('GET /api/things', function() { it('should route to thing.controller.index', function() { routerStub.get .withArgs('/', 'thingCtrl.index') .should.have.been.calledOnce; }); }); describe('GET /api/things/:id', function() { it('should route to thing.controller.show', function() { routerStub.get .withArgs('/:id', 'thingCtrl.show') .should.have.been.calledOnce; }); }); describe('POST /api/things', function() { it('should route to thing.controller.create', function() { routerStub.post .withArgs('/', 'thingCtrl.create') .should.have.been.calledOnce; }); }); describe('PUT /api/things/:id', function() { it('should route to thing.controller.update', function() { routerStub.put .withArgs('/:id', 'thingCtrl.update') .should.have.been.calledOnce; }); }); describe('PATCH /api/things/:id', function() { it('should route to thing.controller.update', function() { routerStub.patch .withArgs('/:id', 'thingCtrl.update') .should.have.been.calledOnce; }); }); describe('DELETE /api/things/:id', function() { it('should route to thing.controller.destroy', function() { routerStub.delete .withArgs('/:id', 'thingCtrl.destroy') .should.have.been.calledOnce; }); }); }); ================================================ FILE: server/api/thing/thing.controller.js ================================================ /** * Using Rails-like standard naming convention for endpoints. * GET /api/things -> index * POST /api/things -> create * GET /api/things/:id -> show * PUT /api/things/:id -> update * DELETE /api/things/:id -> destroy */ 'use strict'; import _ from 'lodash'; import Thing from './thing.model'; function respondWithResult(res, statusCode) { statusCode = statusCode || 200; return function(entity) { if (entity) { res.status(statusCode).json(entity); } }; } function saveUpdates(updates) { return function(entity) { var updated = _.merge(entity, updates); return updated.saveAsync() .spread(updated => { return updated; }); }; } function removeEntity(res) { return function(entity) { if (entity) { return entity.removeAsync() .then(() => { res.status(204).end(); }); } }; } function handleEntityNotFound(res) { return function(entity) { if (!entity) { res.status(404).end(); return null; } return entity; }; } function handleError(res, statusCode) { statusCode = statusCode || 500; return function(err) { res.status(statusCode).send(err); }; } // Gets a list of Things export function index(req, res) { Thing.findAsync() .then(respondWithResult(res)) .catch(handleError(res)); } // Gets a single Thing from the DB export function show(req, res) { Thing.findByIdAsync(req.params.id) .then(handleEntityNotFound(res)) .then(respondWithResult(res)) .catch(handleError(res)); } // Creates a new Thing in the DB export function create(req, res) { Thing.createAsync(req.body) .then(respondWithResult(res, 201)) .catch(handleError(res)); } // Updates an existing Thing in the DB export function update(req, res) { if (req.body._id) { delete req.body._id; } Thing.findByIdAsync(req.params.id) .then(handleEntityNotFound(res)) .then(saveUpdates(req.body)) .then(respondWithResult(res)) .catch(handleError(res)); } // Deletes a Thing from the DB export function destroy(req, res) { Thing.findByIdAsync(req.params.id) .then(handleEntityNotFound(res)) .then(removeEntity(res)) .catch(handleError(res)); } ================================================ FILE: server/api/thing/thing.events.js ================================================ /** * Thing model events */ 'use strict'; import {EventEmitter} from 'events'; var Thing = require('./thing.model'); var ThingEvents = new EventEmitter(); // Set max event listeners (0 == unlimited) ThingEvents.setMaxListeners(0); // Model events var events = { 'save': 'save', 'remove': 'remove' }; // Register the event emitter to the model events for (var e in events) { var event = events[e]; Thing.schema.post(e, emitEvent(event)); } function emitEvent(event) { return function(doc) { ThingEvents.emit(event + ':' + doc._id, doc); ThingEvents.emit(event, doc); } } export default ThingEvents; ================================================ FILE: server/api/thing/thing.integration.js ================================================ 'use strict'; var app = require('../..'); import request from 'supertest'; var newThing; describe('Thing API:', function() { describe('GET /api/things', function() { var things; beforeEach(function(done) { request(app) .get('/api/things') .expect(200) .expect('Content-Type', /json/) .end((err, res) => { if (err) { return done(err); } things = res.body; done(); }); }); it('should respond with JSON array', function() { things.should.be.instanceOf(Array); }); }); describe('POST /api/things', function() { beforeEach(function(done) { request(app) .post('/api/things') .send({ name: 'New Thing', info: 'This is the brand new thing!!!' }) .expect(201) .expect('Content-Type', /json/) .end((err, res) => { if (err) { return done(err); } newThing = res.body; done(); }); }); it('should respond with the newly created thing', function() { newThing.name.should.equal('New Thing'); newThing.info.should.equal('This is the brand new thing!!!'); }); }); describe('GET /api/things/:id', function() { var thing; beforeEach(function(done) { request(app) .get('/api/things/' + newThing._id) .expect(200) .expect('Content-Type', /json/) .end((err, res) => { if (err) { return done(err); } thing = res.body; done(); }); }); afterEach(function() { thing = {}; }); it('should respond with the requested thing', function() { thing.name.should.equal('New Thing'); thing.info.should.equal('This is the brand new thing!!!'); }); }); describe('PUT /api/things/:id', function() { var updatedThing; beforeEach(function(done) { request(app) .put('/api/things/' + newThing._id) .send({ name: 'Updated Thing', info: 'This is the updated thing!!!' }) .expect(200) .expect('Content-Type', /json/) .end(function(err, res) { if (err) { return done(err); } updatedThing = res.body; done(); }); }); afterEach(function() { updatedThing = {}; }); it('should respond with the updated thing', function() { updatedThing.name.should.equal('Updated Thing'); updatedThing.info.should.equal('This is the updated thing!!!'); }); }); describe('DELETE /api/things/:id', function() { it('should respond with 204 on successful removal', function(done) { request(app) .delete('/api/things/' + newThing._id) .expect(204) .end((err, res) => { if (err) { return done(err); } done(); }); }); it('should respond with 404 when thing does not exist', function(done) { request(app) .delete('/api/things/' + newThing._id) .expect(404) .end((err, res) => { if (err) { return done(err); } done(); }); }); }); }); ================================================ FILE: server/api/thing/thing.model.js ================================================ 'use strict'; var mongoose = require('bluebird').promisifyAll(require('mongoose')); var ThingSchema = new mongoose.Schema({ name: String, info: String, active: Boolean }); export default mongoose.model('Thing', ThingSchema); ================================================ FILE: server/api/thing/thing.socket.js ================================================ /** * Broadcast updates to client when the model changes */ 'use strict'; var ThingEvents = require('./thing.events'); // Model events to emit var events = ['save', 'remove']; export function register(socket) { // Bind model events to socket events for (var i = 0, eventsLength = events.length; i < eventsLength; i++) { var event = events[i]; var listener = createListener('thing:' + event, socket); ThingEvents.on(event, listener); socket.on('disconnect', removeListener(event, listener)); } } function createListener(event, socket) { return function(doc) { socket.emit(event, doc); }; } function removeListener(event, listener) { return function() { ThingEvents.removeListener(event, listener); }; } ================================================ FILE: server/api/user/index.js ================================================ 'use strict'; import {Router} from 'express'; import * as controller from './user.controller'; import * as auth from '../../auth/auth.service'; var router = new 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); export default router; ================================================ FILE: server/api/user/index.spec.js ================================================ 'use strict'; var proxyquire = require('proxyquire').noPreserveCache(); var userCtrlStub = { index: 'userCtrl.index', destroy: 'userCtrl.destroy', me: 'userCtrl.me', changePassword: 'userCtrl.changePassword', show: 'userCtrl.show', create: 'userCtrl.create' }; var authServiceStub = { isAuthenticated() { return 'authService.isAuthenticated'; }, hasRole(role) { return 'authService.hasRole.' + role; } }; var routerStub = { get: sinon.spy(), put: sinon.spy(), post: sinon.spy(), delete: sinon.spy() }; // require the index with our stubbed out modules var userIndex = proxyquire('./index', { 'express': { Router() { return routerStub; } }, './user.controller': userCtrlStub, '../../auth/auth.service': authServiceStub }); describe('User API Router:', function() { it('should return an express router instance', function() { userIndex.should.equal(routerStub); }); describe('GET /api/users', function() { it('should verify admin role and route to user.controller.index', function() { routerStub.get .withArgs('/', 'authService.hasRole.admin', 'userCtrl.index') .should.have.been.calledOnce; }); }); describe('DELETE /api/users/:id', function() { it('should verify admin role and route to user.controller.destroy', function() { routerStub.delete .withArgs('/:id', 'authService.hasRole.admin', 'userCtrl.destroy') .should.have.been.calledOnce; }); }); describe('GET /api/users/me', function() { it('should be authenticated and route to user.controller.me', function() { routerStub.get .withArgs('/me', 'authService.isAuthenticated', 'userCtrl.me') .should.have.been.calledOnce; }); }); describe('PUT /api/users/:id/password', function() { it('should be authenticated and route to user.controller.changePassword', function() { routerStub.put .withArgs('/:id/password', 'authService.isAuthenticated', 'userCtrl.changePassword') .should.have.been.calledOnce; }); }); describe('GET /api/users/:id', function() { it('should be authenticated and route to user.controller.show', function() { routerStub.get .withArgs('/:id', 'authService.isAuthenticated', 'userCtrl.show') .should.have.been.calledOnce; }); }); describe('POST /api/users', function() { it('should route to user.controller.create', function() { routerStub.post .withArgs('/', 'userCtrl.create') .should.have.been.calledOnce; }); }); }); ================================================ FILE: server/api/user/user.controller.js ================================================ 'use strict'; import User from './user.model'; import passport from 'passport'; import config from '../../config/environment'; import jwt from 'jsonwebtoken'; function validationError(res, statusCode) { statusCode = statusCode || 422; return function(err) { res.status(statusCode).json(err); } } function handleError(res, statusCode) { statusCode = statusCode || 500; return function(err) { res.status(statusCode).send(err); }; } /** * Get list of users * restriction: 'admin' */ export function index(req, res) { User.findAsync({}, '-salt -password') .then(users => { res.status(200).json(users); }) .catch(handleError(res)); } /** * Creates a new user */ export function create(req, res, next) { var newUser = new User(req.body); newUser.provider = 'local'; newUser.role = 'user'; newUser.saveAsync() .spread(function(user) { var token = jwt.sign({ _id: user._id }, config.secrets.session, { expiresIn: 60 * 60 * 5 }); res.json({ token }); }) .catch(validationError(res)); } /** * Get a single user */ export function show(req, res, next) { var userId = req.params.id; User.findByIdAsync(userId) .then(user => { if (!user) { return res.status(404).end(); } res.json(user.profile); }) .catch(err => next(err)); } /** * Deletes a user * restriction: 'admin' */ export function destroy(req, res) { User.findByIdAndRemoveAsync(req.params.id) .then(function() { res.status(204).end(); }) .catch(handleError(res)); } /** * Change a users password */ export function changePassword(req, res, next) { var userId = req.user._id; var oldPass = String(req.body.oldPassword); var newPass = String(req.body.newPassword); User.findByIdAsync(userId) .then(user => { if (user.authenticate(oldPass)) { user.password = newPass; return user.saveAsync() .then(() => { res.status(204).end(); }) .catch(validationError(res)); } else { return res.status(403).end(); } }); } /** * Get my info */ export function me(req, res, next) { var userId = req.user._id; User.findOneAsync({ _id: userId }, '-salt -password') .then(user => { // don't ever give out the password or salt if (!user) { return res.status(401).end(); } res.json(user); }) .catch(err => next(err)); } /** * Authentication callback */ export function authCallback(req, res, next) { res.redirect('/'); } ================================================ FILE: server/api/user/user.events.js ================================================ /** * User model events */ 'use strict'; import {EventEmitter} from 'events'; import User from './user.model'; var UserEvents = new EventEmitter(); // Set max event listeners (0 == unlimited) UserEvents.setMaxListeners(0); // Model events var events = { 'save': 'save', 'remove': 'remove' }; // Register the event emitter to the model events for (var e in events) { var event = events[e]; User.schema.post(e, emitEvent(event)); } function emitEvent(event) { return function(doc) { UserEvents.emit(event + ':' + doc._id, doc); UserEvents.emit(event, doc); } } export default UserEvents; ================================================ FILE: server/api/user/user.integration.js ================================================ 'use strict'; import app from '../..'; import User from './user.model'; import request from 'supertest'; describe('User API:', function() { var user; // Clear users before testing before(function() { return User.removeAsync().then(function() { user = new User({ name: 'Fake User', email: 'test@example.com', password: 'password' }); return user.saveAsync(); }); }); // Clear users after testing after(function() { return User.removeAsync(); }); describe('GET /api/users/me', function() { var token; before(function(done) { request(app) .post('/auth/local') .send({ email: 'test@example.com', password: 'password' }) .expect(200) .expect('Content-Type', /json/) .end((err, res) => { token = res.body.token; done(); }); }); it('should respond with a user profile when authenticated', function(done) { request(app) .get('/api/users/me') .set('authorization', 'Bearer ' + token) .expect(200) .expect('Content-Type', /json/) .end((err, res) => { res.body._id.toString().should.equal(user._id.toString()); done(); }); }); it('should respond with a 401 when not authenticated', function(done) { request(app) .get('/api/users/me') .expect(401) .end(done); }); }); }); ================================================ FILE: server/api/user/user.model.js ================================================ 'use strict'; import crypto from 'crypto'; var mongoose = require('bluebird').promisifyAll(require('mongoose')); import {Schema} from 'mongoose'; const authTypes = ['github', 'twitter', 'facebook', 'google']; var UserSchema = new Schema({ name: String, email: { type: String, lowercase: true }, role: { type: String, default: 'user' }, password: String, provider: String, salt: String, facebook: {}, twitter: {}, google: {}, github: {} }); /** * Virtuals */ // 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) { if (authTypes.indexOf(this.provider) !== -1) { return true; } return email.length; }, 'Email cannot be blank'); // Validate empty password UserSchema .path('password') .validate(function(password) { if (authTypes.indexOf(this.provider) !== -1) { return true; } return password.length; }, 'Password cannot be blank'); // Validate email is not taken UserSchema .path('email') .validate(function(value, respond) { var self = this; return this.constructor.findOneAsync({ email: value }) .then(function(user) { if (user) { if (self.id === user.id) { return respond(true); } return respond(false); } return respond(true); }) .catch(function(err) { throw err; }); }, 'The specified email address is already in use.'); var validatePresenceOf = function(value) { return value && value.length; }; /** * Pre-save hook */ UserSchema .pre('save', function(next) { // Handle new/update passwords if (!this.isModified('password')) { return next(); } if (!validatePresenceOf(this.password) && authTypes.indexOf(this.provider) === -1) { next(new Error('Invalid password')); } // Make salt with a callback this.makeSalt((saltErr, salt) => { if (saltErr) { next(saltErr); } this.salt = salt; this.encryptPassword(this.password, (encryptErr, hashedPassword) => { if (encryptErr) { next(encryptErr); } this.password = hashedPassword; next(); }); }); }); /** * Methods */ UserSchema.methods = { /** * Authenticate - check if the passwords are the same * * @param {String} password * @param {Function} callback * @return {Boolean} * @api public */ authenticate(password, callback) { if (!callback) { return this.password === this.encryptPassword(password); } this.encryptPassword(password, (err, pwdGen) => { if (err) { return callback(err); } if (this.password === pwdGen) { callback(null, true); } else { callback(null, false); } }); }, /** * Make salt * * @param {Number} byteSize Optional salt byte size, default to 16 * @param {Function} callback * @return {String} * @api public */ makeSalt(byteSize, callback) { var defaultByteSize = 16; if (typeof arguments[0] === 'function') { callback = arguments[0]; byteSize = defaultByteSize; } else if (typeof arguments[1] === 'function') { callback = arguments[1]; } if (!byteSize) { byteSize = defaultByteSize; } if (!callback) { return crypto.randomBytes(byteSize).toString('base64'); } return crypto.randomBytes(byteSize, (err, salt) => { if (err) { callback(err); } else { callback(null, salt.toString('base64')); } }); }, /** * Encrypt password * * @param {String} password * @param {Function} callback * @return {String} * @api public */ encryptPassword(password, callback) { if (!password || !this.salt) { return null; } var defaultIterations = 10000; var defaultKeyLength = 64; var salt = new Buffer(this.salt, 'base64'); if (!callback) { return crypto.pbkdf2Sync(password, salt, defaultIterations, defaultKeyLength) .toString('base64'); } return crypto.pbkdf2(password, salt, defaultIterations, defaultKeyLength, (err, key) => { if (err) { callback(err); } else { callback(null, key.toString('base64')); } }); } }; export default mongoose.model('User', UserSchema); ================================================ FILE: server/api/user/user.model.spec.js ================================================ 'use strict'; import app from '../..'; import User from './user.model'; var user; var genUser = function() { user = new User({ provider: 'local', name: 'Fake User', email: 'test@example.com', password: 'password' }); return user; }; describe('User Model', function() { before(function() { // Clear users before testing return User.removeAsync(); }); beforeEach(function() { genUser(); }); afterEach(function() { return User.removeAsync(); }); it('should begin with no users', function() { return User.findAsync({}).should .eventually.have.length(0); }); it('should fail when saving a duplicate user', function() { return user.saveAsync() .then(function() { var userDup = genUser(); return userDup.saveAsync(); }).should.be.rejected; }); describe('#email', function() { it('should fail when saving without an email', function() { user.email = ''; return user.saveAsync().should.be.rejected; }); }); describe('#password', function() { beforeEach(function() { return user.saveAsync(); }); it('should authenticate user if valid', function() { user.authenticate('password').should.be.true; }); it('should not authenticate user if invalid', function() { user.authenticate('blah').should.not.be.true; }); it('should remain the same hash unless the password is updated', function() { user.name = 'Test User'; return user.saveAsync() .spread(function(u) { return u.authenticate('password'); }).should.eventually.be.true; }); }); }); ================================================ FILE: server/app.js ================================================ /** * Main application file */ 'use strict'; import express from 'express'; import mongoose from 'mongoose'; mongoose.Promise = require('bluebird'); import config from './config/environment'; import http from 'http'; // Connect to MongoDB mongoose.connect(config.mongo.uri, config.mongo.options); mongoose.connection.on('error', function(err) { console.error('MongoDB connection error: ' + err); process.exit(-1); }); // Populate databases with sample data if (config.seedDB) { require('./config/seed'); } // Setup server var app = express(); var server = http.createServer(app); var socketio = require('socket.io')(server, { serveClient: config.env !== 'production', path: '/socket.io-client' }); require('./config/socketio')(socketio); require('./config/express')(app); require('./routes')(app); // Start server function startServer() { app.angularFullstack = server.listen(config.port, config.ip, function() { console.log('Express server listening on %d, in %s mode', config.port, app.get('env')); }); } setImmediate(startServer); // Expose app exports = module.exports = app; ================================================ FILE: server/auth/auth.service.js ================================================ 'use strict'; import passport from 'passport'; import config from '../config/environment'; import jwt from 'jsonwebtoken'; import expressJwt from 'express-jwt'; import compose from 'composable-middleware'; import User from '../api/user/user.model'; var validateJwt = expressJwt({ secret: config.secrets.session }); /** * Attaches the user object to the request if authenticated * Otherwise returns 403 */ export 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.findByIdAsync(req.user._id) .then(user => { if (!user) { return res.status(401).end(); } req.user = user; next(); }) .catch(err => next(err)); }); } /** * Checks if the user role meets the minimum requirements of the route */ export 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.status(403).send('Forbidden'); } }); } /** * Returns a jwt token signed by the app secret */ export function signToken(id, role) { return jwt.sign({ _id: id, role: role }, config.secrets.session, { expiresIn: 60 * 60 * 5 }); } /** * Set token cookie directly for oAuth strategies */ export function setTokenCookie(req, res) { if (!req.user) { return res.status(404).send('It looks like you aren\'t logged in, please try again.'); } var token = signToken(req.user._id, req.user.role); res.cookie('token', token); res.redirect('/'); } ================================================ FILE: server/auth/facebook/index.js ================================================ 'use strict'; import express from 'express'; import passport from 'passport'; import {setTokenCookie} from '../auth.service'; var router = express.Router(); router .get('/', passport.authenticate('facebook', { scope: ['email', 'user_about_me'], failureRedirect: '/signup', session: false })) .get('/callback', passport.authenticate('facebook', { failureRedirect: '/signup', session: false }), setTokenCookie); export default router; ================================================ FILE: server/auth/facebook/passport.js ================================================ import passport from 'passport'; import {Strategy as FacebookStrategy} from 'passport-facebook'; export function setup(User, config) { passport.use(new FacebookStrategy({ clientID: config.facebook.clientID, clientSecret: config.facebook.clientSecret, callbackURL: config.facebook.callbackURL, profileFields: [ 'displayName', 'emails' ] }, function(accessToken, refreshToken, profile, done) { User.findOneAsync({ 'facebook.id': profile.id }) .then(user => { if (user) { return done(null, user); } user = new User({ name: profile.displayName, email: profile.emails[0].value, role: 'user', provider: 'facebook', facebook: profile._json }); user.save() .then(user => done(null, user)) .catch(err => done(err)); }) .catch(err => done(err)); })); } ================================================ FILE: server/auth/google/index.js ================================================ 'use strict'; import express from 'express'; import passport from 'passport'; import {setTokenCookie} from '../auth.service'; var router = express.Router(); router .get('/', passport.authenticate('google', { failureRedirect: '/signup', scope: [ 'profile', 'email' ], session: false })) .get('/callback', passport.authenticate('google', { failureRedirect: '/signup', session: false }), setTokenCookie); export default router; ================================================ FILE: server/auth/google/passport.js ================================================ import passport from 'passport'; import {OAuth2Strategy as GoogleStrategy} from 'passport-google-oauth'; export function setup(User, config) { passport.use(new GoogleStrategy({ clientID: config.google.clientID, clientSecret: config.google.clientSecret, callbackURL: config.google.callbackURL }, function(accessToken, refreshToken, profile, done) { User.findOneAsync({ 'google.id': profile.id }) .then(user => { if (user) { return done(null, user); } user = new User({ name: profile.displayName, email: profile.emails[0].value, role: 'user', username: profile.emails[0].value.split('@')[0], provider: 'google', google: profile._json }); user.save() .then(user => done(null, user)) .catch(err => done(err)); }) .catch(err => done(err)); })); } ================================================ FILE: server/auth/index.js ================================================ 'use strict'; import express from 'express'; import passport from 'passport'; import config from '../config/environment'; import User from '../api/user/user.model'; // Passport Configuration require('./local/passport').setup(User, config); require('./facebook/passport').setup(User, config); require('./google/passport').setup(User, config); require('./twitter/passport').setup(User, config); var router = express.Router(); router.use('/local', require('./local')); router.use('/facebook', require('./facebook')); router.use('/twitter', require('./twitter')); router.use('/google', require('./google')); export default router; ================================================ FILE: server/auth/local/index.js ================================================ 'use strict'; import express from 'express'; import passport from 'passport'; import {signToken} from '../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.status(401).json(error); } if (!user) { return res.status(404).json({message: 'Something went wrong, please try again.'}); } var token = signToken(user._id, user.role); res.json({ token }); })(req, res, next) }); export default router; ================================================ FILE: server/auth/local/passport.js ================================================ import passport from 'passport'; import {Strategy as LocalStrategy} from 'passport-local'; function localAuthenticate(User, email, password, done) { User.findOneAsync({ email: email.toLowerCase() }) .then(user => { if (!user) { return done(null, false, { message: 'This email is not registered.' }); } user.authenticate(password, function(authError, authenticated) { if (authError) { return done(authError); } if (!authenticated) { return done(null, false, { message: 'This password is not correct.' }); } else { return done(null, user); } }); }) .catch(err => done(err)); } export function setup(User, config) { passport.use(new LocalStrategy({ usernameField: 'email', passwordField: 'password' // this is the virtual field on the model }, function(email, password, done) { return localAuthenticate(User, email, password, done); })); } ================================================ FILE: server/auth/twitter/index.js ================================================ 'use strict'; import express from 'express'; import passport from 'passport'; import {setTokenCookie} from '../auth.service'; var router = express.Router(); router .get('/', passport.authenticate('twitter', { failureRedirect: '/signup', session: false })) .get('/callback', passport.authenticate('twitter', { failureRedirect: '/signup', session: false }), setTokenCookie); export default router; ================================================ FILE: server/auth/twitter/passport.js ================================================ import passport from 'passport'; import {Strategy as TwitterStrategy} from 'passport-twitter'; export function setup(User, config) { passport.use(new TwitterStrategy({ consumerKey: config.twitter.clientID, consumerSecret: config.twitter.clientSecret, callbackURL: config.twitter.callbackURL }, function(token, tokenSecret, profile, done) { User.findOneAsync({ 'twitter.id_str': profile.id }) .then(user => { if (user) { return done(null, user); } user = new User({ name: profile.displayName, username: profile.username, role: 'user', provider: 'twitter', twitter: profile._json }); user.save() .then(user => done(null, user)) .catch(err => done(err)); }) .catch(err => done(err)); })); } ================================================ FILE: server/components/errors/index.js ================================================ /** * 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, html) { if (err) { return res.json(result, result.status); } res.send(html); }); }; ================================================ FILE: server/config/environment/development.js ================================================ 'use strict'; // Development specific configuration // ================================== module.exports = { // MongoDB connection options mongo: { uri: 'mongodb://localhost/paizaqa-dev' }, // Seed database on startup seedDB: true }; ================================================ FILE: server/config/environment/index.js ================================================ '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, // Server IP ip: process.env.IP || '0.0.0.0', // 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: 'paizaqa-secret' }, // MongoDB connection options mongo: { options: { db: { safe: true } } }, facebook: { clientID: process.env.FACEBOOK_ID || 'id', clientSecret: process.env.FACEBOOK_SECRET || 'secret', callbackURL: (process.env.DOMAIN || '') + '/auth/facebook/callback' }, twitter: { clientID: process.env.TWITTER_ID || 'id', clientSecret: process.env.TWITTER_SECRET || 'secret', callbackURL: (process.env.DOMAIN || '') + '/auth/twitter/callback' }, google: { clientID: process.env.GOOGLE_ID || 'id', clientSecret: process.env.GOOGLE_SECRET || 'secret', callbackURL: (process.env.DOMAIN || '') + '/auth/google/callback' } }; // Export the config object based on the NODE_ENV // ============================================== module.exports = _.merge( all, require('./shared'), require('./' + process.env.NODE_ENV + '.js') || {}); ================================================ FILE: server/config/environment/production.js ================================================ '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/paizaqa' } }; ================================================ FILE: server/config/environment/shared.js ================================================ 'use strict'; exports = module.exports = { // List of user roles userRoles: ['guest', 'user', 'admin'] }; ================================================ FILE: server/config/environment/test.js ================================================ 'use strict'; // Test specific configuration // =========================== module.exports = { // MongoDB connection options mongo: { uri: 'mongodb://localhost/paizaqa-test' }, sequelize: { uri: 'sqlite://', options: { logging: false, storage: 'test.sqlite', define: { timestamps: false } } } }; ================================================ FILE: server/config/express.js ================================================ /** * Express configuration */ 'use strict'; import express from 'express'; import favicon from 'serve-favicon'; import morgan from 'morgan'; import compression from 'compression'; import bodyParser from 'body-parser'; import methodOverride from 'method-override'; import cookieParser from 'cookie-parser'; import errorHandler from 'errorhandler'; import path from 'path'; import lusca from 'lusca'; import config from './environment'; import passport from 'passport'; import session from 'express-session'; import connectMongo from 'connect-mongo'; import mongoose from 'mongoose'; var mongoStore = connectMongo(session); export default function(app) { var env = app.get('env'); app.set('views', config.root + '/server/views'); app.engine('html', require('ejs').renderFile); app.set('view engine', 'html'); app.use(compression()); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); app.use(methodOverride()); app.use(cookieParser()); app.use(passport.initialize()); // Persist sessions with mongoStore / sequelizeStore // We need to enable sessions for passport-twitter because it's an // oauth 1.0 strategy, and Lusca depends on sessions app.use(session({ secret: config.secrets.session, saveUninitialized: true, resave: false, store: new mongoStore({ mongooseConnection: mongoose.connection, db: 'paizaqa' }) })); /** * Lusca - express server security * https://github.com/krakenjs/lusca */ if ('test' !== env) { app.use(lusca({ csrf: { angular: true }, xframe: 'SAMEORIGIN', hsts: { maxAge: 31536000, //1 year, in seconds includeSubDomains: true, preload: true }, xssProtection: true })); } app.set('appPath', path.join(config.root, 'client')); if ('production' === env) { app.use(favicon(path.join(config.root, 'client', 'favicon.ico'))); app.use(express.static(app.get('appPath'))); app.use(morgan('dev')); } if ('development' === env) { app.use(require('connect-livereload')()); } if ('development' === env || 'test' === env) { app.use(express.static(path.join(config.root, '.tmp'))); app.use(express.static(app.get('appPath'))); app.use(morgan('dev')); app.use(errorHandler()); // Error handler - has to be last } } ================================================ FILE: server/config/local.env.sample.js ================================================ '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: 'paizaqa-secret', FACEBOOK_ID: 'app-id', FACEBOOK_SECRET: 'secret', TWITTER_ID: 'app-id', TWITTER_SECRET: 'secret', GOOGLE_ID: 'app-id', GOOGLE_SECRET: 'secret', // Control debug level for modules using visionmedia/debug DEBUG: '' }; ================================================ FILE: server/config/seed.js ================================================ /** * Populate DB with sample data on server start * to disable, edit config/environment/index.js, and set `seedDB: false` */ 'use strict'; import Thing from '../api/thing/thing.model'; import User from '../api/user/user.model'; Thing.find({}).removeAsync() .then(() => { Thing.create({ name: 'Development Tools', info: 'Integration with popular tools such as Bower, Grunt, Babel, Karma, ' + 'Mocha, JSHint, Node Inspector, Livereload, Protractor, Jade, ' + 'Stylus, Sass, and Less.' }, { name: 'Server and Client integration', info: 'Built with a powerful and fun stack: MongoDB, Express, ' + 'AngularJS, and Node.' }, { name: 'Smart Build System', info: 'Build system ignores `spec` files, allowing you to keep ' + 'tests alongside code. Automatic injection of scripts and ' + 'styles into your index.html' }, { name: 'Modular Structure', info: 'Best practice client and server structures allow for more ' + 'code reusability and maximum scalability' }, { name: 'Optimized Build', info: 'Build process packs up your templates as a single JavaScript ' + 'payload, minifies your scripts/css/images, and rewrites asset ' + 'names for caching.' }, { name: 'Deployment Ready', info: 'Easily deploy your app to Heroku or Openshift with the heroku ' + 'and openshift subgenerators' }); }); User.find({}).removeAsync() .then(() => { User.createAsync({ provider: 'local', name: 'Test User', email: 'test@example.com', password: 'test' }, { provider: 'local', role: 'admin', name: 'Admin', email: 'admin@example.com', password: 'admin' }) .then(() => { console.log('finished populating users'); }); }); ================================================ FILE: server/config/socketio.js ================================================ /** * Socket.io configuration */ 'use strict'; import config from './environment'; // When the user disconnects.. perform this function onDisconnect(socket) { } // When the user connects.. perform this function onConnect(socket) { // When the client emits 'info', this listens and executes socket.on('info', data => { socket.log(JSON.stringify(data, null, 2)); }); // Insert sockets below require('../api/question/question.socket').register(socket); require('../api/thing/thing.socket').register(socket); } export default function(socketio) { // socket.io (v1.x.x) is powered by debug. // In order to see all the debug output, set DEBUG (in server/config/local.env.js) to including the desired scope. // // ex: DEBUG: "http*,socket.io:socket" // We can authenticate socket.io users and access their token through socket.decoded_token // // 1. You will need to send the token in `client/components/socket/socket.service.js` // // 2. Require authentication here: // socketio.use(require('socketio-jwt').authorize({ // secret: config.secrets.session, // handshake: true // })); socketio.on('connection', function(socket) { socket.address = socket.request.connection.remoteAddress + ':' + socket.request.connection.remotePort; socket.connectedAt = new Date(); socket.log = function(...data) { console.log(`SocketIO ${socket.nsp.name} [${socket.address}]`, ...data); }; // Call onDisconnect. socket.on('disconnect', () => { onDisconnect(socket); socket.log('DISCONNECTED'); }); // Call onConnect. onConnect(socket); socket.log('CONNECTED'); }); } ================================================ FILE: server/index.js ================================================ 'use strict'; // Set default node environment to development var env = process.env.NODE_ENV = process.env.NODE_ENV || 'development'; if (env === 'development' || env === 'test') { // Register the Babel require hook require('babel-core/register'); } // Export the application exports = module.exports = require('./app'); ================================================ FILE: server/routes.js ================================================ /** * Main application routes */ 'use strict'; import errors from './components/errors'; import path from 'path'; export default function(app) { // Insert routes below app.use('/api/questions', require('./api/question')); app.use('/api/things', require('./api/thing')); 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((req, res) => { res.sendFile(path.resolve(app.get('appPath') + '/index.html')); }); } ================================================ FILE: server/views/404.html ================================================ Page Not Found :(

Not found :(

Sorry, but the page you were trying to view does not exist.

It looks like this was the result of either: