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/
#
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
How to make QA sites like this: Building a QA web service in an hour - MEAN stack development(3)
| Stars | Answers | Question |
|---|---|---|
|
{{question.stars.length}}
|
{{question.answers.length}}
|
by {{question.user.name}}
- {{question.createdAt|fromNow}}
{{tag.text}}
|
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('
================================================
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
================================================
Sorry, but the page you were trying to view does not exist.
It looks like this was the result of either:
add a comment