├── .slugignore ├── .bowerrc ├── Procfile ├── public ├── robots.txt ├── modules │ ├── core │ │ ├── img │ │ │ ├── brand │ │ │ │ ├── favicon.ico │ │ │ │ └── northwind.png │ │ │ └── loaders │ │ │ │ └── loader.gif │ │ ├── core.client.module.js │ │ ├── controllers │ │ │ ├── home.client.controller.js │ │ │ └── header.client.controller.js │ │ ├── css │ │ │ └── core.css │ │ ├── config │ │ │ └── core.client.routes.js │ │ ├── tests │ │ │ ├── home.client.controller.test.js │ │ │ └── header.client.controller.test.js │ │ ├── views │ │ │ ├── header.client.view.html │ │ │ └── home.client.view.html │ │ └── services │ │ │ └── menus.client.service.js │ ├── users │ │ ├── img │ │ │ └── buttons │ │ │ │ ├── github.png │ │ │ │ ├── google.png │ │ │ │ ├── facebook.png │ │ │ │ ├── linkedin.png │ │ │ │ └── twitter.png │ │ ├── users.client.module.js │ │ ├── views │ │ │ ├── password │ │ │ │ ├── reset-password-success.client.view.html │ │ │ │ ├── reset-password-invalid.client.view.html │ │ │ │ ├── forgot-password.client.view.html │ │ │ │ └── reset-password.client.view.html │ │ │ ├── settings │ │ │ │ ├── change-password.client.view.html │ │ │ │ ├── social-accounts.client.view.html │ │ │ │ └── edit-profile.client.view.html │ │ │ └── authentication │ │ │ │ ├── signin.client.view.html │ │ │ │ └── signup.client.view.html │ │ ├── css │ │ │ └── users.css │ │ ├── services │ │ │ ├── authentication.client.service.js │ │ │ └── users.client.service.js │ │ ├── config │ │ │ ├── users.client.config.js │ │ │ └── users.client.routes.js │ │ ├── controllers │ │ │ ├── authentication.client.controller.js │ │ │ ├── password.client.controller.js │ │ │ └── settings.client.controller.js │ │ └── tests │ │ │ └── authentication.client.controller.test.js │ ├── products │ │ ├── products.client.module.js │ │ ├── services │ │ │ └── products.client.service.js │ │ ├── config │ │ │ ├── products.client.config.js │ │ │ └── products.client.routes.js │ │ ├── views │ │ │ ├── view-product.client.view.html │ │ │ ├── list-products.client.view.html │ │ │ ├── create-product.client.view.html │ │ │ └── edit-product.client.view.html │ │ ├── controllers │ │ │ └── products.client.controller.js │ │ └── tests │ │ │ └── products.client.controller.test.js │ └── categories │ │ ├── categories.client.module.js │ │ ├── services │ │ └── categories.client.service.js │ │ ├── config │ │ ├── categories.client.config.js │ │ └── categories.client.routes.js │ │ ├── views │ │ ├── view-category.client.view.html │ │ ├── list-categories.client.view.html │ │ ├── create-category.client.view.html │ │ └── edit-category.client.view.html │ │ ├── controllers │ │ └── categories.client.controller.js │ │ └── tests │ │ └── categories.client.controller.test.js ├── humans.txt ├── application.js └── config.js ├── .travis.yml ├── app ├── views │ ├── index.server.view.html │ ├── 500.server.view.html │ ├── 404.server.view.html │ ├── templates │ │ ├── reset-password-confirm-email.server.view.html │ │ └── reset-password-email.server.view.html │ └── layout.server.view.html ├── routes │ ├── core.server.routes.js │ ├── products.server.routes.js │ ├── categories.server.routes.js │ └── users.server.routes.js ├── controllers │ ├── core.server.controller.js │ ├── products.server.controller.js │ ├── categories.server.controller.js │ ├── api.authorization.server.controller.js │ ├── users.server.controller.js │ ├── errors.server.controller.js │ ├── users │ │ ├── users.authorization.server.controller.js │ │ ├── users.profile.server.controller.js │ │ ├── users.authentication.server.controller.js │ │ └── users.password.server.controller.js │ └── crud.server.controller.js ├── models │ ├── validation.server.model.js │ ├── category.server.model.js │ ├── product.server.model.js │ └── user.server.model.js └── tests │ ├── _.tests.js │ ├── models.server.routes.tests.api.js │ ├── user.server.model.test.js │ ├── products.server.routes.tests.js │ └── categories.server.routes.tests.js ├── .gitignore ├── fig.yml ├── .csslintrc ├── generate-ssl-certs.sh ├── bower.json ├── README.md ├── Dockerfile ├── config ├── passport.js ├── strategies │ ├── local.js │ ├── basic.js │ ├── twitter.js │ ├── github.js │ ├── google.js │ ├── facebook.js │ └── linkedin.js ├── init.js ├── env │ ├── all.js │ ├── development.js │ ├── test.js │ ├── secure.js │ └── production.js ├── config.js └── express.js ├── server.js ├── .editorconfig ├── LICENSE.md ├── migrations ├── 1422219657037-add-categories.js └── 1422753894413-add-products.js ├── karma.conf.js ├── .jshintrc ├── package.json └── gruntfile.js /.slugignore: -------------------------------------------------------------------------------- 1 | /app/tests -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/lib" 3 | } 4 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./node_modules/.bin/forever -m 5 server.js 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | env: 5 | - NODE_ENV=travis 6 | services: 7 | - mongodb -------------------------------------------------------------------------------- /public/modules/core/img/brand/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbraithwaite/NorthwindNode/HEAD/public/modules/core/img/brand/favicon.ico -------------------------------------------------------------------------------- /public/modules/core/img/brand/northwind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbraithwaite/NorthwindNode/HEAD/public/modules/core/img/brand/northwind.png -------------------------------------------------------------------------------- /public/modules/core/img/loaders/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbraithwaite/NorthwindNode/HEAD/public/modules/core/img/loaders/loader.gif -------------------------------------------------------------------------------- /public/modules/users/img/buttons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbraithwaite/NorthwindNode/HEAD/public/modules/users/img/buttons/github.png -------------------------------------------------------------------------------- /public/modules/users/img/buttons/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbraithwaite/NorthwindNode/HEAD/public/modules/users/img/buttons/google.png -------------------------------------------------------------------------------- /public/modules/users/img/buttons/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbraithwaite/NorthwindNode/HEAD/public/modules/users/img/buttons/facebook.png -------------------------------------------------------------------------------- /public/modules/users/img/buttons/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbraithwaite/NorthwindNode/HEAD/public/modules/users/img/buttons/linkedin.png -------------------------------------------------------------------------------- /public/modules/users/img/buttons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbraithwaite/NorthwindNode/HEAD/public/modules/users/img/buttons/twitter.png -------------------------------------------------------------------------------- /app/views/index.server.view.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.server.view.html' %} 2 | 3 | {% block content %} 4 |
5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /app/views/500.server.view.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.server.view.html' %} 2 | 3 | {% block content %} 4 |

Server Error

5 |
6 | 	{{error}}
7 | 
8 | {% endblock %} -------------------------------------------------------------------------------- /public/modules/core/core.client.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use Applicaion configuration module to register a new module 4 | ApplicationConfiguration.registerModule('core'); -------------------------------------------------------------------------------- /public/modules/users/users.client.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use Applicaion configuration module to register a new module 4 | ApplicationConfiguration.registerModule('users'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nodemonignore 3 | .sass-cache/ 4 | npm-debug.log 5 | node_modules/ 6 | public/lib 7 | app/tests/coverage/ 8 | .bower-*/ 9 | .idea/ 10 | .migrate 11 | -------------------------------------------------------------------------------- /public/modules/products/products.client.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use applicaion configuration module to register a new module 4 | ApplicationConfiguration.registerModule('products'); -------------------------------------------------------------------------------- /app/views/404.server.view.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.server.view.html' %} 2 | 3 | {% block content %} 4 |

Page Not Found

5 |
6 | 	{{url}} is not a valid path.
7 | 
8 | {% endblock %} -------------------------------------------------------------------------------- /public/modules/categories/categories.client.module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Use application configuration module to register a new module 4 | ApplicationConfiguration.registerModule('categories'); 5 | -------------------------------------------------------------------------------- /fig.yml: -------------------------------------------------------------------------------- 1 | web: 2 | build: . 3 | links: 4 | - db 5 | ports: 6 | - "3000:3000" 7 | environment: 8 | NODE_ENV: development 9 | db: 10 | image: mongo 11 | ports: 12 | - "27017:27017" -------------------------------------------------------------------------------- /app/routes/core.server.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(app) { 4 | // Root routing 5 | var core = require('../../app/controllers/core.server.controller'); 6 | app.route('/').get(core.index); 7 | }; -------------------------------------------------------------------------------- /app/controllers/core.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | exports.index = function(req, res) { 7 | res.render('index', { 8 | user: req.user || null, 9 | request: req 10 | }); 11 | }; -------------------------------------------------------------------------------- /public/modules/users/views/password/reset-password-success.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Password successfully reset

3 | Continue to home page 4 |
-------------------------------------------------------------------------------- /public/modules/users/views/password/reset-password-invalid.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Password reset is invalid

3 | Ask for a new password reset 4 |
-------------------------------------------------------------------------------- /public/humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org/ 2 | # The humans responsible & technology colophon 3 | 4 | # TEAM 5 | 6 | -- -- 7 | 8 | # THANKS 9 | 10 | 11 | 12 | # TECHNOLOGY COLOPHON 13 | 14 | HTML5, CSS3 15 | jQuery, Modernizr 16 | -------------------------------------------------------------------------------- /public/modules/core/controllers/home.client.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | angular.module('core').controller('HomeController', ['$scope', 'Authentication', 5 | function($scope, Authentication) { 6 | // This provides Authentication context. 7 | $scope.authentication = Authentication; 8 | } 9 | ]); -------------------------------------------------------------------------------- /public/modules/users/css/users.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 992px) { 2 | .nav-users { 3 | position: fixed; 4 | } 5 | } 6 | .remove-account-container { 7 | display: inline-block; 8 | position: relative; 9 | } 10 | .btn-remove-account { 11 | top: 10px; 12 | right: 10px; 13 | position: absolute; 14 | } -------------------------------------------------------------------------------- /app/models/validation.server.model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Validation - wires into our custom validator function - http://mongoosejs.com/docs/api.html#schematype_SchemaType-validate 5 | */ 6 | module.exports.len = function(max) { 7 | return function (v) { 8 | return v.length <= max; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /public/modules/users/services/authentication.client.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Authentication service for user variables 4 | angular.module('users').factory('Authentication', [ 5 | function() { 6 | var _this = this; 7 | 8 | _this._data = { 9 | user: window.user 10 | }; 11 | 12 | return _this._data; 13 | } 14 | ]); -------------------------------------------------------------------------------- /public/modules/users/services/users.client.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Users service used for communicating with the users REST endpoint 4 | angular.module('users').factory('Users', ['$resource', 5 | function($resource) { 6 | return $resource('users', {}, { 7 | update: { 8 | method: 'PUT' 9 | } 10 | }); 11 | } 12 | ]); -------------------------------------------------------------------------------- /public/modules/core/css/core.css: -------------------------------------------------------------------------------- 1 | .content { 2 | margin-top: 50px; 3 | } 4 | .undecorated-link:hover { 5 | text-decoration: none; 6 | } 7 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 8 | display: none !important; 9 | } 10 | .ng-invalid.ng-dirty { 11 | border-color: #FA787E; 12 | } 13 | .ng-valid.ng-dirty { 14 | border-color: #78FA89; 15 | } -------------------------------------------------------------------------------- /app/views/templates/reset-password-confirm-email.server.view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Dear {{name}},

7 |

8 |

This is a confirmation that the password for your account has just been changed

9 |
10 |
11 |

The {{appName}} Support Team

12 | 13 | -------------------------------------------------------------------------------- /public/modules/products/services/products.client.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //Products service used to communicate Products REST endpoints 4 | angular.module('products').factory('Products', ['$resource', 5 | function($resource) { 6 | return $resource('products/:productId', { productId: '@_id' 7 | }, { 8 | update: { 9 | method: 'PUT' 10 | } 11 | }); 12 | } 13 | ]); -------------------------------------------------------------------------------- /app/controllers/products.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | errorHandler = require('./errors.server.controller'), 8 | Category = mongoose.model('Category'), 9 | _ = require('lodash'); 10 | 11 | var crud = require('./crud.server.controller')('Product', 'name'); 12 | 13 | module.exports = crud; 14 | -------------------------------------------------------------------------------- /app/controllers/categories.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | errorHandler = require('./errors.server.controller'), 8 | Category = mongoose.model('Category'), 9 | _ = require('lodash'); 10 | 11 | var crud = require('./crud.server.controller')('Category', 'name'); 12 | 13 | module.exports = crud; 14 | -------------------------------------------------------------------------------- /public/modules/categories/services/categories.client.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //Categories service used to communicate Categories REST endpoints 4 | angular.module('categories').factory('Categories', ['$resource', 5 | function($resource) { 6 | return $resource('categories/:categoryId', { categoryId: '@_id' 7 | }, { 8 | update: { 9 | method: 'PUT' 10 | } 11 | }); 12 | } 13 | ]); 14 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-model": false, 4 | "box-sizing": false, 5 | "floats": false, 6 | "font-sizes": false, 7 | "important": false, 8 | "known-properties": false, 9 | "overqualified-elements": false, 10 | "qualified-headings": false, 11 | "regex-selectors": false, 12 | "unique-headings": false, 13 | "universal-selector": false, 14 | "unqualified-attributes": false 15 | } 16 | -------------------------------------------------------------------------------- /app/controllers/api.authorization.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var passport = require('passport'); 4 | 5 | module.exports = function(req, res, next) { 6 | if (req.headers.authorization) { 7 | passport.authenticate('basic', { session: false }, function(err, user, info) { 8 | if (user) { 9 | req.apiAuthed = true; 10 | } 11 | next(); 12 | })(req, res, next); 13 | 14 | } else { 15 | next(); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /generate-ssl-certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Generating self-signed certificates..." 3 | openssl genrsa -out ./config/sslcerts/key.pem -aes256 1024 4 | openssl req -new -key ./config/sslcerts/key.pem -out ./config/sslcerts/csr.pem 5 | openssl x509 -req -days 9999 -in ./config/sslcerts/csr.pem -signkey ./config/sslcerts/key.pem -out ./config/sslcerts/cert.pem 6 | rm ./config/sslcerts/csr.pem 7 | chmod 600 ./config/sslcerts/key.pem ./config/sslcerts/cert.pem 8 | -------------------------------------------------------------------------------- /app/controllers/users.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var _ = require('lodash'); 7 | 8 | /** 9 | * Extend user's controller 10 | */ 11 | module.exports = _.extend( 12 | require('./users/users.authentication.server.controller'), 13 | require('./users/users.authorization.server.controller'), 14 | require('./users/users.password.server.controller'), 15 | require('./users/users.profile.server.controller') 16 | ); -------------------------------------------------------------------------------- /public/modules/products/config/products.client.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Configuring the Articles module 4 | angular.module('products').run(['Menus', 5 | function(Menus) { 6 | // Set top bar menu items 7 | Menus.addMenuItem('topbar', 'Products', 'products', 'dropdown', '/products(/create)?'); 8 | Menus.addSubMenuItem('topbar', 'products', 'List Products', 'products'); 9 | Menus.addSubMenuItem('topbar', 'products', 'New Product', 'products/create'); 10 | } 11 | ]); -------------------------------------------------------------------------------- /public/modules/core/config/core.client.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Setting up route 4 | angular.module('core').config(['$stateProvider', '$urlRouterProvider', 5 | function($stateProvider, $urlRouterProvider) { 6 | // Redirect to home view when route not found 7 | $urlRouterProvider.otherwise('/'); 8 | 9 | // Home state routing 10 | $stateProvider. 11 | state('home', { 12 | url: '/', 13 | templateUrl: 'modules/core/views/home.client.view.html' 14 | }); 15 | } 16 | ]); -------------------------------------------------------------------------------- /public/modules/categories/config/categories.client.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Configuring the Articles module 4 | angular.module('categories').run(['Menus', 5 | function(Menus) { 6 | // Set top bar menu items 7 | Menus.addMenuItem('topbar', 'Categories', 'categories', 'dropdown', '/categories(/create)?'); 8 | Menus.addSubMenuItem('topbar', 'categories', 'List Categories', 'categories'); 9 | Menus.addSubMenuItem('topbar', 'categories', 'New Category', 'categories/create'); 10 | } 11 | ]); 12 | -------------------------------------------------------------------------------- /app/views/templates/reset-password-email.server.view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

Dear {{name}},

7 |
8 |

9 | You have requested to have your password reset for your account at {{appName}} 10 |

11 |

Please visit this url to reset your password:

12 |

{{url}}

13 | If you didn't make this request, you can ignore this email. 14 |
15 |
16 |

The {{appName}} Support Team

17 | 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "northwindnode", 3 | "version": "0.0.1", 4 | "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js", 5 | "dependencies": { 6 | "bootstrap": "~3", 7 | "angular": "~1.2", 8 | "angular-resource": "~1.2", 9 | "angular-mocks": "~1.2", 10 | "angular-cookies": "~1.2", 11 | "angular-animate": "~1.2", 12 | "angular-touch": "~1.2", 13 | "angular-sanitize": "~1.2", 14 | "angular-bootstrap": "~0.12.0", 15 | "angular-ui-utils": "~0.1.1", 16 | "angular-ui-router": "~0.2.11" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/modules/products/views/view-product.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 14 |
15 | -------------------------------------------------------------------------------- /public/modules/categories/views/view-category.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 14 |
15 | -------------------------------------------------------------------------------- /public/modules/core/controllers/header.client.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('core').controller('HeaderController', ['$scope', 'Authentication', 'Menus', 4 | function($scope, Authentication, Menus) { 5 | $scope.authentication = Authentication; 6 | $scope.isCollapsed = false; 7 | $scope.menu = Menus.getMenu('topbar'); 8 | 9 | $scope.toggleCollapsibleMenu = function() { 10 | $scope.isCollapsed = !$scope.isCollapsed; 11 | }; 12 | 13 | // Collapsing the menu after navigation 14 | $scope.$on('$stateChangeSuccess', function() { 15 | $scope.isCollapsed = false; 16 | }); 17 | } 18 | ]); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NorthwindNode 2 | 3 | This example project accompanies the _Learn the Mean Stack- Beginner Tutorial_. The learning material can be found at: 4 | 5 | [http://www.bradoncode.com/tutorials/learn-mean-stack-tutorial/](http://www.bradoncode.com/tutorials/learn-mean-stack-tutorial/) 6 | 7 | 8 | ## Populating Test Data 9 | 10 | You can make use of data migrations to load some test data. 11 | 12 | The NPM package _migrate_ is a dev dependency. Once that is installed, ensure that that app is running, create a user account with the details: 13 | 14 | * username: admin 15 | * password: password 16 | 17 | Then run: 18 | 19 | $ migrate up 20 | -------------------------------------------------------------------------------- /public/modules/core/tests/home.client.controller.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | describe('HomeController', function() { 5 | //Initialize global variables 6 | var scope, 7 | HomeController; 8 | 9 | // Load the main application module 10 | beforeEach(module(ApplicationConfiguration.applicationModuleName)); 11 | 12 | beforeEach(inject(function($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | 15 | HomeController = $controller('HomeController', { 16 | $scope: scope 17 | }); 18 | })); 19 | 20 | it('should expose the authentication service', function() { 21 | expect(scope.authentication).toBeTruthy(); 22 | }); 23 | }); 24 | })(); -------------------------------------------------------------------------------- /public/modules/core/tests/header.client.controller.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | describe('HeaderController', function() { 5 | //Initialize global variables 6 | var scope, 7 | HeaderController; 8 | 9 | // Load the main application module 10 | beforeEach(module(ApplicationConfiguration.applicationModuleName)); 11 | 12 | beforeEach(inject(function($controller, $rootScope) { 13 | scope = $rootScope.$new(); 14 | 15 | HeaderController = $controller('HeaderController', { 16 | $scope: scope 17 | }); 18 | })); 19 | 20 | it('should expose the authentication service', function() { 21 | expect(scope.authentication).toBeTruthy(); 22 | }); 23 | }); 24 | })(); -------------------------------------------------------------------------------- /app/tests/_.tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var should = require('should'), 7 | mongoose = require('mongoose'), 8 | User = mongoose.model('User'); 9 | 10 | /** 11 | * Unit tests 12 | */ 13 | describe('Wait for database connection:', function() { 14 | // This is a workaround to the following issue: https://github.com/meanjs/mean/issues/224 15 | // It appears to still not be fixed in master and I don't want to have to change the server.js source. 16 | // By running this test first, the db connection is started before the web server starts. 17 | it('should be connected', function(done) { 18 | User.find({}, function(err, users) { 19 | done(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /app/models/category.server.model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | validation = require('./validation.server.model'); 9 | 10 | /** 11 | * Category Schema 12 | */ 13 | var CategorySchema = new Schema({ 14 | created: { 15 | type: Date, 16 | default: Date.now 17 | }, 18 | description: { 19 | type: String, 20 | default: '', 21 | trim: true 22 | }, 23 | name: { 24 | type: String, 25 | default: '', 26 | trim: true, 27 | unique : true, 28 | required: 'name cannot be blank', 29 | validate: [validation.len(15), 'name must be 15 chars in length or less'] 30 | } 31 | }); 32 | 33 | mongoose.model('Category', CategorySchema); 34 | -------------------------------------------------------------------------------- /app/routes/products.server.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(app) { 4 | var products = require('../controllers/products.server.controller'); 5 | var users = require('../controllers/users.server.controller'); 6 | var apiAuth = require('../controllers/api.authorization.server.controller'); 7 | 8 | app.route('/products') 9 | .get(apiAuth, users.requiresLogin, products.list) 10 | .post(apiAuth, users.requiresLogin, products.create); 11 | 12 | app.route('/products/:productId') 13 | .get(apiAuth, users.requiresLogin, products.read) 14 | .put(apiAuth, users.requiresLogin, products.update) 15 | .delete(apiAuth, users.requiresLogin, products.delete); 16 | 17 | // Finish by binding the article middleware 18 | app.param('productId', products.getByID); 19 | }; 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM dockerfile/nodejs 2 | 3 | MAINTAINER Matthias Luebken, matthias@catalyst-zero.com 4 | 5 | WORKDIR /home/mean 6 | 7 | # Install Mean.JS Prerequisites 8 | RUN npm install -g grunt-cli 9 | RUN npm install -g bower 10 | 11 | # Install Mean.JS packages 12 | ADD package.json /home/mean/package.json 13 | RUN npm install 14 | 15 | # Manually trigger bower. Why doesnt this work via npm install? 16 | ADD .bowerrc /home/mean/.bowerrc 17 | ADD bower.json /home/mean/bower.json 18 | RUN bower install --config.interactive=false --allow-root 19 | 20 | # Make everything available for start 21 | ADD . /home/mean 22 | 23 | # currently only works for development 24 | ENV NODE_ENV development 25 | 26 | # Port 3000 for server 27 | # Port 35729 for livereload 28 | EXPOSE 3000 35729 29 | CMD ["grunt"] -------------------------------------------------------------------------------- /app/routes/categories.server.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(app) { 4 | var categories = require('../controllers/categories.server.controller'); 5 | var users = require('../controllers/users.server.controller'); 6 | var apiAuth = require('../controllers/api.authorization.server.controller'); 7 | 8 | app.route('/categories') 9 | .get(apiAuth, users.requiresLogin, categories.list) 10 | .post(apiAuth, users.requiresLogin, categories.create); 11 | 12 | app.route('/categories/:categoryId') 13 | .get(apiAuth, users.requiresLogin, categories.read) 14 | .put(apiAuth, users.requiresLogin, categories.update) 15 | .delete(apiAuth, users.requiresLogin, categories.delete); 16 | 17 | // Finish by binding the article middleware 18 | app.param('categoryId', categories.getByID); 19 | }; 20 | -------------------------------------------------------------------------------- /public/application.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //Start by defining the main module and adding the module dependencies 4 | angular.module(ApplicationConfiguration.applicationModuleName, ApplicationConfiguration.applicationModuleVendorDependencies); 5 | 6 | // Setting HTML5 Location Mode 7 | angular.module(ApplicationConfiguration.applicationModuleName).config(['$locationProvider', 8 | function($locationProvider) { 9 | $locationProvider.hashPrefix('!'); 10 | } 11 | ]); 12 | 13 | //Then define the init function for starting up the application 14 | angular.element(document).ready(function() { 15 | //Fixing facebook bug with redirect 16 | if (window.location.hash === '#_=_') window.location.hash = '#!'; 17 | 18 | //Then init the app 19 | angular.bootstrap(document, [ApplicationConfiguration.applicationModuleName]); 20 | }); -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'), 7 | User = require('mongoose').model('User'), 8 | path = require('path'), 9 | config = require('./config'); 10 | 11 | /** 12 | * Module init function. 13 | */ 14 | module.exports = function() { 15 | // Serialize sessions 16 | passport.serializeUser(function(user, done) { 17 | done(null, user.id); 18 | }); 19 | 20 | // Deserialize sessions 21 | passport.deserializeUser(function(id, done) { 22 | User.findOne({ 23 | _id: id 24 | }, '-salt -password', function(err, user) { 25 | done(err, user); 26 | }); 27 | }); 28 | 29 | // Initialize strategies 30 | config.getGlobbedFiles('./config/strategies/**/*.js').forEach(function(strategy) { 31 | require(path.resolve(strategy))(); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /public/modules/products/config/products.client.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //Setting up route 4 | angular.module('products').config(['$stateProvider', 5 | function($stateProvider) { 6 | // Products state routing 7 | $stateProvider. 8 | state('listProducts', { 9 | url: '/products', 10 | templateUrl: 'modules/products/views/list-products.client.view.html' 11 | }). 12 | state('createProduct', { 13 | url: '/products/create', 14 | templateUrl: 'modules/products/views/create-product.client.view.html' 15 | }). 16 | state('viewProduct', { 17 | url: '/products/:productId', 18 | templateUrl: 'modules/products/views/view-product.client.view.html' 19 | }). 20 | state('editProduct', { 21 | url: '/products/:productId/edit', 22 | templateUrl: 'modules/products/views/edit-product.client.view.html' 23 | }); 24 | } 25 | ]); -------------------------------------------------------------------------------- /public/modules/users/config/users.client.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Config HTTP Error Handling 4 | angular.module('users').config(['$httpProvider', 5 | function($httpProvider) { 6 | // Set the httpProvider "not authorized" interceptor 7 | $httpProvider.interceptors.push(['$q', '$location', 'Authentication', 8 | function($q, $location, Authentication) { 9 | return { 10 | responseError: function(rejection) { 11 | switch (rejection.status) { 12 | case 401: 13 | // Deauthenticate the global user 14 | Authentication.user = null; 15 | 16 | // Redirect to signin page 17 | $location.path('signin'); 18 | break; 19 | case 403: 20 | // Add unauthorized behaviour 21 | break; 22 | } 23 | 24 | return $q.reject(rejection); 25 | } 26 | }; 27 | } 28 | ]); 29 | } 30 | ]); -------------------------------------------------------------------------------- /public/modules/categories/config/categories.client.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //Setting up route 4 | angular.module('categories').config(['$stateProvider', 5 | function($stateProvider) { 6 | // Categories state routing 7 | $stateProvider. 8 | state('listCategories', { 9 | url: '/categories', 10 | templateUrl: 'modules/categories/views/list-categories.client.view.html' 11 | }). 12 | state('createCategory', { 13 | url: '/categories/create', 14 | templateUrl: 'modules/categories/views/create-category.client.view.html' 15 | }). 16 | state('viewCategory', { 17 | url: '/categories/:categoryId', 18 | templateUrl: 'modules/categories/views/view-category.client.view.html' 19 | }). 20 | state('editCategory', { 21 | url: '/categories/:categoryId/edit', 22 | templateUrl: 'modules/categories/views/edit-category.client.view.html' 23 | }); 24 | } 25 | ]); 26 | -------------------------------------------------------------------------------- /public/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Init the application configuration module for AngularJS application 4 | var ApplicationConfiguration = (function() { 5 | // Init module configuration options 6 | var applicationModuleName = 'northwindnode'; 7 | var applicationModuleVendorDependencies = ['ngResource', 'ngCookies', 'ngAnimate', 'ngTouch', 'ngSanitize', 'ui.router', 'ui.bootstrap', 'ui.utils']; 8 | 9 | // Add a new vertical module 10 | var registerModule = function(moduleName, dependencies) { 11 | // Create angular module 12 | angular.module(moduleName, dependencies || []); 13 | 14 | // Add the module to the AngularJS configuration file 15 | angular.module(applicationModuleName).requires.push(moduleName); 16 | }; 17 | 18 | return { 19 | applicationModuleName: applicationModuleName, 20 | applicationModuleVendorDependencies: applicationModuleVendorDependencies, 21 | registerModule: registerModule 22 | }; 23 | })(); -------------------------------------------------------------------------------- /config/strategies/local.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'), 7 | LocalStrategy = require('passport-local').Strategy, 8 | User = require('mongoose').model('User'); 9 | 10 | module.exports = function() { 11 | // Use local strategy 12 | passport.use(new LocalStrategy({ 13 | usernameField: 'username', 14 | passwordField: 'password' 15 | }, 16 | function(username, password, done) { 17 | User.findOne({ 18 | username: username 19 | }, function(err, user) { 20 | if (err) { 21 | return done(err); 22 | } 23 | if (!user) { 24 | return done(null, false, { 25 | message: 'Unknown user or invalid password' 26 | }); 27 | } 28 | if (!user.authenticate(password)) { 29 | return done(null, false, { 30 | message: 'Unknown user or invalid password' 31 | }); 32 | } 33 | 34 | return done(null, user); 35 | }); 36 | } 37 | )); 38 | }; -------------------------------------------------------------------------------- /config/strategies/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'), 7 | BasicStrategy = require('passport-http').BasicStrategy, 8 | User = require('mongoose').model('User'); 9 | 10 | module.exports = function() { 11 | // Use basic strategy 12 | passport.use(new BasicStrategy({ 13 | usernameField: 'username', 14 | passwordField: 'password' 15 | }, 16 | function(username, password, done) { 17 | User.findOne({ 18 | username: username 19 | }, function(err, user) { 20 | if (err) { 21 | return done(err); 22 | } 23 | if (!user) { 24 | return done(null, false, { 25 | message: 'Unknown user or invalid password' 26 | }); 27 | } 28 | if (!user.authenticate(password)) { 29 | return done(null, false, { 30 | message: 'Unknown user or invalid password' 31 | }); 32 | } 33 | 34 | return done(null, user); 35 | }); 36 | } 37 | )); 38 | }; 39 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Module dependencies. 4 | */ 5 | var init = require('./config/init')(), 6 | config = require('./config/config'), 7 | mongoose = require('mongoose'), 8 | chalk = require('chalk'); 9 | 10 | /** 11 | * Main application entry file. 12 | * Please note that the order of loading is important. 13 | */ 14 | 15 | // Bootstrap db connection 16 | var db = mongoose.connect(config.db, function(err) { 17 | if (err) { 18 | console.error(chalk.red('Could not connect to MongoDB!')); 19 | console.log(chalk.red(err)); 20 | } 21 | }); 22 | 23 | // Init the express application 24 | var app = require('./config/express')(db); 25 | 26 | // Bootstrap passport config 27 | require('./config/passport')(); 28 | 29 | // Start the app by listening on 30 | app.listen(config.port); 31 | 32 | // Expose app 33 | exports = module.exports = app; 34 | 35 | // Logging initialization 36 | console.log('MEAN.JS application started on port ' + config.port); 37 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # Howto with your editor: 4 | # Sublime: https://github.com/sindresorhus/editorconfig-sublime 5 | 6 | # top-most EditorConfig file 7 | root = true 8 | 9 | # Unix-style newlines with a newline ending every file 10 | [**] 11 | end_of_line = lf 12 | insert_final_newline = true 13 | 14 | # Standard at: https://github.com/felixge/node-style-guide 15 | [**.js, **.json] 16 | trim_trailing_whitespace = true 17 | indent_style = tab 18 | quote_type = single 19 | curly_bracket_next_line = false 20 | spaces_around_operators = true 21 | space_after_control_statements = true 22 | space_after_anonymous_functions = false 23 | spaces_in_brackets = false 24 | 25 | # No Standard. Please document a standard if different from .js 26 | [**.yml, **.html, **.css] 27 | trim_trailing_whitespace = true 28 | indent_style = tab 29 | 30 | # No standard. Please document a standard if different from .js 31 | [**.md] 32 | indent_style = tab 33 | 34 | # Standard at: 35 | [Makefile] 36 | indent_style = tab -------------------------------------------------------------------------------- /app/controllers/errors.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Get unique error field name 5 | */ 6 | var getUniqueErrorMessage = function(err) { 7 | var output; 8 | 9 | try { 10 | var fieldName = err.err.substring(err.err.lastIndexOf('.$') + 2, err.err.lastIndexOf('_1')); 11 | output = fieldName.charAt(0).toUpperCase() + fieldName.slice(1) + ' already exists'; 12 | 13 | } catch (ex) { 14 | output = 'Unique field already exists'; 15 | } 16 | 17 | return output; 18 | }; 19 | 20 | /** 21 | * Get the error message from error object 22 | */ 23 | exports.getErrorMessage = function(err) { 24 | var message = err.message; 25 | if (err.code) { 26 | switch (err.code) { 27 | case 11000: 28 | case 11001: 29 | message = getUniqueErrorMessage(err); 30 | break; 31 | default: 32 | message = 'Something went wrong'; 33 | } 34 | } else { 35 | for (var errName in err.errors) { 36 | if (err.errors[errName].message) message = err.errors[errName].message; 37 | } 38 | } 39 | 40 | return message; 41 | }; 42 | -------------------------------------------------------------------------------- /public/modules/users/views/password/forgot-password.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Restore your password

3 |

Enter your account username.

4 |
5 | 21 |
22 |
-------------------------------------------------------------------------------- /config/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var glob = require('glob'), 7 | chalk = require('chalk'); 8 | 9 | /** 10 | * Module init function. 11 | */ 12 | module.exports = function() { 13 | /** 14 | * Before we begin, lets set the environment variable 15 | * We'll Look for a valid NODE_ENV variable and if one cannot be found load the development NODE_ENV 16 | */ 17 | glob('./config/env/' + process.env.NODE_ENV + '.js', { 18 | sync: true 19 | }, function(err, environmentFiles) { 20 | if (!environmentFiles.length) { 21 | if (process.env.NODE_ENV) { 22 | console.error(chalk.red('No configuration file found for "' + process.env.NODE_ENV + '" environment using development instead')); 23 | } else { 24 | console.error(chalk.red('NODE_ENV is not defined! Using default development environment')); 25 | } 26 | 27 | process.env.NODE_ENV = 'development'; 28 | } else { 29 | console.log(chalk.black.bgWhite('Application loaded using the "' + process.env.NODE_ENV + '" environment configuration')); 30 | } 31 | }); 32 | 33 | }; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## License 2 | (The MIT License) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | 'Software'), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/models/product.server.model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | validation = require('./validation.server.model'); 9 | 10 | /** 11 | * Product Schema 12 | */ 13 | var ProductSchema = new Schema({ 14 | category: { 15 | type: Schema.Types.ObjectId, 16 | ref: 'Category' 17 | //, required: 'invalid category' // TODO: make tests pass valid category 18 | }, 19 | created: { 20 | type: Date, 21 | default: Date.now 22 | }, 23 | name: { 24 | type: String, 25 | default: '', 26 | trim: true, 27 | required: 'name cannot be blank', 28 | validate: [validation.len(40), 'name must be 40 chars in length or less'] 29 | }, 30 | quantityPerUnit: { 31 | type: String 32 | }, 33 | unitPrice: { 34 | type: Number, 35 | default: 0 36 | }, 37 | unitsInStock: { 38 | type: Number, 39 | default: 0, 40 | min: 0 41 | }, 42 | unitsOnOrder: { 43 | type: Number, 44 | default: 0, 45 | min: 0 46 | }, 47 | discontinued: { 48 | type: Boolean, 49 | default: false 50 | } 51 | }); 52 | 53 | mongoose.model('Product', ProductSchema); 54 | -------------------------------------------------------------------------------- /config/strategies/twitter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'), 7 | url = require('url'), 8 | TwitterStrategy = require('passport-twitter').Strategy, 9 | config = require('../config'), 10 | users = require('../../app/controllers/users.server.controller'); 11 | 12 | module.exports = function() { 13 | // Use twitter strategy 14 | passport.use(new TwitterStrategy({ 15 | consumerKey: config.twitter.clientID, 16 | consumerSecret: config.twitter.clientSecret, 17 | callbackURL: config.twitter.callbackURL, 18 | passReqToCallback: true 19 | }, 20 | function(req, token, tokenSecret, profile, done) { 21 | // Set the provider data and include tokens 22 | var providerData = profile._json; 23 | providerData.token = token; 24 | providerData.tokenSecret = tokenSecret; 25 | 26 | // Create the user OAuth profile 27 | var providerUserProfile = { 28 | displayName: profile.displayName, 29 | username: profile.username, 30 | provider: 'twitter', 31 | providerIdentifierField: 'id_str', 32 | providerData: providerData 33 | }; 34 | 35 | // Save the user OAuth profile 36 | users.saveOAuthUserProfile(req, providerUserProfile, done); 37 | } 38 | )); 39 | }; -------------------------------------------------------------------------------- /public/modules/users/controllers/authentication.client.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('users').controller('AuthenticationController', ['$scope', '$http', '$location', 'Authentication', 4 | function($scope, $http, $location, Authentication) { 5 | $scope.authentication = Authentication; 6 | 7 | // If user is signed in then redirect back home 8 | if ($scope.authentication.user) $location.path('/'); 9 | 10 | $scope.signup = function() { 11 | $http.post('/auth/signup', $scope.credentials).success(function(response) { 12 | // If successful we assign the response to the global user model 13 | $scope.authentication.user = response; 14 | 15 | // And redirect to the index page 16 | $location.path('/'); 17 | }).error(function(response) { 18 | $scope.error = response.message; 19 | }); 20 | }; 21 | 22 | $scope.signin = function() { 23 | $http.post('/auth/signin', $scope.credentials).success(function(response) { 24 | // If successful we assign the response to the global user model 25 | $scope.authentication.user = response; 26 | 27 | // And redirect to the index page 28 | $location.path('/'); 29 | }).error(function(response) { 30 | $scope.error = response.message; 31 | }); 32 | }; 33 | } 34 | ]); -------------------------------------------------------------------------------- /public/modules/products/views/list-products.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 |
8 | 14 | 15 |
16 | No Products yet, why don't you create one? 17 |
18 |
19 | -------------------------------------------------------------------------------- /public/modules/categories/views/list-categories.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 |
8 | 14 | 15 |
16 | No categories yet, why don't you create one? 17 |
18 |
19 | -------------------------------------------------------------------------------- /config/strategies/github.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'), 7 | url = require('url'), 8 | GithubStrategy = require('passport-github').Strategy, 9 | config = require('../config'), 10 | users = require('../../app/controllers/users.server.controller'); 11 | 12 | module.exports = function() { 13 | // Use github strategy 14 | passport.use(new GithubStrategy({ 15 | clientID: config.github.clientID, 16 | clientSecret: config.github.clientSecret, 17 | callbackURL: config.github.callbackURL, 18 | passReqToCallback: true 19 | }, 20 | function(req, accessToken, refreshToken, profile, done) { 21 | // Set the provider data and include tokens 22 | var providerData = profile._json; 23 | providerData.accessToken = accessToken; 24 | providerData.refreshToken = refreshToken; 25 | 26 | // Create the user OAuth profile 27 | var providerUserProfile = { 28 | displayName: profile.displayName, 29 | email: profile.emails[0].value, 30 | username: profile.username, 31 | provider: 'github', 32 | providerIdentifierField: 'id', 33 | providerData: providerData 34 | }; 35 | 36 | // Save the user OAuth profile 37 | users.saveOAuthUserProfile(req, providerUserProfile, done); 38 | } 39 | )); 40 | }; -------------------------------------------------------------------------------- /app/controllers/users/users.authorization.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var _ = require('lodash'), 7 | mongoose = require('mongoose'), 8 | User = mongoose.model('User'); 9 | 10 | /** 11 | * User middleware 12 | */ 13 | exports.userByID = function(req, res, next, id) { 14 | User.findOne({ 15 | _id: id 16 | }).exec(function(err, user) { 17 | if (err) return next(err); 18 | if (!user) return next(new Error('Failed to load User ' + id)); 19 | req.profile = user; 20 | next(); 21 | }); 22 | }; 23 | 24 | /** 25 | * Require login routing middleware 26 | */ 27 | exports.requiresLogin = function(req, res, next) { 28 | if (!req.isAuthenticated() && !req.apiAuthed) { 29 | return res.status(401).send({ 30 | message: 'User is not logged in' 31 | }); 32 | } 33 | 34 | next(); 35 | }; 36 | 37 | /** 38 | * User authorizations routing middleware 39 | */ 40 | exports.hasAuthorization = function(roles) { 41 | var _this = this; 42 | 43 | return function(req, res, next) { 44 | _this.requiresLogin(req, res, function() { 45 | if (_.intersection(req.user.roles, roles).length) { 46 | return next(); 47 | } else { 48 | return res.status(403).send({ 49 | message: 'User is not authorized' 50 | }); 51 | } 52 | }); 53 | }; 54 | }; 55 | -------------------------------------------------------------------------------- /public/modules/users/views/password/reset-password.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Reset your password

3 |
4 | 25 |
26 |
-------------------------------------------------------------------------------- /app/controllers/users/users.profile.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var _ = require('lodash'), 7 | errorHandler = require('../errors.server.controller.js'), 8 | mongoose = require('mongoose'), 9 | passport = require('passport'), 10 | User = mongoose.model('User'); 11 | 12 | /** 13 | * Update user details 14 | */ 15 | exports.update = function(req, res) { 16 | // Init Variables 17 | var user = req.user; 18 | var message = null; 19 | 20 | // For security measurement we remove the roles from the req.body object 21 | delete req.body.roles; 22 | 23 | if (user) { 24 | // Merge existing user 25 | user = _.extend(user, req.body); 26 | user.updated = Date.now(); 27 | user.displayName = user.firstName + ' ' + user.lastName; 28 | 29 | user.save(function(err) { 30 | if (err) { 31 | return res.status(400).send({ 32 | message: errorHandler.getErrorMessage(err) 33 | }); 34 | } else { 35 | req.login(user, function(err) { 36 | if (err) { 37 | res.status(400).send(err); 38 | } else { 39 | res.json(user); 40 | } 41 | }); 42 | } 43 | }); 44 | } else { 45 | res.status(400).send({ 46 | message: 'User is not signed in' 47 | }); 48 | } 49 | }; 50 | 51 | /** 52 | * Send User 53 | */ 54 | exports.me = function(req, res) { 55 | res.json(req.user || null); 56 | }; -------------------------------------------------------------------------------- /config/strategies/google.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'), 7 | url = require('url'), 8 | GoogleStrategy = require('passport-google-oauth').OAuth2Strategy, 9 | config = require('../config'), 10 | users = require('../../app/controllers/users.server.controller'); 11 | 12 | module.exports = function() { 13 | // Use google strategy 14 | passport.use(new GoogleStrategy({ 15 | clientID: config.google.clientID, 16 | clientSecret: config.google.clientSecret, 17 | callbackURL: config.google.callbackURL, 18 | passReqToCallback: true 19 | }, 20 | function(req, accessToken, refreshToken, profile, done) { 21 | // Set the provider data and include tokens 22 | var providerData = profile._json; 23 | providerData.accessToken = accessToken; 24 | providerData.refreshToken = refreshToken; 25 | 26 | // Create the user OAuth profile 27 | var providerUserProfile = { 28 | firstName: profile.name.givenName, 29 | lastName: profile.name.familyName, 30 | displayName: profile.displayName, 31 | email: profile.emails[0].value, 32 | username: profile.username, 33 | provider: 'google', 34 | providerIdentifierField: 'id', 35 | providerData: providerData 36 | }; 37 | 38 | // Save the user OAuth profile 39 | users.saveOAuthUserProfile(req, providerUserProfile, done); 40 | } 41 | )); 42 | }; -------------------------------------------------------------------------------- /config/strategies/facebook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'), 7 | url = require('url'), 8 | FacebookStrategy = require('passport-facebook').Strategy, 9 | config = require('../config'), 10 | users = require('../../app/controllers/users.server.controller'); 11 | 12 | module.exports = function() { 13 | // Use facebook strategy 14 | passport.use(new FacebookStrategy({ 15 | clientID: config.facebook.clientID, 16 | clientSecret: config.facebook.clientSecret, 17 | callbackURL: config.facebook.callbackURL, 18 | passReqToCallback: true 19 | }, 20 | function(req, accessToken, refreshToken, profile, done) { 21 | // Set the provider data and include tokens 22 | var providerData = profile._json; 23 | providerData.accessToken = accessToken; 24 | providerData.refreshToken = refreshToken; 25 | 26 | // Create the user OAuth profile 27 | var providerUserProfile = { 28 | firstName: profile.name.givenName, 29 | lastName: profile.name.familyName, 30 | displayName: profile.displayName, 31 | email: profile.emails[0].value, 32 | username: profile.username, 33 | provider: 'facebook', 34 | providerIdentifierField: 'id', 35 | providerData: providerData 36 | }; 37 | 38 | // Save the user OAuth profile 39 | users.saveOAuthUserProfile(req, providerUserProfile, done); 40 | } 41 | )); 42 | }; -------------------------------------------------------------------------------- /config/env/all.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | app: { 5 | title: 'NorthwindNode', 6 | description: 'Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js', 7 | keywords: 'MongoDB, Express, AngularJS, Node.js' 8 | }, 9 | port: process.env.PORT || 3000, 10 | templateEngine: 'swig', 11 | sessionSecret: 'MEAN', 12 | sessionCollection: 'sessions', 13 | assets: { 14 | lib: { 15 | css: [ 16 | 'public/lib/bootstrap/dist/css/bootstrap.css', 17 | 'public/lib/bootstrap/dist/css/bootstrap-theme.css', 18 | ], 19 | js: [ 20 | 'public/lib/angular/angular.js', 21 | 'public/lib/angular-resource/angular-resource.js', 22 | 'public/lib/angular-cookies/angular-cookies.js', 23 | 'public/lib/angular-animate/angular-animate.js', 24 | 'public/lib/angular-touch/angular-touch.js', 25 | 'public/lib/angular-sanitize/angular-sanitize.js', 26 | 'public/lib/angular-ui-router/release/angular-ui-router.js', 27 | 'public/lib/angular-ui-utils/ui-utils.js', 28 | 'public/lib/angular-bootstrap/ui-bootstrap-tpls.js' 29 | ] 30 | }, 31 | css: [ 32 | 'public/modules/**/css/*.css' 33 | ], 34 | js: [ 35 | 'public/config.js', 36 | 'public/application.js', 37 | 'public/modules/*/*.js', 38 | 'public/modules/*/*[!tests]*/*.js' 39 | ], 40 | tests: [ 41 | 'public/lib/angular-mocks/angular-mocks.js', 42 | 'public/modules/*/tests/*.js' 43 | ] 44 | } 45 | }; -------------------------------------------------------------------------------- /public/modules/categories/views/create-category.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | db: 'mongodb://localhost/northwindnode-dev', 5 | app: { 6 | title: 'NorthwindNode - Development Environment' 7 | }, 8 | facebook: { 9 | clientID: process.env.FACEBOOK_ID || 'APP_ID', 10 | clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', 11 | callbackURL: '/auth/facebook/callback' 12 | }, 13 | twitter: { 14 | clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', 15 | clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', 16 | callbackURL: '/auth/twitter/callback' 17 | }, 18 | google: { 19 | clientID: process.env.GOOGLE_ID || 'APP_ID', 20 | clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', 21 | callbackURL: '/auth/google/callback' 22 | }, 23 | linkedin: { 24 | clientID: process.env.LINKEDIN_ID || 'APP_ID', 25 | clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', 26 | callbackURL: '/auth/linkedin/callback' 27 | }, 28 | github: { 29 | clientID: process.env.GITHUB_ID || 'APP_ID', 30 | clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', 31 | callbackURL: '/auth/github/callback' 32 | }, 33 | mailer: { 34 | from: process.env.MAILER_FROM || 'MAILER_FROM', 35 | options: { 36 | service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', 37 | auth: { 38 | user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', 39 | pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' 40 | } 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /config/env/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | db: 'mongodb://localhost/northwindnode-test', 5 | port: 3001, 6 | app: { 7 | title: 'NorthwindNode - Test Environment' 8 | }, 9 | facebook: { 10 | clientID: process.env.FACEBOOK_ID || 'APP_ID', 11 | clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', 12 | callbackURL: '/auth/facebook/callback' 13 | }, 14 | twitter: { 15 | clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', 16 | clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', 17 | callbackURL: '/auth/twitter/callback' 18 | }, 19 | google: { 20 | clientID: process.env.GOOGLE_ID || 'APP_ID', 21 | clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', 22 | callbackURL: '/auth/google/callback' 23 | }, 24 | linkedin: { 25 | clientID: process.env.LINKEDIN_ID || 'APP_ID', 26 | clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', 27 | callbackURL: '/auth/linkedin/callback' 28 | }, 29 | github: { 30 | clientID: process.env.GITHUB_ID || 'APP_ID', 31 | clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', 32 | callbackURL: '/auth/github/callback' 33 | }, 34 | mailer: { 35 | from: process.env.MAILER_FROM || 'MAILER_FROM', 36 | options: { 37 | service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', 38 | auth: { 39 | user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', 40 | pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' 41 | } 42 | } 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /config/strategies/linkedin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'), 7 | url = require('url'), 8 | LinkedInStrategy = require('passport-linkedin').Strategy, 9 | config = require('../config'), 10 | users = require('../../app/controllers/users.server.controller'); 11 | 12 | module.exports = function() { 13 | // Use linkedin strategy 14 | passport.use(new LinkedInStrategy({ 15 | consumerKey: config.linkedin.clientID, 16 | consumerSecret: config.linkedin.clientSecret, 17 | callbackURL: config.linkedin.callbackURL, 18 | passReqToCallback: true, 19 | profileFields: ['id', 'first-name', 'last-name', 'email-address'] 20 | }, 21 | function(req, accessToken, refreshToken, profile, done) { 22 | // Set the provider data and include tokens 23 | var providerData = profile._json; 24 | providerData.accessToken = accessToken; 25 | providerData.refreshToken = refreshToken; 26 | 27 | // Create the user OAuth profile 28 | var providerUserProfile = { 29 | firstName: profile.name.givenName, 30 | lastName: profile.name.familyName, 31 | displayName: profile.displayName, 32 | email: profile.emails[0].value, 33 | username: profile.username, 34 | provider: 'linkedin', 35 | providerIdentifierField: 'id', 36 | providerData: providerData 37 | }; 38 | 39 | // Save the user OAuth profile 40 | users.saveOAuthUserProfile(req, providerUserProfile, done); 41 | } 42 | )); 43 | }; -------------------------------------------------------------------------------- /public/modules/categories/views/edit-category.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /migrations/1422219657037-add-categories.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | request = require('request'); 3 | 4 | var data = [ 5 | { 'name': 'Beverages', 'description': 'Soft drinks, coffees, teas, beers, and ales' }, 6 | { 'name': 'Condiments', 'description': 'Sweet and savory sauces, relishes, spreads, and seasonings' }, 7 | { 'name': 'Confections', 'description': 'Desserts, candies, and sweet breads' }, 8 | { 'name': 'Dairy Products', 'description': 'Cheeses' }, 9 | { 'name': 'Grains/Cereals', 'description': 'Breads, crackers, pasta, and cereal' }, 10 | { 'name': 'Meat/Poultry', 'description': 'Prepared meats' }, 11 | { 'name': 'Produce', 'description': 'Dried fruit and bean curd' }, 12 | { 'name': 'Seafood', 'description': 'Seaweed and fish' }]; 13 | 14 | exports.up = function(next) { 15 | 16 | async.each(data, function(c, callback) { 17 | var options = { 18 | method: 'post', 19 | body: c, 20 | json: true, 21 | url: 'http://localhost:3000/categories', 22 | auth: { 23 | user: 'admin', 24 | pass: 'password' 25 | } 26 | }; 27 | 28 | request(options, function (err, response) { 29 | if (response.statusCode == 201) { 30 | callback(); 31 | } else { 32 | callback(err); 33 | } 34 | }); 35 | }, function(err) { 36 | if (err) { 37 | next(err); 38 | } else { 39 | next(); 40 | } 41 | }); 42 | }; 43 | 44 | exports.down = function(next) { 45 | next(); 46 | }; 47 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var applicationConfiguration = require('./config/config'); 7 | 8 | // Karma configuration 9 | module.exports = function(config) { 10 | config.set({ 11 | // Frameworks to use 12 | frameworks: ['jasmine'], 13 | 14 | // List of files / patterns to load in the browser 15 | files: applicationConfiguration.assets.lib.js.concat(applicationConfiguration.assets.js, applicationConfiguration.assets.tests), 16 | 17 | // Test results reporter to use 18 | // Possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 19 | //reporters: ['progress'], 20 | reporters: ['progress'], 21 | 22 | // Web server port 23 | port: 9876, 24 | 25 | // Enable / disable colors in the output (reporters and logs) 26 | colors: true, 27 | 28 | // Level of logging 29 | // Possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 30 | logLevel: config.LOG_INFO, 31 | 32 | // Enable / disable watching file and executing tests whenever any file changes 33 | autoWatch: true, 34 | 35 | // Start these browsers, currently available: 36 | // - Chrome 37 | // - ChromeCanary 38 | // - Firefox 39 | // - Opera 40 | // - Safari (only Mac) 41 | // - PhantomJS 42 | // - IE (only Windows) 43 | browsers: ['PhantomJS'], 44 | 45 | // If browser does not capture in given timeout [ms], kill it 46 | captureTimeout: 60000, 47 | 48 | // Continuous Integration mode 49 | // If true, it capture browsers, run tests and exit 50 | singleRun: true 51 | }); 52 | }; 53 | -------------------------------------------------------------------------------- /app/tests/models.server.routes.tests.api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'), 4 | request = require('supertest'), 5 | mongoose = require('mongoose'); 6 | 7 | module.exports = function(app, model, path) { 8 | var agent = request.agent(app); 9 | var Model = mongoose.model(model); 10 | 11 | var create = function(item, cb) { 12 | agent.post(path) 13 | .auth('username', 'password') 14 | .send(item) 15 | .end(function (err, res) { 16 | cb(res); 17 | }); 18 | }; 19 | 20 | var getById = function(id, cb) { 21 | agent.get(path + id) 22 | .auth('username', 'password') 23 | .end(function (err, res) { 24 | cb(res); 25 | }); 26 | }; 27 | 28 | var get = function(cb) { 29 | agent.get(path) 30 | .auth('username', 'password') 31 | .end(function (err, res) { 32 | cb(res); 33 | }); 34 | }; 35 | 36 | var update = function(item, cb) { 37 | agent.put(path + item._id) 38 | .auth('username', 'password') 39 | .send(item) 40 | .end(function (err, res) { 41 | cb(res); 42 | }); 43 | }; 44 | 45 | var deleteById = function(id, cb) { 46 | agent.delete(path + id) 47 | .auth('username', 'password') 48 | .end(function (err, res) { 49 | cb(res); 50 | }); 51 | }; 52 | 53 | var clearData = function(cb) { 54 | Model.remove().exec(cb); 55 | }; 56 | 57 | return { 58 | create: create, 59 | get: getById, 60 | update: update, 61 | list: get, 62 | clear: clearData, 63 | delete: deleteById 64 | }; 65 | 66 | }; 67 | -------------------------------------------------------------------------------- /public/modules/users/controllers/password.client.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('users').controller('PasswordController', ['$scope', '$stateParams', '$http', '$location', 'Authentication', 4 | function($scope, $stateParams, $http, $location, Authentication) { 5 | $scope.authentication = Authentication; 6 | 7 | //If user is signed in then redirect back home 8 | if ($scope.authentication.user) $location.path('/'); 9 | 10 | // Submit forgotten password account id 11 | $scope.askForPasswordReset = function() { 12 | $scope.success = $scope.error = null; 13 | 14 | $http.post('/auth/forgot', $scope.credentials).success(function(response) { 15 | // Show user success message and clear form 16 | $scope.credentials = null; 17 | $scope.success = response.message; 18 | 19 | }).error(function(response) { 20 | // Show user error message and clear form 21 | $scope.credentials = null; 22 | $scope.error = response.message; 23 | }); 24 | }; 25 | 26 | // Change user password 27 | $scope.resetUserPassword = function() { 28 | $scope.success = $scope.error = null; 29 | 30 | $http.post('/auth/reset/' + $stateParams.token, $scope.passwordDetails).success(function(response) { 31 | // If successful show success message and clear form 32 | $scope.passwordDetails = null; 33 | 34 | // Attach user profile 35 | Authentication.user = response; 36 | 37 | // And redirect to the index page 38 | $location.path('/password/reset/success'); 39 | }).error(function(response) { 40 | $scope.error = response.message; 41 | }); 42 | }; 43 | } 44 | ]); -------------------------------------------------------------------------------- /public/modules/users/config/users.client.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Setting up route 4 | angular.module('users').config(['$stateProvider', 5 | function($stateProvider) { 6 | // Users state routing 7 | $stateProvider. 8 | state('profile', { 9 | url: '/settings/profile', 10 | templateUrl: 'modules/users/views/settings/edit-profile.client.view.html' 11 | }). 12 | state('password', { 13 | url: '/settings/password', 14 | templateUrl: 'modules/users/views/settings/change-password.client.view.html' 15 | }). 16 | state('accounts', { 17 | url: '/settings/accounts', 18 | templateUrl: 'modules/users/views/settings/social-accounts.client.view.html' 19 | }). 20 | state('signup', { 21 | url: '/signup', 22 | templateUrl: 'modules/users/views/authentication/signup.client.view.html' 23 | }). 24 | state('signin', { 25 | url: '/signin', 26 | templateUrl: 'modules/users/views/authentication/signin.client.view.html' 27 | }). 28 | state('forgot', { 29 | url: '/password/forgot', 30 | templateUrl: 'modules/users/views/password/forgot-password.client.view.html' 31 | }). 32 | state('reset-invalid', { 33 | url: '/password/reset/invalid', 34 | templateUrl: 'modules/users/views/password/reset-password-invalid.client.view.html' 35 | }). 36 | state('reset-success', { 37 | url: '/password/reset/success', 38 | templateUrl: 'modules/users/views/password/reset-password-success.client.view.html' 39 | }). 40 | state('reset', { 41 | url: '/password/reset/:token', 42 | templateUrl: 'modules/users/views/password/reset-password.client.view.html' 43 | }); 44 | } 45 | ]); -------------------------------------------------------------------------------- /public/modules/users/views/settings/change-password.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Change your password

3 |
4 | 29 |
30 |
-------------------------------------------------------------------------------- /public/modules/users/views/settings/social-accounts.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Connected social accounts:

3 |
4 | 10 |
11 |

Connect other social accounts:

12 | 29 |
-------------------------------------------------------------------------------- /app/tests/user.server.model.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var should = require('should'), 7 | mongoose = require('mongoose'), 8 | User = mongoose.model('User'); 9 | 10 | /** 11 | * Globals 12 | */ 13 | var user, user2; 14 | 15 | /** 16 | * Unit tests 17 | */ 18 | describe('User Model Unit Tests:', function() { 19 | before(function(done) { 20 | user = new User({ 21 | firstName: 'Full', 22 | lastName: 'Name', 23 | displayName: 'Full Name', 24 | email: 'test@test.com', 25 | username: 'username', 26 | password: 'password', 27 | provider: 'local' 28 | }); 29 | user2 = new User({ 30 | firstName: 'Full', 31 | lastName: 'Name', 32 | displayName: 'Full Name', 33 | email: 'test@test.com', 34 | username: 'username', 35 | password: 'password', 36 | provider: 'local' 37 | }); 38 | 39 | done(); 40 | }); 41 | 42 | describe('Method Save', function() { 43 | it('should begin with no users', function(done) { 44 | User.find({}, function(err, users) { 45 | users.should.have.length(0); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should be able to save without problems', function(done) { 51 | user.save(done); 52 | }); 53 | 54 | it('should fail to save an existing user again', function(done) { 55 | user.save(); 56 | return user2.save(function(err) { 57 | should.exist(err); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('should be able to show an error when try to save without first name', function(done) { 63 | user.firstName = ''; 64 | return user.save(function(err) { 65 | should.exist(err); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | 71 | after(function(done) { 72 | User.remove().exec(); 73 | done(); 74 | }); 75 | }); -------------------------------------------------------------------------------- /public/modules/users/views/settings/edit-profile.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Edit your profile

3 |
4 | 33 |
34 |
-------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. 3 | "browser": true, // Standard browser globals e.g. `window`, `document`. 4 | "esnext": true, // Allow ES.next specific features such as `const` and `let`. 5 | "bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.). 6 | "camelcase": false, // Permit only camelcase for `var` and `object indexes`. 7 | "curly": false, // Require {} for every new block or scope. 8 | "eqeqeq": true, // Require triple equals i.e. `===`. 9 | "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 10 | "latedef": true, // Prohibit variable use before definition. 11 | "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. 12 | "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. 13 | "quotmark": "single", // Define quotes to string values. 14 | "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. 15 | "undef": true, // Require all non-global variables be declared before they are used. 16 | "unused": false, // Warn unused variables. 17 | "strict": true, // Require `use strict` pragma in every file. 18 | "trailing": true, // Prohibit trailing whitespaces. 19 | "smarttabs": false, // Suppresses warnings about mixed tabs and spaces 20 | "globals": { // Globals variables. 21 | "jasmine": true, 22 | "angular": true, 23 | "ApplicationConfiguration": true 24 | }, 25 | "predef": [ // Extra globals. 26 | "define", 27 | "require", 28 | "exports", 29 | "module", 30 | "describe", 31 | "before", 32 | "beforeEach", 33 | "after", 34 | "afterEach", 35 | "it", 36 | "inject", 37 | "expect" 38 | ], 39 | "indent": 4, // Specify indentation spacing 40 | "devel": true, // Allow development statements e.g. `console.log();`. 41 | "noempty": true // Prohibit use of empty blocks. 42 | } -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var _ = require('lodash'), 7 | glob = require('glob'); 8 | 9 | /** 10 | * Load app configurations 11 | */ 12 | module.exports = _.extend( 13 | require('./env/all'), 14 | require('./env/' + process.env.NODE_ENV) || {} 15 | ); 16 | 17 | /** 18 | * Get files by glob patterns 19 | */ 20 | module.exports.getGlobbedFiles = function(globPatterns, removeRoot) { 21 | // For context switching 22 | var _this = this; 23 | 24 | // URL paths regex 25 | var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i'); 26 | 27 | // The output array 28 | var output = []; 29 | 30 | // If glob pattern is array so we use each pattern in a recursive way, otherwise we use glob 31 | if (_.isArray(globPatterns)) { 32 | globPatterns.forEach(function(globPattern) { 33 | output = _.union(output, _this.getGlobbedFiles(globPattern, removeRoot)); 34 | }); 35 | } else if (_.isString(globPatterns)) { 36 | if (urlRegex.test(globPatterns)) { 37 | output.push(globPatterns); 38 | } else { 39 | glob(globPatterns, { 40 | sync: true 41 | }, function(err, files) { 42 | if (removeRoot) { 43 | files = files.map(function(file) { 44 | return file.replace(removeRoot, ''); 45 | }); 46 | } 47 | 48 | output = _.union(output, files); 49 | }); 50 | } 51 | } 52 | 53 | return output; 54 | }; 55 | 56 | /** 57 | * Get the modules JavaScript files 58 | */ 59 | module.exports.getJavaScriptAssets = function(includeTests) { 60 | var output = this.getGlobbedFiles(this.assets.lib.js.concat(this.assets.js), 'public/'); 61 | 62 | // To include tests 63 | if (includeTests) { 64 | output = _.union(output, this.getGlobbedFiles(this.assets.tests)); 65 | } 66 | 67 | return output; 68 | }; 69 | 70 | /** 71 | * Get the modules CSS files 72 | */ 73 | module.exports.getCSSAssets = function() { 74 | var output = this.getGlobbedFiles(this.assets.lib.css.concat(this.assets.css), 'public/'); 75 | return output; 76 | }; -------------------------------------------------------------------------------- /public/modules/users/views/authentication/signin.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Sign in using your social accounts

3 | 20 |

Or with your account

21 |
22 | 44 |
45 |
-------------------------------------------------------------------------------- /app/routes/users.server.routes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var passport = require('passport'); 7 | 8 | module.exports = function(app) { 9 | // User Routes 10 | var users = require('../../app/controllers/users.server.controller'); 11 | 12 | // Setting up the users profile api 13 | app.route('/users/me').get(users.me); 14 | app.route('/users').put(users.update); 15 | app.route('/users/accounts').delete(users.removeOAuthProvider); 16 | 17 | // Setting up the users password api 18 | app.route('/users/password').post(users.changePassword); 19 | app.route('/auth/forgot').post(users.forgot); 20 | app.route('/auth/reset/:token').get(users.validateResetToken); 21 | app.route('/auth/reset/:token').post(users.reset); 22 | 23 | // Setting up the users authentication api 24 | app.route('/auth/signup').post(users.signup); 25 | app.route('/auth/signin').post(users.signin); 26 | app.route('/auth/signout').get(users.signout); 27 | 28 | // Setting the facebook oauth routes 29 | app.route('/auth/facebook').get(passport.authenticate('facebook', { 30 | scope: ['email'] 31 | })); 32 | app.route('/auth/facebook/callback').get(users.oauthCallback('facebook')); 33 | 34 | // Setting the twitter oauth routes 35 | app.route('/auth/twitter').get(passport.authenticate('twitter')); 36 | app.route('/auth/twitter/callback').get(users.oauthCallback('twitter')); 37 | 38 | // Setting the google oauth routes 39 | app.route('/auth/google').get(passport.authenticate('google', { 40 | scope: [ 41 | 'https://www.googleapis.com/auth/userinfo.profile', 42 | 'https://www.googleapis.com/auth/userinfo.email' 43 | ] 44 | })); 45 | app.route('/auth/google/callback').get(users.oauthCallback('google')); 46 | 47 | // Setting the linkedin oauth routes 48 | app.route('/auth/linkedin').get(passport.authenticate('linkedin')); 49 | app.route('/auth/linkedin/callback').get(users.oauthCallback('linkedin')); 50 | 51 | // Setting the github oauth routes 52 | app.route('/auth/github').get(passport.authenticate('github')); 53 | app.route('/auth/github/callback').get(users.oauthCallback('github')); 54 | 55 | // Finish by binding the user middleware 56 | app.param('userId', users.userByID); 57 | }; -------------------------------------------------------------------------------- /config/env/secure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | port: 443, 5 | db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://localhost/northwindnode', 6 | assets: { 7 | lib: { 8 | css: [ 9 | 'public/lib/bootstrap/dist/css/bootstrap.min.css', 10 | 'public/lib/bootstrap/dist/css/bootstrap-theme.min.css', 11 | ], 12 | js: [ 13 | 'public/lib/angular/angular.min.js', 14 | 'public/lib/angular-resource/angular-resource.min.js', 15 | 'public/lib/angular-animate/angular-animate.min.js', 16 | 'public/lib/angular-ui-router/release/angular-ui-router.min.js', 17 | 'public/lib/angular-ui-utils/ui-utils.min.js', 18 | 'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js' 19 | ] 20 | }, 21 | css: 'public/dist/application.min.css', 22 | js: 'public/dist/application.min.js' 23 | }, 24 | facebook: { 25 | clientID: process.env.FACEBOOK_ID || 'APP_ID', 26 | clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', 27 | callbackURL: 'https://localhost:443/auth/facebook/callback' 28 | }, 29 | twitter: { 30 | clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', 31 | clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', 32 | callbackURL: 'https://localhost:443/auth/twitter/callback' 33 | }, 34 | google: { 35 | clientID: process.env.GOOGLE_ID || 'APP_ID', 36 | clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', 37 | callbackURL: 'https://localhost:443/auth/google/callback' 38 | }, 39 | linkedin: { 40 | clientID: process.env.LINKEDIN_ID || 'APP_ID', 41 | clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', 42 | callbackURL: 'https://localhost:443/auth/linkedin/callback' 43 | }, 44 | github: { 45 | clientID: process.env.GITHUB_ID || 'APP_ID', 46 | clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', 47 | callbackURL: 'https://localhost:443/auth/github/callback' 48 | }, 49 | mailer: { 50 | from: process.env.MAILER_FROM || 'MAILER_FROM', 51 | options: { 52 | service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', 53 | auth: { 54 | user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', 55 | pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' 56 | } 57 | } 58 | } 59 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "northwindnode", 3 | "description": "Full-Stack JavaScript with MongoDB, Express, AngularJS, and Node.js", 4 | "version": "0.0.1", 5 | "author": "Bradoncode.com", 6 | "engines": { 7 | "node": "0.10.x", 8 | "npm": "1.4.x" 9 | }, 10 | "scripts": { 11 | "start": "grunt", 12 | "test": "grunt test", 13 | "postinstall": "bower install --config.interactive=false" 14 | }, 15 | "dependencies": { 16 | "async": "~0.9.0", 17 | "body-parser": "~1.9.0", 18 | "bower": "~1.3.8", 19 | "chalk": "~0.5", 20 | "compression": "~1.2.0", 21 | "connect-flash": "~0.1.1", 22 | "connect-mongo": "~0.4.1", 23 | "consolidate": "~0.10.0", 24 | "cookie-parser": "~1.3.2", 25 | "express": "~4.10.1", 26 | "express-session": "~1.9.1", 27 | "forever": "~0.11.0", 28 | "glob": "~4.0.5", 29 | "grunt-cli": "~0.1.13", 30 | "helmet": "~0.5.0", 31 | "lodash": "~2.4.1", 32 | "method-override": "~2.3.0", 33 | "mongoose": "~3.8.8", 34 | "morgan": "~1.4.1", 35 | "nodemailer": "~1.3.0", 36 | "passport": "~0.2.0", 37 | "passport-facebook": "~1.0.2", 38 | "passport-github": "~0.1.5", 39 | "passport-google-oauth": "~0.1.5", 40 | "passport-http": "^0.2.2", 41 | "passport-linkedin": "~0.1.3", 42 | "passport-local": "~1.0.0", 43 | "passport-twitter": "~1.0.2", 44 | "swig": "~1.4.1" 45 | }, 46 | "devDependencies": { 47 | "supertest": "~0.14.0", 48 | "should": "~4.1.0", 49 | "grunt-env": "~0.4.1", 50 | "grunt-node-inspector": "~0.1.3", 51 | "grunt-contrib-watch": "~0.6.1", 52 | "grunt-contrib-jshint": "~0.10.0", 53 | "grunt-contrib-csslint": "^0.3.1", 54 | "grunt-ng-annotate": "~0.4.0", 55 | "grunt-contrib-uglify": "~0.6.0", 56 | "grunt-contrib-cssmin": "~0.10.0", 57 | "grunt-nodemon": "~0.3.0", 58 | "grunt-concurrent": "~1.0.0", 59 | "grunt-mocha-test": "~0.12.1", 60 | "grunt-karma": "~0.9.0", 61 | "load-grunt-tasks": "~1.0.0", 62 | "karma": "~0.12.0", 63 | "karma-jasmine": "~0.2.1", 64 | "karma-coverage": "~0.2.0", 65 | "karma-chrome-launcher": "~0.1.2", 66 | "karma-firefox-launcher": "~0.1.3", 67 | "karma-phantomjs-launcher": "~0.1.2", 68 | "migrate": "^0.2.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /config/env/production.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | db: process.env.MONGOHQ_URL || process.env.MONGOLAB_URI || 'mongodb://' + (process.env.DB_1_PORT_27017_TCP_ADDR || 'localhost') + '/northwindnode', 5 | assets: { 6 | lib: { 7 | css: [ 8 | 'public/lib/bootstrap/dist/css/bootstrap.min.css', 9 | 'public/lib/bootstrap/dist/css/bootstrap-theme.min.css', 10 | ], 11 | js: [ 12 | 'public/lib/angular/angular.min.js', 13 | 'public/lib/angular-resource/angular-resource.js', 14 | 'public/lib/angular-cookies/angular-cookies.js', 15 | 'public/lib/angular-animate/angular-animate.js', 16 | 'public/lib/angular-touch/angular-touch.js', 17 | 'public/lib/angular-sanitize/angular-sanitize.js', 18 | 'public/lib/angular-ui-router/release/angular-ui-router.min.js', 19 | 'public/lib/angular-ui-utils/ui-utils.min.js', 20 | 'public/lib/angular-bootstrap/ui-bootstrap-tpls.min.js' 21 | ] 22 | }, 23 | css: 'public/dist/application.min.css', 24 | js: 'public/dist/application.min.js' 25 | }, 26 | facebook: { 27 | clientID: process.env.FACEBOOK_ID || 'APP_ID', 28 | clientSecret: process.env.FACEBOOK_SECRET || 'APP_SECRET', 29 | callbackURL: '/auth/facebook/callback' 30 | }, 31 | twitter: { 32 | clientID: process.env.TWITTER_KEY || 'CONSUMER_KEY', 33 | clientSecret: process.env.TWITTER_SECRET || 'CONSUMER_SECRET', 34 | callbackURL: '/auth/twitter/callback' 35 | }, 36 | google: { 37 | clientID: process.env.GOOGLE_ID || 'APP_ID', 38 | clientSecret: process.env.GOOGLE_SECRET || 'APP_SECRET', 39 | callbackURL: '/auth/google/callback' 40 | }, 41 | linkedin: { 42 | clientID: process.env.LINKEDIN_ID || 'APP_ID', 43 | clientSecret: process.env.LINKEDIN_SECRET || 'APP_SECRET', 44 | callbackURL: '/auth/linkedin/callback' 45 | }, 46 | github: { 47 | clientID: process.env.GITHUB_ID || 'APP_ID', 48 | clientSecret: process.env.GITHUB_SECRET || 'APP_SECRET', 49 | callbackURL: '/auth/github/callback' 50 | }, 51 | mailer: { 52 | from: process.env.MAILER_FROM || 'MAILER_FROM', 53 | options: { 54 | service: process.env.MAILER_SERVICE_PROVIDER || 'MAILER_SERVICE_PROVIDER', 55 | auth: { 56 | user: process.env.MAILER_EMAIL_ID || 'MAILER_EMAIL_ID', 57 | pass: process.env.MAILER_PASSWORD || 'MAILER_PASSWORD' 58 | } 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /public/modules/categories/controllers/categories.client.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Categories controller 4 | angular.module('categories').controller('CategoriesController', ['$scope', '$stateParams', '$location', 'Authentication', 'Categories', 5 | function($scope, $stateParams, $location, Authentication, Categories) { 6 | $scope.authentication = Authentication; 7 | $scope.currentPage = 1; 8 | $scope.pageSize = 10; 9 | $scope.offset = 0; 10 | 11 | // Page changed handler 12 | $scope.pageChanged = function() { 13 | $scope.offset = ($scope.currentPage - 1) * $scope.pageSize; 14 | }; 15 | 16 | // Create new Category 17 | $scope.create = function() { 18 | // Create new Category object 19 | var category = new Categories ({ 20 | name: this.name, 21 | description: this.description 22 | }); 23 | 24 | // Redirect after save 25 | category.$save(function(response) { 26 | $location.path('categories/' + response._id); 27 | 28 | // Clear form fields 29 | $scope.name = ''; 30 | }, function(errorResponse) { 31 | $scope.error = errorResponse.data.message; 32 | }); 33 | }; 34 | 35 | // Remove existing Category 36 | $scope.remove = function(category) { 37 | if ( category ) { 38 | category.$remove(); 39 | 40 | for (var i in $scope.categories) { 41 | if ($scope.categories [i] === category) { 42 | $scope.categories.splice(i, 1); 43 | } 44 | } 45 | } else { 46 | $scope.category.$remove(function() { 47 | $location.path('categories'); 48 | }); 49 | } 50 | }; 51 | 52 | // Update existing Category 53 | $scope.update = function() { 54 | var category = $scope.category; 55 | 56 | category.$update(function() { 57 | $location.path('categories/' + category._id); 58 | }, function(errorResponse) { 59 | $scope.error = errorResponse.data.message; 60 | }); 61 | }; 62 | 63 | // Find a list of Categories 64 | $scope.find = function() { 65 | $scope.categories = Categories.query(); 66 | }; 67 | 68 | // Find existing Category 69 | $scope.findOne = function() { 70 | $scope.category = Categories.get({ 71 | categoryId: $stateParams.categoryId 72 | }); 73 | }; 74 | 75 | // Search for a category 76 | $scope.categorySearch = function(product) { 77 | $location.path('categories/' + product._id); 78 | }; 79 | } 80 | ]); 81 | -------------------------------------------------------------------------------- /app/controllers/crud.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | errorHandler = require('./errors.server.controller'), 8 | _ = require('lodash'); 9 | 10 | module.exports = function(modelName, sortBy) { 11 | 12 | var Model = mongoose.model(modelName); 13 | 14 | return { 15 | create: function(req, res) { 16 | var model = new Model(req.body); 17 | 18 | model.save(function(err) { 19 | if (err) { 20 | return res.status(400).send({ 21 | message: errorHandler.getErrorMessage(err) 22 | }); 23 | } else { 24 | res.status(201).json(model); 25 | } 26 | }); 27 | }, 28 | read: function(req, res) { 29 | res.json(req.modelName); 30 | }, 31 | update: function(req, res) { 32 | var model = req.modelName; 33 | 34 | model = _.extend(model, req.body); 35 | 36 | model.save(function(err) { 37 | if (err) { 38 | return res.status(400).send({ 39 | message: errorHandler.getErrorMessage(err) 40 | }); 41 | } else { 42 | res.json(model); 43 | } 44 | }); 45 | }, 46 | delete: function(req, res) { 47 | var model = req.modelName; 48 | 49 | model.remove(function(err) { 50 | if (err) { 51 | return res.status(400).send({ 52 | message: errorHandler.getErrorMessage(err) 53 | }); 54 | } else { 55 | res.json(model); 56 | } 57 | }); 58 | }, 59 | list: function(req, res) { 60 | var query = {}; 61 | if (req.query.filter) { 62 | // TODO: extend this to handle multiple filters 63 | query = req.query.filter; 64 | } 65 | 66 | Model.find(query).sort(sortBy).exec(function(err, models) { 67 | if (err) { 68 | return res.status(400).send({ 69 | message: errorHandler.getErrorMessage(err) 70 | }); 71 | } else { 72 | res.json(models); 73 | } 74 | }); 75 | }, 76 | getByID: function(req, res, next, id) { 77 | if (!mongoose.Types.ObjectId.isValid(id)) { 78 | return res.status(400).send({ 79 | message: modelName + ' is invalid' 80 | }); 81 | } 82 | 83 | Model.findById(id).exec(function(err, model) { 84 | if (err) return next(err); 85 | if (!model) { 86 | return res.status(404).send({ 87 | message: modelName + ' not found' 88 | }); 89 | } 90 | req.modelName = model; 91 | next(); 92 | }); 93 | } 94 | }; 95 | }; 96 | -------------------------------------------------------------------------------- /public/modules/users/controllers/settings.client.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('users').controller('SettingsController', ['$scope', '$http', '$location', 'Users', 'Authentication', 4 | function($scope, $http, $location, Users, Authentication) { 5 | $scope.user = Authentication.user; 6 | 7 | // If user is not signed in then redirect back home 8 | if (!$scope.user) $location.path('/'); 9 | 10 | // Check if there are additional accounts 11 | $scope.hasConnectedAdditionalSocialAccounts = function(provider) { 12 | for (var i in $scope.user.additionalProvidersData) { 13 | return true; 14 | } 15 | 16 | return false; 17 | }; 18 | 19 | // Check if provider is already in use with current user 20 | $scope.isConnectedSocialAccount = function(provider) { 21 | return $scope.user.provider === provider || ($scope.user.additionalProvidersData && $scope.user.additionalProvidersData[provider]); 22 | }; 23 | 24 | // Remove a user social account 25 | $scope.removeUserSocialAccount = function(provider) { 26 | $scope.success = $scope.error = null; 27 | 28 | $http.delete('/users/accounts', { 29 | params: { 30 | provider: provider 31 | } 32 | }).success(function(response) { 33 | // If successful show success message and clear form 34 | $scope.success = true; 35 | $scope.user = Authentication.user = response; 36 | }).error(function(response) { 37 | $scope.error = response.message; 38 | }); 39 | }; 40 | 41 | // Update a user profile 42 | $scope.updateUserProfile = function(isValid) { 43 | if (isValid) { 44 | $scope.success = $scope.error = null; 45 | var user = new Users($scope.user); 46 | 47 | user.$update(function(response) { 48 | $scope.success = true; 49 | Authentication.user = response; 50 | }, function(response) { 51 | $scope.error = response.data.message; 52 | }); 53 | } else { 54 | $scope.submitted = true; 55 | } 56 | }; 57 | 58 | // Change user password 59 | $scope.changeUserPassword = function() { 60 | $scope.success = $scope.error = null; 61 | 62 | $http.post('/users/password', $scope.passwordDetails).success(function(response) { 63 | // If successful show success message and clear form 64 | $scope.success = true; 65 | $scope.passwordDetails = null; 66 | }).error(function(response) { 67 | $scope.error = response.message; 68 | }); 69 | }; 70 | } 71 | ]); -------------------------------------------------------------------------------- /app/views/layout.server.view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {% for cssFile in cssFiles %}{% endfor %} 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 |
49 |
50 | {% block content %}{% endblock %} 51 |
52 |
53 | 54 | 55 | 58 | 59 | 60 | {% for jsFile in jsFiles %}{% endfor %} 61 | 62 | {% if process.env.NODE_ENV === 'development' %} 63 | 64 | 65 | {% endif %} 66 | 67 | 68 | -------------------------------------------------------------------------------- /public/modules/users/views/authentication/signup.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |

Sign up using your social accounts

3 | 20 |

Or with your email

21 |
22 | 53 |
54 |
-------------------------------------------------------------------------------- /public/modules/core/views/header.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 11 | 58 |
59 | -------------------------------------------------------------------------------- /public/modules/products/controllers/products.client.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Products controller 4 | angular.module('products').controller('ProductsController', ['$scope', '$stateParams', '$location', 'Authentication', 'Products', 'Categories', '$filter', 5 | function($scope, $stateParams, $location, Authentication, Products, Categories, $filter) { 6 | $scope.authentication = Authentication; 7 | $scope.categories = Categories.query(); 8 | $scope.currentPage = 1; 9 | $scope.pageSize = 10; 10 | $scope.offset = 0; 11 | 12 | // Page changed handler 13 | $scope.pageChanged = function() { 14 | $scope.offset = ($scope.currentPage - 1) * $scope.pageSize; 15 | }; 16 | 17 | // Create new Product 18 | $scope.create = function() { 19 | var product = new Products ({ 20 | name: this.name, 21 | category: this.category, 22 | quantityPerUnit: this.quantityPerUnit, 23 | unitPrice: this.unitPrice, 24 | unitsInStock: this.unitsInStock, 25 | unitsOnOrder: this.unitsOnOrder 26 | }); 27 | 28 | // Redirect after save 29 | product.$save(function(response) { 30 | $location.path('products/' + response._id); 31 | 32 | // Clear form fields 33 | $scope.name = ''; 34 | }, function(errorResponse) { 35 | $scope.error = errorResponse.data.message; 36 | }); 37 | }; 38 | 39 | // Remove existing Product 40 | $scope.remove = function(product) { 41 | if ( product ) { 42 | product.$remove(); 43 | 44 | for (var i in $scope.products) { 45 | if ($scope.products [i] === product) { 46 | $scope.products.splice(i, 1); 47 | } 48 | } 49 | } else { 50 | $scope.product.$remove(function() { 51 | $location.path('products'); 52 | }); 53 | } 54 | }; 55 | 56 | // Update existing Product 57 | $scope.update = function() { 58 | var product = $scope.product; 59 | product.category = product.category._id; 60 | 61 | product.$update(function() { 62 | $location.path('products/' + product._id); 63 | }, function(errorResponse) { 64 | $scope.error = errorResponse.data.message; 65 | }); 66 | }; 67 | 68 | var appendCategory = function appendCategory(p) { 69 | // You could substitue use of filter here with underscore etc. 70 | p.category = $filter('filter')($scope.categories, {_id: p.category})[0]; 71 | }; 72 | 73 | // Find a list of Products 74 | $scope.find = function() { 75 | Products.query(function loadedProducts(products) { 76 | products.forEach(appendCategory); 77 | $scope.products = products; 78 | }); 79 | }; 80 | 81 | // Find existing Product 82 | $scope.findOne = function() { 83 | $scope.product = Products.get({ 84 | productId: $stateParams.productId 85 | }, appendCategory); 86 | }; 87 | 88 | // Search for a product 89 | $scope.productSearch = function(product) { 90 | $location.path('products/' + product._id); 91 | }; 92 | } 93 | ]); 94 | -------------------------------------------------------------------------------- /public/modules/core/views/home.client.view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | northwind 6 |
7 |
8 |
9 |
10 |

11 | Welcome to the Northwind Node Example App. 12 |

13 |
14 |
15 |

16 | See the full tutorial 17 |

18 |
19 |
20 |
21 |

Congrats! You've configured and run the sample application.

22 |

This app makes use of the MEAN.JS stack for creating a web application boilerplate.

23 |

This sample application has registration and login, category management and product search.

24 |
    25 |
  • 26 | Click 27 | Signup 28 | to get started. 29 |
  • 30 |
  • 31 | Configure your app to work with your social accounts, by editing the 32 | /config/env/*.js 33 | files. 34 |
  • 35 |
  • Now you can edit Categories and Products.
  • 36 |
37 |
38 |
39 |
40 |

41 | MongoDB 42 |

43 |

MongoDB is a database. MongoDB's great manual is the place to get started with NoSQL and MongoDB.

44 |
45 |
46 |

47 | Express 48 |

49 |

Express is an app server. Check out The Express Guide or StackOverflow for more info.

50 |
51 |
52 |

53 | AngularJS 54 |

55 |

AngularJS is web app framework. Angular's website offers a lot. The Thinkster Popular Guide and Egghead Videos are great resources.

56 |
57 |
58 |

59 | Node.js 60 |

61 |

Node.js is a web server. Node's website and this stackOverflow thread offer excellent starting points to get to grasps with node.

62 |
63 |
64 |
65 | -------------------------------------------------------------------------------- /public/modules/products/views/create-product.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 |
45 | 46 |
47 | 48 |
49 |
50 |
51 | 52 |
53 |
54 | 55 |
56 |
57 |
58 |
59 |
60 | -------------------------------------------------------------------------------- /public/modules/products/views/edit-product.client.view.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 |
41 | 42 |
43 |
44 |
45 | 46 |
47 | 48 |
49 |
50 | 51 |
52 | 53 |
54 |
55 | 56 |
57 |
58 |
59 |
60 |
61 | -------------------------------------------------------------------------------- /app/models/user.server.model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | crypto = require('crypto'); 9 | 10 | /** 11 | * A Validation function for local strategy properties 12 | */ 13 | var validateLocalStrategyProperty = function(property) { 14 | return ((this.provider !== 'local' && !this.updated) || property.length); 15 | }; 16 | 17 | /** 18 | * A Validation function for local strategy password 19 | */ 20 | var validateLocalStrategyPassword = function(password) { 21 | return (this.provider !== 'local' || (password && password.length > 6)); 22 | }; 23 | 24 | /** 25 | * User Schema 26 | */ 27 | var UserSchema = new Schema({ 28 | firstName: { 29 | type: String, 30 | trim: true, 31 | default: '', 32 | validate: [validateLocalStrategyProperty, 'Please fill in your first name'] 33 | }, 34 | lastName: { 35 | type: String, 36 | trim: true, 37 | default: '', 38 | validate: [validateLocalStrategyProperty, 'Please fill in your last name'] 39 | }, 40 | displayName: { 41 | type: String, 42 | trim: true 43 | }, 44 | email: { 45 | type: String, 46 | trim: true, 47 | default: '', 48 | validate: [validateLocalStrategyProperty, 'Please fill in your email'], 49 | match: [/.+\@.+\..+/, 'Please fill a valid email address'] 50 | }, 51 | username: { 52 | type: String, 53 | unique: 'testing error message', 54 | required: 'Please fill in a username', 55 | trim: true 56 | }, 57 | password: { 58 | type: String, 59 | default: '', 60 | validate: [validateLocalStrategyPassword, 'Password should be longer'] 61 | }, 62 | salt: { 63 | type: String 64 | }, 65 | provider: { 66 | type: String, 67 | required: 'Provider is required' 68 | }, 69 | providerData: {}, 70 | additionalProvidersData: {}, 71 | roles: { 72 | type: [{ 73 | type: String, 74 | enum: ['user', 'admin'] 75 | }], 76 | default: ['user'] 77 | }, 78 | updated: { 79 | type: Date 80 | }, 81 | created: { 82 | type: Date, 83 | default: Date.now 84 | }, 85 | /* For reset password */ 86 | resetPasswordToken: { 87 | type: String 88 | }, 89 | resetPasswordExpires: { 90 | type: Date 91 | } 92 | }); 93 | 94 | /** 95 | * Hook a pre save method to hash the password 96 | */ 97 | UserSchema.pre('save', function(next) { 98 | if (this.password && this.password.length > 6) { 99 | this.salt = new Buffer(crypto.randomBytes(16).toString('base64'), 'base64'); 100 | this.password = this.hashPassword(this.password); 101 | } 102 | 103 | next(); 104 | }); 105 | 106 | /** 107 | * Create instance method for hashing a password 108 | */ 109 | UserSchema.methods.hashPassword = function(password) { 110 | if (this.salt && password) { 111 | return crypto.pbkdf2Sync(password, this.salt, 10000, 64).toString('base64'); 112 | } else { 113 | return password; 114 | } 115 | }; 116 | 117 | /** 118 | * Create instance method for authenticating user 119 | */ 120 | UserSchema.methods.authenticate = function(password) { 121 | return this.password === this.hashPassword(password); 122 | }; 123 | 124 | /** 125 | * Find possible not used username 126 | */ 127 | UserSchema.statics.findUniqueUsername = function(username, suffix, callback) { 128 | var _this = this; 129 | var possibleUsername = username + (suffix || ''); 130 | 131 | _this.findOne({ 132 | username: possibleUsername 133 | }, function(err, user) { 134 | if (!err) { 135 | if (!user) { 136 | callback(possibleUsername); 137 | } else { 138 | return _this.findUniqueUsername(username, (suffix || 0) + 1, callback); 139 | } 140 | } else { 141 | callback(null); 142 | } 143 | }); 144 | }; 145 | 146 | mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /public/modules/users/tests/authentication.client.controller.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | // Authentication controller Spec 5 | describe('AuthenticationController', function() { 6 | // Initialize global variables 7 | var AuthenticationController, 8 | scope, 9 | $httpBackend, 10 | $stateParams, 11 | $location; 12 | 13 | beforeEach(function() { 14 | jasmine.addMatchers({ 15 | toEqualData: function(util, customEqualityTesters) { 16 | return { 17 | compare: function(actual, expected) { 18 | return { 19 | pass: angular.equals(actual, expected) 20 | }; 21 | } 22 | }; 23 | } 24 | }); 25 | }); 26 | 27 | // Load the main application module 28 | beforeEach(module(ApplicationConfiguration.applicationModuleName)); 29 | 30 | // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). 31 | // This allows us to inject a service but then attach it to a variable 32 | // with the same name as the service. 33 | beforeEach(inject(function($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_) { 34 | // Set a new global scope 35 | scope = $rootScope.$new(); 36 | 37 | // Point global variables to injected services 38 | $stateParams = _$stateParams_; 39 | $httpBackend = _$httpBackend_; 40 | $location = _$location_; 41 | 42 | // Initialize the Authentication controller 43 | AuthenticationController = $controller('AuthenticationController', { 44 | $scope: scope 45 | }); 46 | })); 47 | 48 | 49 | it('$scope.signin() should login with a correct user and password', function() { 50 | // Test expected GET request 51 | $httpBackend.when('POST', '/auth/signin').respond(200, 'Fred'); 52 | 53 | scope.signin(); 54 | $httpBackend.flush(); 55 | 56 | // Test scope value 57 | expect(scope.authentication.user).toEqual('Fred'); 58 | expect($location.url()).toEqual('/'); 59 | }); 60 | 61 | it('$scope.signin() should fail to log in with nothing', function() { 62 | // Test expected POST request 63 | $httpBackend.expectPOST('/auth/signin').respond(400, { 64 | 'message': 'Missing credentials' 65 | }); 66 | 67 | scope.signin(); 68 | $httpBackend.flush(); 69 | 70 | // Test scope value 71 | expect(scope.error).toEqual('Missing credentials'); 72 | }); 73 | 74 | it('$scope.signin() should fail to log in with wrong credentials', function() { 75 | // Foo/Bar combo assumed to not exist 76 | scope.authentication.user = 'Foo'; 77 | scope.credentials = 'Bar'; 78 | 79 | // Test expected POST request 80 | $httpBackend.expectPOST('/auth/signin').respond(400, { 81 | 'message': 'Unknown user' 82 | }); 83 | 84 | scope.signin(); 85 | $httpBackend.flush(); 86 | 87 | // Test scope value 88 | expect(scope.error).toEqual('Unknown user'); 89 | }); 90 | 91 | it('$scope.signup() should register with correct data', function() { 92 | // Test expected GET request 93 | scope.authentication.user = 'Fred'; 94 | $httpBackend.when('POST', '/auth/signup').respond(200, 'Fred'); 95 | 96 | scope.signup(); 97 | $httpBackend.flush(); 98 | 99 | // test scope value 100 | expect(scope.authentication.user).toBe('Fred'); 101 | expect(scope.error).toEqual(undefined); 102 | expect($location.url()).toBe('/'); 103 | }); 104 | 105 | it('$scope.signup() should fail to register with duplicate Username', function() { 106 | // Test expected POST request 107 | $httpBackend.when('POST', '/auth/signup').respond(400, { 108 | 'message': 'Username already exists' 109 | }); 110 | 111 | scope.signup(); 112 | $httpBackend.flush(); 113 | 114 | // Test scope value 115 | expect(scope.error).toBe('Username already exists'); 116 | }); 117 | }); 118 | }()); 119 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | // Unified Watch Object 5 | var watchFiles = { 6 | serverViews: ['app/views/**/*.*'], 7 | serverJS: ['gruntfile.js', 'server.js', 'config/**/*.js', 'app/**/*.js'], 8 | clientViews: ['public/modules/**/views/**/*.html'], 9 | clientJS: ['public/js/*.js', 'public/modules/**/*.js'], 10 | clientCSS: ['public/modules/**/*.css'], 11 | mochaTests: ['app/tests/**/*.js'] 12 | }; 13 | 14 | // Project Configuration 15 | grunt.initConfig({ 16 | pkg: grunt.file.readJSON('package.json'), 17 | watch: { 18 | serverViews: { 19 | files: watchFiles.serverViews, 20 | options: { 21 | livereload: true 22 | } 23 | }, 24 | serverJS: { 25 | files: watchFiles.serverJS, 26 | tasks: ['jshint'], 27 | options: { 28 | livereload: true 29 | } 30 | }, 31 | clientViews: { 32 | files: watchFiles.clientViews, 33 | options: { 34 | livereload: true, 35 | } 36 | }, 37 | clientJS: { 38 | files: watchFiles.clientJS, 39 | tasks: ['jshint'], 40 | options: { 41 | livereload: true 42 | } 43 | }, 44 | clientCSS: { 45 | files: watchFiles.clientCSS, 46 | tasks: ['csslint'], 47 | options: { 48 | livereload: true 49 | } 50 | } 51 | }, 52 | jshint: { 53 | all: { 54 | src: watchFiles.clientJS.concat(watchFiles.serverJS), 55 | options: { 56 | jshintrc: true 57 | } 58 | } 59 | }, 60 | csslint: { 61 | options: { 62 | csslintrc: '.csslintrc', 63 | }, 64 | all: { 65 | src: watchFiles.clientCSS 66 | } 67 | }, 68 | uglify: { 69 | production: { 70 | options: { 71 | mangle: false 72 | }, 73 | files: { 74 | 'public/dist/application.min.js': 'public/dist/application.js' 75 | } 76 | } 77 | }, 78 | cssmin: { 79 | combine: { 80 | files: { 81 | 'public/dist/application.min.css': '<%= applicationCSSFiles %>' 82 | } 83 | } 84 | }, 85 | nodemon: { 86 | dev: { 87 | script: 'server.js', 88 | options: { 89 | nodeArgs: ['--debug'], 90 | ext: 'js,html', 91 | watch: watchFiles.serverViews.concat(watchFiles.serverJS) 92 | } 93 | } 94 | }, 95 | 'node-inspector': { 96 | custom: { 97 | options: { 98 | 'web-port': 1337, 99 | 'web-host': 'localhost', 100 | 'debug-port': 5858, 101 | 'save-live-edit': true, 102 | 'no-preload': true, 103 | 'stack-trace-limit': 50, 104 | 'hidden': [] 105 | } 106 | } 107 | }, 108 | ngAnnotate: { 109 | production: { 110 | files: { 111 | 'public/dist/application.js': '<%= applicationJavaScriptFiles %>' 112 | } 113 | } 114 | }, 115 | concurrent: { 116 | default: ['nodemon', 'watch'], 117 | debug: ['nodemon', 'watch', 'node-inspector'], 118 | options: { 119 | logConcurrentOutput: true, 120 | limit: 10 121 | } 122 | }, 123 | env: { 124 | test: { 125 | NODE_ENV: 'test' 126 | }, 127 | secure: { 128 | NODE_ENV: 'secure' 129 | } 130 | }, 131 | mochaTest: { 132 | src: watchFiles.mochaTests, 133 | options: { 134 | reporter: 'spec', 135 | require: 'server.js' 136 | } 137 | }, 138 | karma: { 139 | unit: { 140 | configFile: 'karma.conf.js' 141 | } 142 | } 143 | }); 144 | 145 | // Load NPM tasks 146 | require('load-grunt-tasks')(grunt); 147 | 148 | // Making grunt default to force in order not to break the project. 149 | grunt.option('force', true); 150 | 151 | // A Task for loading the configuration object 152 | grunt.task.registerTask('loadConfig', 'Task that loads the config into a grunt option.', function() { 153 | var init = require('./config/init')(); 154 | var config = require('./config/config'); 155 | 156 | grunt.config.set('applicationJavaScriptFiles', config.assets.js); 157 | grunt.config.set('applicationCSSFiles', config.assets.css); 158 | }); 159 | 160 | // Default task(s). 161 | grunt.registerTask('default', ['lint', 'concurrent:default']); 162 | 163 | // Debug task. 164 | grunt.registerTask('debug', ['lint', 'concurrent:debug']); 165 | 166 | // Secure task(s). 167 | grunt.registerTask('secure', ['env:secure', 'lint', 'concurrent:default']); 168 | 169 | // Lint task(s). 170 | grunt.registerTask('lint', ['jshint', 'csslint']); 171 | 172 | // Build task(s). 173 | grunt.registerTask('build', ['lint', 'loadConfig', 'ngAnnotate', 'uglify', 'cssmin']); 174 | 175 | // Server tests task. 176 | grunt.registerTask('test-server', ['env:test', 'mochaTest']); 177 | 178 | // Client tests task. 179 | grunt.registerTask('test-client', ['env:test', 'karma:unit']); 180 | 181 | // Test task. 182 | grunt.registerTask('test', ['env:test', 'mochaTest', 'karma:unit']); 183 | }; 184 | -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var fs = require('fs'), 7 | http = require('http'), 8 | https = require('https'), 9 | express = require('express'), 10 | morgan = require('morgan'), 11 | bodyParser = require('body-parser'), 12 | session = require('express-session'), 13 | compress = require('compression'), 14 | methodOverride = require('method-override'), 15 | cookieParser = require('cookie-parser'), 16 | helmet = require('helmet'), 17 | passport = require('passport'), 18 | mongoStore = require('connect-mongo')({ 19 | session: session 20 | }), 21 | flash = require('connect-flash'), 22 | config = require('./config'), 23 | consolidate = require('consolidate'), 24 | path = require('path'); 25 | 26 | module.exports = function(db) { 27 | // Initialize express app 28 | var app = express(); 29 | 30 | // Globbing model files 31 | config.getGlobbedFiles('./app/models/**/*.js').forEach(function(modelPath) { 32 | require(path.resolve(modelPath)); 33 | }); 34 | 35 | // Setting application local variables 36 | app.locals.title = config.app.title; 37 | app.locals.description = config.app.description; 38 | app.locals.keywords = config.app.keywords; 39 | app.locals.facebookAppId = config.facebook.clientID; 40 | app.locals.jsFiles = config.getJavaScriptAssets(); 41 | app.locals.cssFiles = config.getCSSAssets(); 42 | 43 | // Passing the request url to environment locals 44 | app.use(function(req, res, next) { 45 | res.locals.url = req.protocol + '://' + req.headers.host + req.url; 46 | next(); 47 | }); 48 | 49 | // Should be placed before express.static 50 | app.use(compress({ 51 | filter: function(req, res) { 52 | return (/json|text|javascript|css/).test(res.getHeader('Content-Type')); 53 | }, 54 | level: 9 55 | })); 56 | 57 | // Showing stack errors 58 | app.set('showStackError', true); 59 | 60 | // Set swig as the template engine 61 | app.engine('server.view.html', consolidate[config.templateEngine]); 62 | 63 | // Set views path and view engine 64 | app.set('view engine', 'server.view.html'); 65 | app.set('views', './app/views'); 66 | 67 | // Environment dependent middleware 68 | if (process.env.NODE_ENV === 'development') { 69 | // Enable logger (morgan) 70 | app.use(morgan('dev')); 71 | 72 | // Disable views cache 73 | app.set('view cache', false); 74 | } else if (process.env.NODE_ENV === 'production') { 75 | app.locals.cache = 'memory'; 76 | } 77 | 78 | // Request body parsing middleware should be above methodOverride 79 | app.use(bodyParser.urlencoded({ 80 | extended: true 81 | })); 82 | app.use(bodyParser.json()); 83 | app.use(methodOverride()); 84 | 85 | // CookieParser should be above session 86 | app.use(cookieParser()); 87 | 88 | // Express MongoDB session storage 89 | app.use(session({ 90 | saveUninitialized: true, 91 | resave: true, 92 | secret: config.sessionSecret, 93 | store: new mongoStore({ 94 | db: db.connection.db, 95 | collection: config.sessionCollection 96 | }) 97 | })); 98 | 99 | // use passport session 100 | app.use(passport.initialize()); 101 | app.use(passport.session()); 102 | 103 | // connect flash for flash messages 104 | app.use(flash()); 105 | 106 | // Use helmet to secure Express headers 107 | app.use(helmet.xframe()); 108 | app.use(helmet.xssFilter()); 109 | app.use(helmet.nosniff()); 110 | app.use(helmet.ienoopen()); 111 | app.disable('x-powered-by'); 112 | 113 | // Setting the app router and static folder 114 | app.use(express.static(path.resolve('./public'))); 115 | 116 | // Globbing routing files 117 | config.getGlobbedFiles('./app/routes/**/*.js').forEach(function(routePath) { 118 | require(path.resolve(routePath))(app); 119 | }); 120 | 121 | // Assume 'not found' in the error msgs is a 404. this is somewhat silly, but valid, you can do whatever you like, set properties, use instanceof etc. 122 | app.use(function(err, req, res, next) { 123 | // If the error object doesn't exists 124 | if (!err) return next(); 125 | 126 | // Log it 127 | console.error(err.stack); 128 | 129 | // Error page 130 | res.status(500).render('500', { 131 | error: err.stack 132 | }); 133 | }); 134 | 135 | // Assume 404 since no middleware responded 136 | app.use(function(req, res) { 137 | res.status(404).render('404', { 138 | url: req.originalUrl, 139 | error: 'Not Found' 140 | }); 141 | }); 142 | 143 | if (process.env.NODE_ENV === 'secure') { 144 | // Log SSL usage 145 | console.log('Securely using https protocol'); 146 | 147 | // Load SSL key and certificate 148 | var privateKey = fs.readFileSync('./config/sslcerts/key.pem', 'utf8'); 149 | var certificate = fs.readFileSync('./config/sslcerts/cert.pem', 'utf8'); 150 | 151 | // Create HTTPS Server 152 | var httpsServer = https.createServer({ 153 | key: privateKey, 154 | cert: certificate 155 | }, app); 156 | 157 | // Return HTTPS server instance 158 | return httpsServer; 159 | } 160 | 161 | // Return Express server instance 162 | return app; 163 | }; 164 | -------------------------------------------------------------------------------- /public/modules/core/services/menus.client.service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //Menu service used for managing menus 4 | angular.module('core').service('Menus', [ 5 | 6 | function() { 7 | // Define a set of default roles 8 | this.defaultRoles = ['*']; 9 | 10 | // Define the menus object 11 | this.menus = {}; 12 | 13 | // A private function for rendering decision 14 | var shouldRender = function(user) { 15 | if (user) { 16 | if (!!~this.roles.indexOf('*')) { 17 | return true; 18 | } else { 19 | for (var userRoleIndex in user.roles) { 20 | for (var roleIndex in this.roles) { 21 | if (this.roles[roleIndex] === user.roles[userRoleIndex]) { 22 | return true; 23 | } 24 | } 25 | } 26 | } 27 | } else { 28 | return this.isPublic; 29 | } 30 | 31 | return false; 32 | }; 33 | 34 | // Validate menu existance 35 | this.validateMenuExistance = function(menuId) { 36 | if (menuId && menuId.length) { 37 | if (this.menus[menuId]) { 38 | return true; 39 | } else { 40 | throw new Error('Menu does not exists'); 41 | } 42 | } else { 43 | throw new Error('MenuId was not provided'); 44 | } 45 | 46 | return false; 47 | }; 48 | 49 | // Get the menu object by menu id 50 | this.getMenu = function(menuId) { 51 | // Validate that the menu exists 52 | this.validateMenuExistance(menuId); 53 | 54 | // Return the menu object 55 | return this.menus[menuId]; 56 | }; 57 | 58 | // Add new menu object by menu id 59 | this.addMenu = function(menuId, isPublic, roles) { 60 | // Create the new menu 61 | this.menus[menuId] = { 62 | isPublic: isPublic || false, 63 | roles: roles || this.defaultRoles, 64 | items: [], 65 | shouldRender: shouldRender 66 | }; 67 | 68 | // Return the menu object 69 | return this.menus[menuId]; 70 | }; 71 | 72 | // Remove existing menu object by menu id 73 | this.removeMenu = function(menuId) { 74 | // Validate that the menu exists 75 | this.validateMenuExistance(menuId); 76 | 77 | // Return the menu object 78 | delete this.menus[menuId]; 79 | }; 80 | 81 | // Add menu item object 82 | this.addMenuItem = function(menuId, menuItemTitle, menuItemURL, menuItemType, menuItemUIRoute, isPublic, roles, position) { 83 | // Validate that the menu exists 84 | this.validateMenuExistance(menuId); 85 | 86 | // Push new menu item 87 | this.menus[menuId].items.push({ 88 | title: menuItemTitle, 89 | link: menuItemURL, 90 | menuItemType: menuItemType || 'item', 91 | menuItemClass: menuItemType, 92 | uiRoute: menuItemUIRoute || ('/' + menuItemURL), 93 | isPublic: ((isPublic === null || typeof isPublic === 'undefined') ? this.menus[menuId].isPublic : isPublic), 94 | roles: ((roles === null || typeof roles === 'undefined') ? this.menus[menuId].roles : roles), 95 | position: position || 0, 96 | items: [], 97 | shouldRender: shouldRender 98 | }); 99 | 100 | // Return the menu object 101 | return this.menus[menuId]; 102 | }; 103 | 104 | // Add submenu item object 105 | this.addSubMenuItem = function(menuId, rootMenuItemURL, menuItemTitle, menuItemURL, menuItemUIRoute, isPublic, roles, position) { 106 | // Validate that the menu exists 107 | this.validateMenuExistance(menuId); 108 | 109 | // Search for menu item 110 | for (var itemIndex in this.menus[menuId].items) { 111 | if (this.menus[menuId].items[itemIndex].link === rootMenuItemURL) { 112 | // Push new submenu item 113 | this.menus[menuId].items[itemIndex].items.push({ 114 | title: menuItemTitle, 115 | link: menuItemURL, 116 | uiRoute: menuItemUIRoute || ('/' + menuItemURL), 117 | isPublic: ((isPublic === null || typeof isPublic === 'undefined') ? this.menus[menuId].items[itemIndex].isPublic : isPublic), 118 | roles: ((roles === null || typeof roles === 'undefined') ? this.menus[menuId].items[itemIndex].roles : roles), 119 | position: position || 0, 120 | shouldRender: shouldRender 121 | }); 122 | } 123 | } 124 | 125 | // Return the menu object 126 | return this.menus[menuId]; 127 | }; 128 | 129 | // Remove existing menu object by menu id 130 | this.removeMenuItem = function(menuId, menuItemURL) { 131 | // Validate that the menu exists 132 | this.validateMenuExistance(menuId); 133 | 134 | // Search for menu item to remove 135 | for (var itemIndex in this.menus[menuId].items) { 136 | if (this.menus[menuId].items[itemIndex].link === menuItemURL) { 137 | this.menus[menuId].items.splice(itemIndex, 1); 138 | } 139 | } 140 | 141 | // Return the menu object 142 | return this.menus[menuId]; 143 | }; 144 | 145 | // Remove existing menu object by menu id 146 | this.removeSubMenuItem = function(menuId, submenuItemURL) { 147 | // Validate that the menu exists 148 | this.validateMenuExistance(menuId); 149 | 150 | // Search for menu item to remove 151 | for (var itemIndex in this.menus[menuId].items) { 152 | for (var subitemIndex in this.menus[menuId].items[itemIndex].items) { 153 | if (this.menus[menuId].items[itemIndex].items[subitemIndex].link === submenuItemURL) { 154 | this.menus[menuId].items[itemIndex].items.splice(subitemIndex, 1); 155 | } 156 | } 157 | } 158 | 159 | // Return the menu object 160 | return this.menus[menuId]; 161 | }; 162 | 163 | //Adding the topbar menu 164 | this.addMenu('topbar'); 165 | } 166 | ]); -------------------------------------------------------------------------------- /public/modules/products/tests/products.client.controller.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | // Products Controller Spec 5 | describe('Products Controller Tests', function() { 6 | // Initialize global variables 7 | var ProductsController, 8 | scope, 9 | $httpBackend, 10 | $stateParams, 11 | $location; 12 | 13 | // The $resource service augments the response object with methods for updating and deleting the resource. 14 | // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match 15 | // the responses exactly. To solve the problem, we define a new toEqualData Jasmine matcher. 16 | // When the toEqualData matcher compares two objects, it takes only object properties into 17 | // account and ignores methods. 18 | beforeEach(function() { 19 | jasmine.addMatchers({ 20 | toEqualData: function(util, customEqualityTesters) { 21 | return { 22 | compare: function(actual, expected) { 23 | return { 24 | pass: angular.equals(actual, expected) 25 | }; 26 | } 27 | }; 28 | } 29 | }); 30 | }); 31 | 32 | // Then we can start by loading the main application module 33 | beforeEach(module(ApplicationConfiguration.applicationModuleName)); 34 | 35 | // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). 36 | // This allows us to inject a service but then attach it to a variable 37 | // with the same name as the service. 38 | beforeEach(inject(function($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_, Categories) { 39 | // Set a new global scope 40 | scope = $rootScope.$new(); 41 | 42 | // Point global variables to injected services 43 | $stateParams = _$stateParams_; 44 | $httpBackend = _$httpBackend_; 45 | $location = _$location_; 46 | 47 | var category = new Categories({ 48 | name: 'Beverages' 49 | }); 50 | 51 | $httpBackend.expectGET('categories').respond([category]); 52 | 53 | // Initialize the Products controller. 54 | ProductsController = $controller('ProductsController', { 55 | $scope: scope 56 | }); 57 | })); 58 | 59 | it('$scope.find() should create an array with at least one Product object fetched from XHR', inject(function(Products) { 60 | // Create sample Product using the Products service 61 | var sampleProduct = new Products({ 62 | name: 'New Product' 63 | }); 64 | 65 | // Create a sample Products array that includes the new Product 66 | var sampleProducts = [sampleProduct]; 67 | 68 | // Set GET response 69 | $httpBackend.expectGET('products').respond(sampleProducts); 70 | 71 | // Run controller functionality 72 | scope.find(); 73 | $httpBackend.flush(); 74 | 75 | // Test scope value 76 | expect(scope.products).toEqualData(sampleProducts); 77 | })); 78 | 79 | it('$scope.findOne() should create an array with one Product object fetched from XHR using a productId URL parameter', inject(function(Products) { 80 | // Define a sample Product object 81 | var sampleProduct = new Products({ 82 | name: 'New Product' 83 | }); 84 | 85 | // Set the URL parameter 86 | $stateParams.productId = '525a8422f6d0f87f0e407a33'; 87 | 88 | // Set GET response 89 | $httpBackend.expectGET(/products\/([0-9a-fA-F]{24})$/).respond(sampleProduct); 90 | 91 | // Run controller functionality 92 | scope.findOne(); 93 | $httpBackend.flush(); 94 | 95 | // Test scope value 96 | expect(scope.product).toEqualData(sampleProduct); 97 | })); 98 | 99 | it('$scope.create() with valid form data should send a POST request with the form input values and then locate to new object URL', inject(function(Products) { 100 | // Create a sample Product object 101 | var sampleProductPostData = new Products({ 102 | name: 'New Product' 103 | }); 104 | 105 | // Create a sample Product response 106 | var sampleProductResponse = new Products({ 107 | _id: '525cf20451979dea2c000001', 108 | name: 'New Product' 109 | }); 110 | 111 | // Fixture mock form input values 112 | scope.name = 'New Product'; 113 | 114 | // Set POST response 115 | $httpBackend.expectPOST('products', sampleProductPostData).respond(sampleProductResponse); 116 | 117 | // Run controller functionality 118 | scope.create(); 119 | $httpBackend.flush(); 120 | 121 | // Test form inputs are reset 122 | expect(scope.name).toEqual(''); 123 | 124 | // Test URL redirection after the Product was created 125 | expect($location.path()).toBe('/products/' + sampleProductResponse._id); 126 | })); 127 | 128 | it('$scope.update() should update a valid Product', inject(function(Products) { 129 | // Define a sample Product put data 130 | var sampleProductPutData = new Products({ 131 | _id: '525cf20451979dea2c000001', 132 | name: 'New Product' 133 | }); 134 | 135 | // Mock Product in scope 136 | scope.product = sampleProductPutData; 137 | 138 | // Set PUT response 139 | $httpBackend.expectPUT(/products\/([0-9a-fA-F]{24})$/).respond(); 140 | 141 | // Run controller functionality 142 | scope.update(); 143 | $httpBackend.flush(); 144 | 145 | // Test URL location to new object 146 | expect($location.path()).toBe('/products/' + sampleProductPutData._id); 147 | })); 148 | 149 | it('$scope.remove() should send a DELETE request with a valid productId and remove the Product from the scope', inject(function(Products) { 150 | // Create new Product object 151 | var sampleProduct = new Products({ 152 | _id: '525a8422f6d0f87f0e407a33' 153 | }); 154 | 155 | // Create new Products array and include the Product 156 | scope.products = [sampleProduct]; 157 | 158 | // Set expected DELETE response 159 | $httpBackend.expectDELETE(/products\/([0-9a-fA-F]{24})$/).respond(204); 160 | 161 | // Run controller functionality 162 | scope.remove(sampleProduct); 163 | $httpBackend.flush(); 164 | 165 | // Test array after successful delete 166 | expect(scope.products.length).toBe(0); 167 | })); 168 | }); 169 | }()); 170 | -------------------------------------------------------------------------------- /public/modules/categories/tests/categories.client.controller.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | // Categories Controller Spec 5 | describe('Categories Controller Tests', function() { 6 | // Initialize global variables 7 | var CategoriesController, 8 | scope, 9 | $httpBackend, 10 | $stateParams, 11 | $location; 12 | 13 | // The $resource service augments the response object with methods for updating and deleting the resource. 14 | // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match 15 | // the responses exactly. To solve the problem, we define a new toEqualData Jasmine matcher. 16 | // When the toEqualData matcher compares two objects, it takes only object properties into 17 | // account and ignores methods. 18 | beforeEach(function() { 19 | jasmine.addMatchers({ 20 | toEqualData: function(util, customEqualityTesters) { 21 | return { 22 | compare: function(actual, expected) { 23 | return { 24 | pass: angular.equals(actual, expected) 25 | }; 26 | } 27 | }; 28 | } 29 | }); 30 | }); 31 | 32 | // Then we can start by loading the main application module 33 | beforeEach(module(ApplicationConfiguration.applicationModuleName)); 34 | 35 | // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). 36 | // This allows us to inject a service but then attach it to a variable 37 | // with the same name as the service. 38 | beforeEach(inject(function($controller, $rootScope, _$location_, _$stateParams_, _$httpBackend_) { 39 | // Set a new global scope 40 | scope = $rootScope.$new(); 41 | 42 | // Point global variables to injected services 43 | $stateParams = _$stateParams_; 44 | $httpBackend = _$httpBackend_; 45 | $location = _$location_; 46 | 47 | // Initialize the Categories controller. 48 | CategoriesController = $controller('CategoriesController', { 49 | $scope: scope 50 | }); 51 | })); 52 | 53 | it('$scope.find() should create an array with at least one Category object fetched from XHR', inject(function(Categories) { 54 | // Create sample Category using the Categories service 55 | var sampleCategory = new Categories({ 56 | name: 'New Category', 57 | description: 'New Category Description' 58 | }); 59 | 60 | // Create a sample Categories array that includes the new Category 61 | var sampleCategories = [sampleCategory]; 62 | 63 | // Set GET response 64 | $httpBackend.expectGET('categories').respond(sampleCategories); 65 | 66 | // Run controller functionality 67 | scope.find(); 68 | $httpBackend.flush(); 69 | 70 | // Test scope value 71 | expect(scope.categories).toEqualData(sampleCategories); 72 | })); 73 | 74 | it('$scope.findOne() should create an array with one Category object fetched from XHR using a categoryId URL parameter', inject(function(Categories) { 75 | // Define a sample Monkey object 76 | var sampleCategory = new Categories({ 77 | name: 'New Category', 78 | description: 'New Category Description' 79 | }); 80 | 81 | // Set the URL parameter 82 | $stateParams.categoryId = '525a8422f6d0f87f0e407a33'; 83 | 84 | // Set GET response 85 | $httpBackend.expectGET(/categories\/([0-9a-fA-F]{24})$/).respond(sampleCategory); 86 | 87 | // Run controller functionality 88 | scope.findOne(); 89 | $httpBackend.flush(); 90 | 91 | // Test scope value 92 | expect(scope.category).toEqualData(sampleCategory); 93 | })); 94 | 95 | it('$scope.create() with valid form data should send a POST request with the form input values and then locate to new object URL', inject(function(Categories) { 96 | // Create a sample Monkey object 97 | var sampleCategoryPostData = new Categories({ 98 | name: 'New Category', 99 | description: 'New Category Description' 100 | }); 101 | 102 | // Create a sample Category response 103 | var sampleCategoryResponse = new Categories({ 104 | _id: '525cf20451979dea2c000001', 105 | name: 'New Category', 106 | description: 'New Category Description' 107 | }); 108 | 109 | // Fixture mock form input values 110 | scope.name = 'New Category'; 111 | scope.description = 'New Category Description'; 112 | 113 | // Set POST response 114 | $httpBackend.expectPOST('categories', sampleCategoryPostData).respond(sampleCategoryResponse); 115 | 116 | // Run controller functionality 117 | scope.create(); 118 | $httpBackend.flush(); 119 | 120 | // Test form inputs are reset 121 | expect(scope.name).toEqual(''); 122 | 123 | // Test URL redirection after the Category was created 124 | expect($location.path()).toBe('/categories/' + sampleCategoryResponse._id); 125 | })); 126 | 127 | it('$scope.update() should update a valid Category', inject(function(Categories) { 128 | // Define a sample Category put data 129 | var sampleCategoryPutData = new Categories({ 130 | _id: '525cf20451979dea2c000001', 131 | name: 'Update Category', 132 | description: 'Update Category Description' 133 | }); 134 | 135 | // Mock Category in scope 136 | scope.category = sampleCategoryPutData; 137 | 138 | // Set PUT response 139 | $httpBackend.expectPUT(/categories\/([0-9a-fA-F]{24})$/).respond(); 140 | 141 | // Run controller functionality 142 | scope.update(); 143 | $httpBackend.flush(); 144 | 145 | // Test URL location to new object 146 | expect($location.path()).toBe('/categories/' + sampleCategoryPutData._id); 147 | })); 148 | 149 | it('$scope.remove() should send a DELETE request with a valid categoryId and remove the Monkey from the scope', inject(function(Categories) { 150 | // Create new Category object 151 | var sampleCategory = new Categories({ 152 | _id: '525a8422f6d0f87f0e407a33' 153 | }); 154 | 155 | // Create new Categorys array and include the Category 156 | scope.categories = [sampleCategory]; 157 | 158 | // Set expected DELETE response 159 | $httpBackend.expectDELETE(/categories\/([0-9a-fA-F]{24})$/).respond(204); 160 | 161 | // Run controller functionality 162 | scope.remove(sampleCategory); 163 | $httpBackend.flush(); 164 | 165 | // Test array after successful delete 166 | expect(scope.categories.length).toBe(0); 167 | })); 168 | }); 169 | }()); 170 | -------------------------------------------------------------------------------- /app/controllers/users/users.authentication.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var _ = require('lodash'), 7 | errorHandler = require('../errors.server.controller'), 8 | mongoose = require('mongoose'), 9 | passport = require('passport'), 10 | User = mongoose.model('User'); 11 | 12 | /** 13 | * Signup 14 | */ 15 | exports.signup = function(req, res) { 16 | // For security measurement we remove the roles from the req.body object 17 | delete req.body.roles; 18 | 19 | // Init Variables 20 | var user = new User(req.body); 21 | var message = null; 22 | 23 | // Add missing user fields 24 | user.provider = 'local'; 25 | user.displayName = user.firstName + ' ' + user.lastName; 26 | 27 | // Then save the user 28 | user.save(function(err) { 29 | if (err) { 30 | return res.status(400).send({ 31 | message: errorHandler.getErrorMessage(err) 32 | }); 33 | } else { 34 | // Remove sensitive data before login 35 | user.password = undefined; 36 | user.salt = undefined; 37 | 38 | req.login(user, function(err) { 39 | if (err) { 40 | res.status(400).send(err); 41 | } else { 42 | res.json(user); 43 | } 44 | }); 45 | } 46 | }); 47 | }; 48 | 49 | /** 50 | * Signin after passport authentication 51 | */ 52 | exports.signin = function(req, res, next) { 53 | passport.authenticate('local', function(err, user, info) { 54 | if (err || !user) { 55 | res.status(400).send(info); 56 | } else { 57 | // Remove sensitive data before login 58 | user.password = undefined; 59 | user.salt = undefined; 60 | 61 | req.login(user, function(err) { 62 | if (err) { 63 | res.status(400).send(err); 64 | } else { 65 | res.json(user); 66 | } 67 | }); 68 | } 69 | })(req, res, next); 70 | }; 71 | 72 | /** 73 | * Signout 74 | */ 75 | exports.signout = function(req, res) { 76 | req.logout(); 77 | res.redirect('/'); 78 | }; 79 | 80 | /** 81 | * OAuth callback 82 | */ 83 | exports.oauthCallback = function(strategy) { 84 | return function(req, res, next) { 85 | passport.authenticate(strategy, function(err, user, redirectURL) { 86 | if (err || !user) { 87 | return res.redirect('/#!/signin'); 88 | } 89 | req.login(user, function(err) { 90 | if (err) { 91 | return res.redirect('/#!/signin'); 92 | } 93 | 94 | return res.redirect(redirectURL || '/'); 95 | }); 96 | })(req, res, next); 97 | }; 98 | }; 99 | 100 | /** 101 | * Helper function to save or update a OAuth user profile 102 | */ 103 | exports.saveOAuthUserProfile = function(req, providerUserProfile, done) { 104 | if (!req.user) { 105 | // Define a search query fields 106 | var searchMainProviderIdentifierField = 'providerData.' + providerUserProfile.providerIdentifierField; 107 | var searchAdditionalProviderIdentifierField = 'additionalProvidersData.' + providerUserProfile.provider + '.' + providerUserProfile.providerIdentifierField; 108 | 109 | // Define main provider search query 110 | var mainProviderSearchQuery = {}; 111 | mainProviderSearchQuery.provider = providerUserProfile.provider; 112 | mainProviderSearchQuery[searchMainProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; 113 | 114 | // Define additional provider search query 115 | var additionalProviderSearchQuery = {}; 116 | additionalProviderSearchQuery[searchAdditionalProviderIdentifierField] = providerUserProfile.providerData[providerUserProfile.providerIdentifierField]; 117 | 118 | // Define a search query to find existing user with current provider profile 119 | var searchQuery = { 120 | $or: [mainProviderSearchQuery, additionalProviderSearchQuery] 121 | }; 122 | 123 | User.findOne(searchQuery, function(err, user) { 124 | if (err) { 125 | return done(err); 126 | } else { 127 | if (!user) { 128 | var possibleUsername = providerUserProfile.username || ((providerUserProfile.email) ? providerUserProfile.email.split('@')[0] : ''); 129 | 130 | User.findUniqueUsername(possibleUsername, null, function(availableUsername) { 131 | user = new User({ 132 | firstName: providerUserProfile.firstName, 133 | lastName: providerUserProfile.lastName, 134 | username: availableUsername, 135 | displayName: providerUserProfile.displayName, 136 | email: providerUserProfile.email, 137 | provider: providerUserProfile.provider, 138 | providerData: providerUserProfile.providerData 139 | }); 140 | 141 | // And save the user 142 | user.save(function(err) { 143 | return done(err, user); 144 | }); 145 | }); 146 | } else { 147 | return done(err, user); 148 | } 149 | } 150 | }); 151 | } else { 152 | // User is already logged in, join the provider data to the existing user 153 | var user = req.user; 154 | 155 | // Check if user exists, is not signed in using this provider, and doesn't have that provider data already configured 156 | if (user.provider !== providerUserProfile.provider && (!user.additionalProvidersData || !user.additionalProvidersData[providerUserProfile.provider])) { 157 | // Add the provider data to the additional provider data field 158 | if (!user.additionalProvidersData) user.additionalProvidersData = {}; 159 | user.additionalProvidersData[providerUserProfile.provider] = providerUserProfile.providerData; 160 | 161 | // Then tell mongoose that we've updated the additionalProvidersData field 162 | user.markModified('additionalProvidersData'); 163 | 164 | // And save the user 165 | user.save(function(err) { 166 | return done(err, user, '/#!/settings/accounts'); 167 | }); 168 | } else { 169 | return done(new Error('User is already connected using this provider'), user); 170 | } 171 | } 172 | }; 173 | 174 | /** 175 | * Remove OAuth provider 176 | */ 177 | exports.removeOAuthProvider = function(req, res, next) { 178 | var user = req.user; 179 | var provider = req.param('provider'); 180 | 181 | if (user && provider) { 182 | // Delete the additional provider 183 | if (user.additionalProvidersData[provider]) { 184 | delete user.additionalProvidersData[provider]; 185 | 186 | // Then tell mongoose that we've updated the additionalProvidersData field 187 | user.markModified('additionalProvidersData'); 188 | } 189 | 190 | user.save(function(err) { 191 | if (err) { 192 | return res.status(400).send({ 193 | message: errorHandler.getErrorMessage(err) 194 | }); 195 | } else { 196 | req.login(user, function(err) { 197 | if (err) { 198 | res.status(400).send(err); 199 | } else { 200 | res.json(user); 201 | } 202 | }); 203 | } 204 | }); 205 | } 206 | }; -------------------------------------------------------------------------------- /migrations/1422753894413-add-products.js: -------------------------------------------------------------------------------- 1 | var async = require('async'), 2 | request = require('request'), 3 | _ = require('lodash'); 4 | 5 | var categoryIDs = { 6 | 1: 'Beverages', 7 | 2: 'Condiments', 8 | 3: 'Confections', 9 | 4: 'Dairy Products', 10 | 5: 'Grains/Cereals', 11 | 6: 'Meat/Poultry', 12 | 7: 'Produce', 13 | 8: 'Seafood' 14 | }; 15 | 16 | var data = [ 17 | "'Chai',1,1,'10 boxes x 20 bags',18,39,0,10,0 ", 18 | "'Chang',1,1,'24 - 12 oz bottles',19,17,40,25,0 ", 19 | "'Aniseed Syrup',1,2,'12 - 550 ml bottles',10,13,70,25,0 ", 20 | "'Chef Anton''s Cajun Seasoning',2,2,'48 - 6 oz jars',22,53,0,0,0 ", 21 | "'Chef Anton''s Gumbo Mix',2,2,'36 boxes',21.35,0,0,0,1 ", 22 | "'Grandma''s Boysenberry Spread',3,2,'12 - 8 oz jars',25,120,0,25,0 ", 23 | "'Uncle Bob''s Organic Dried Pears',3,7,'12 - 1 lb pkgs.',30,15,0,10,0 ", 24 | "'Northwoods Cranberry Sauce',3,2,'12 - 12 oz jars',40,6,0,0,0 ", 25 | "'Mishi Kobe Niku',4,6,'18 - 500 g pkgs.',97,29,0,0,1 ", 26 | "'Ikura',4,8,'12 - 200 ml jars',31,31,0,0,0 ", 27 | "'Queso Cabrales',5,4,'1 kg pkg.',21,22,30,30,0 ", 28 | "'Queso Manchego La Pastora',5,4,'10 - 500 g pkgs.',38,86,0,0,0 ", 29 | "'Konbu',6,8,'2 kg box',6,24,0,5,0 ", 30 | "'Tofu',6,7,'40 - 100 g pkgs.',23.25,35,0,0,0 ", 31 | "'Genen Shouyu',6,2,'24 - 250 ml bottles',15.5,39,0,5,0 ", 32 | "'Pavlova',7,3,'32 - 500 g boxes',17.45,29,0,10,0 ", 33 | "'Alice Mutton',7,6,'20 - 1 kg tins',39,0,0,0,1 ", 34 | "'Carnarvon Tigers',7,8,'16 kg pkg.',62.5,42,0,0,0 ", 35 | "'Teatime Chocolate Biscuits',8,3,'10 boxes x 12 pieces',9.2,25,0,5,0 ", 36 | "'Sir Rodney''s Marmalade',8,3,'30 gift boxes',81,40,0,0,0 ", 37 | "'Sir Rodney''s Scones',8,3,'24 pkgs. x 4 pieces',10,3,40,5,0 ", 38 | "'Gustaf''s Knäckebröd',9,5,'24 - 500 g pkgs.',21,104,0,25,0 ", 39 | "'Tunnbröd',9,5,'12 - 250 g pkgs.',9,61,0,25,0 ", 40 | "'Guaraná Fantástica',10,1,'12 - 355 ml cans',4.5,20,0,0,1 ", 41 | "'NuNuCa Nuß-Nougat-Creme',11,3,'20 - 450 g glasses',14,76,0,30,0 ", 42 | "'Gumbär Gummibärchen',11,3,'100 - 250 g bags',31.23,15,0,0,0 ", 43 | "'Schoggi Schokolade',11,3,'100 - 100 g pieces',43.9,49,0,30,0 ", 44 | "'Rössle Sauerkraut',12,7,'25 - 825 g cans',45.6,26,0,0,1 ", 45 | "'Thüringer Rostbratwurst',12,6,'50 bags x 30 sausgs.',123.79,0,0,0,1 ", 46 | "'Nord-Ost Matjeshering',13,8,'10 - 200 g glasses',25.89,10,0,15,0 ", 47 | "'Gorgonzola Telino',14,4,'12 - 100 g pkgs',12.5,0,70,20,0 ", 48 | "'Mascarpone Fabioli',14,4,'24 - 200 g pkgs.',32,9,40,25,0 ", 49 | "'Geitost',15,4,'500 g',2.5,112,0,20,0 ", 50 | "'Sasquatch Ale',16,1,'24 - 12 oz bottles',14,111,0,15,0 ", 51 | "'Steeleye Stout',16,1,'24 - 12 oz bottles',18,20,0,15,0 ", 52 | "'Inlagd Sill',17,8,'24 - 250 g jars',19,112,0,20,0 ", 53 | "'Gravad lax',17,8,'12 - 500 g pkgs.',26,11,50,25,0 ", 54 | "'Côte de Blaye',18,1,'12 - 75 cl bottles',263.5,17,0,15,0 ", 55 | "'Chartreuse verte',18,1,'750 cc per bottle',18,69,0,5,0 ", 56 | "'Boston Crab Meat',19,8,'24 - 4 oz tins',18.4,123,0,30,0 ", 57 | "'Jack''s New England Clam Chowder',19,8,'12 - 12 oz cans',9.65,85,0,10,0 ", 58 | "'Singaporean Hokkien Fried Mee',20,5,'32 - 1 kg pkgs.',14,26,0,0,1 ", 59 | "'Ipoh Coffee',20,1,'16 - 500 g tins',46,17,10,25,0 ", 60 | "'Gula Malacca',20,2,'20 - 2 kg bags',19.45,27,0,15,0 ", 61 | "'Rogede sild',21,8,'1k pkg.',9.5,5,70,15,0 ", 62 | "'Spegesild',21,8,'4 - 450 g glasses',12,95,0,0,0 ", 63 | "'Zaanse koeken',22,3,'10 - 4 oz boxes',9.5,36,0,0,0 ", 64 | "'Chocolade',22,3,'10 pkgs.',12.75,15,70,25,0 ", 65 | "'Maxilaku',23,3,'24 - 50 g pkgs.',20,10,60,15,0 ", 66 | "'Valkoinen suklaa',23,3,'12 - 100 g bars',16.25,65,0,30,0 ", 67 | "'Manjimup Dried Apples',24,7,'50 - 300 g pkgs.',53,20,0,10,0 ", 68 | "'Filo Mix',24,5,'16 - 2 kg boxes',7,38,0,25,0 ", 69 | "'Perth Pasties',24,6,'48 pieces',32.8,0,0,0,1 ", 70 | "'Tourtière',25,6,'16 pies',7.45,21,0,10,0 ", 71 | "'Pâté chinois',25,6,'24 boxes x 2 pies',24,115,0,20,0 ", 72 | "'Gnocchi di nonna Alice',26,5,'24 - 250 g pkgs.',38,21,10,30,0 ", 73 | "'Ravioli Angelo',26,5,'24 - 250 g pkgs.',19.5,36,0,20,0 ", 74 | "'Escargots de Bourgogne',27,8,'24 pieces',13.25,62,0,20,0 ", 75 | "'Raclette Courdavault',28,4,'5 kg pkg.',55,79,0,0,0 ", 76 | "'Camembert Pierrot',28,4,'15 - 300 g rounds',34,19,0,0,0 ", 77 | "'Sirop d''érable',29,2,'24 - 500 ml bottles',28.5,113,0,25,0 ", 78 | "'Tarte au sucre',29,3,'48 pies',49.3,17,0,0,0 ", 79 | "'Vegie-spread',7,2,'15 - 625 g jars',43.9,24,0,5,0 ", 80 | "'Wimmers gute Semmelknödel',12,5,'20 bags x 4 pieces',33.25,22,80,30,0 ", 81 | "'Louisiana Fiery Hot Pepper Sauce',2,2,'32 - 8 oz bottles',21.05,76,0,0,0 ", 82 | "'Louisiana Hot Spiced Okra',2,2,'24 - 8 oz jars',17,4,100,20,0 ", 83 | "'Laughing Lumberjack Lager',16,1,'24 - 12 oz bottles',14,52,0,10,0 ", 84 | "'Scottish Longbreads',8,3,'10 boxes x 8 pieces',12.5,6,10,15,0 ", 85 | "'Gudbrandsdalsost',15,4,'10 kg pkg.',36,26,0,15,0 ", 86 | "'Outback Lager',7,1,'24 - 355 ml bottles',15,15,10,30,0 ", 87 | "'Flotemysost',15,4,'10 - 500 g pkgs.',21.5,26,0,0,0 ", 88 | "'Mozzarella di Giovanni',14,4,'24 - 200 g pkgs.',34.8,14,0,0,0 ", 89 | "'Röd Kaviar',17,8,'24 - 150 g jars',15,101,0,5,0 ", 90 | "'Longlife Tofu',4,7,'5 kg pkg.',10,4,20,5,0 ", 91 | "'Rhönbräu Klosterbier',12,1,'24 - 0.5 l bottles',7.75,125,0,25,0 ", 92 | "'Lakkalikööri',23,1,'500 ml',18,57,0,20,0 ", 93 | "'Original Frankfurter grüne Soße',12,2,'12 boxes',13,32,0,15,0 "]; 94 | 95 | exports.up = function(next) { 96 | 97 | var options = { 98 | method: 'get', 99 | json: true, 100 | url: 'http://localhost:3000/categories', 101 | auth: { 102 | user: 'admin', 103 | pass: 'password' 104 | } 105 | }; 106 | 107 | request(options, function (err, response) { 108 | if (response.statusCode == 200) { 109 | saveProducts(response.body, next); 110 | } else { 111 | next('error getting categories'); 112 | } 113 | }); 114 | }; 115 | 116 | exports.down = function(next){ 117 | next(); 118 | }; 119 | 120 | function saveProducts(categories, next) { 121 | async.each(data, function(d, callback) { 122 | 123 | var tokens = d.split(','); 124 | var categoryName = categoryIDs[tokens[2]]; 125 | var categoryMongoID = _.result(_.find(categories, { 'name': categoryName }), '_id'); 126 | var product = { 127 | 'name' : tokens[0].slice(1, tokens[0].length - 1), 128 | 'category' : categoryMongoID, 129 | 'quantityPerUnit' : tokens[2], 130 | 'unitPrice' : tokens[4], 131 | 'unitsInStock' : tokens[5], 132 | 'unitsOnOrder' : tokens[6], 133 | 'discontinued' : tokens[8].trim() 134 | }; 135 | 136 | var options = { 137 | method: 'POST', 138 | body: product, 139 | json: true, 140 | url: 'http://localhost:3000/products', 141 | auth: { 142 | user: 'admin', 143 | pass: 'password' 144 | } 145 | }; 146 | 147 | request(options, function (err, response) { 148 | if (response.statusCode == 201) { 149 | callback(); 150 | } else { 151 | callback('error'); 152 | } 153 | }); 154 | }, function(err) { 155 | if (err) { 156 | next(err); 157 | } else { 158 | next(); 159 | } 160 | }); 161 | } 162 | -------------------------------------------------------------------------------- /app/controllers/users/users.password.server.controller.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var _ = require('lodash'), 7 | errorHandler = require('../errors.server.controller'), 8 | mongoose = require('mongoose'), 9 | passport = require('passport'), 10 | User = mongoose.model('User'), 11 | config = require('../../../config/config'), 12 | nodemailer = require('nodemailer'), 13 | async = require('async'), 14 | crypto = require('crypto'); 15 | 16 | /** 17 | * Forgot for reset password (forgot POST) 18 | */ 19 | exports.forgot = function(req, res, next) { 20 | async.waterfall([ 21 | // Generate random token 22 | function(done) { 23 | crypto.randomBytes(20, function(err, buffer) { 24 | var token = buffer.toString('hex'); 25 | done(err, token); 26 | }); 27 | }, 28 | // Lookup user by username 29 | function(token, done) { 30 | if (req.body.username) { 31 | User.findOne({ 32 | username: req.body.username 33 | }, '-salt -password', function(err, user) { 34 | if (!user) { 35 | return res.status(400).send({ 36 | message: 'No account with that username has been found' 37 | }); 38 | } else if (user.provider !== 'local') { 39 | return res.status(400).send({ 40 | message: 'It seems like you signed up using your ' + user.provider + ' account' 41 | }); 42 | } else { 43 | user.resetPasswordToken = token; 44 | user.resetPasswordExpires = Date.now() + 3600000; // 1 hour 45 | 46 | user.save(function(err) { 47 | done(err, token, user); 48 | }); 49 | } 50 | }); 51 | } else { 52 | return res.status(400).send({ 53 | message: 'Username field must not be blank' 54 | }); 55 | } 56 | }, 57 | function(token, user, done) { 58 | res.render('templates/reset-password-email', { 59 | name: user.displayName, 60 | appName: config.app.title, 61 | url: 'http://' + req.headers.host + '/auth/reset/' + token 62 | }, function(err, emailHTML) { 63 | done(err, emailHTML, user); 64 | }); 65 | }, 66 | // If valid email, send reset email using service 67 | function(emailHTML, user, done) { 68 | var smtpTransport = nodemailer.createTransport(config.mailer.options); 69 | var mailOptions = { 70 | to: user.email, 71 | from: config.mailer.from, 72 | subject: 'Password Reset', 73 | html: emailHTML 74 | }; 75 | smtpTransport.sendMail(mailOptions, function(err) { 76 | if (!err) { 77 | res.send({ 78 | message: 'An email has been sent to ' + user.email + ' with further instructions.' 79 | }); 80 | } 81 | 82 | done(err); 83 | }); 84 | } 85 | ], function(err) { 86 | if (err) return next(err); 87 | }); 88 | }; 89 | 90 | /** 91 | * Reset password GET from email token 92 | */ 93 | exports.validateResetToken = function(req, res) { 94 | User.findOne({ 95 | resetPasswordToken: req.params.token, 96 | resetPasswordExpires: { 97 | $gt: Date.now() 98 | } 99 | }, function(err, user) { 100 | if (!user) { 101 | return res.redirect('/#!/password/reset/invalid'); 102 | } 103 | 104 | res.redirect('/#!/password/reset/' + req.params.token); 105 | }); 106 | }; 107 | 108 | /** 109 | * Reset password POST from email token 110 | */ 111 | exports.reset = function(req, res, next) { 112 | // Init Variables 113 | var passwordDetails = req.body; 114 | 115 | async.waterfall([ 116 | 117 | function(done) { 118 | User.findOne({ 119 | resetPasswordToken: req.params.token, 120 | resetPasswordExpires: { 121 | $gt: Date.now() 122 | } 123 | }, function(err, user) { 124 | if (!err && user) { 125 | if (passwordDetails.newPassword === passwordDetails.verifyPassword) { 126 | user.password = passwordDetails.newPassword; 127 | user.resetPasswordToken = undefined; 128 | user.resetPasswordExpires = undefined; 129 | 130 | user.save(function(err) { 131 | if (err) { 132 | return res.status(400).send({ 133 | message: errorHandler.getErrorMessage(err) 134 | }); 135 | } else { 136 | req.login(user, function(err) { 137 | if (err) { 138 | res.status(400).send(err); 139 | } else { 140 | // Return authenticated user 141 | res.json(user); 142 | 143 | done(err, user); 144 | } 145 | }); 146 | } 147 | }); 148 | } else { 149 | return res.status(400).send({ 150 | message: 'Passwords do not match' 151 | }); 152 | } 153 | } else { 154 | return res.status(400).send({ 155 | message: 'Password reset token is invalid or has expired.' 156 | }); 157 | } 158 | }); 159 | }, 160 | function(user, done) { 161 | res.render('templates/reset-password-confirm-email', { 162 | name: user.displayName, 163 | appName: config.app.title 164 | }, function(err, emailHTML) { 165 | done(err, emailHTML, user); 166 | }); 167 | }, 168 | // If valid email, send reset email using service 169 | function(emailHTML, user, done) { 170 | var smtpTransport = nodemailer.createTransport(config.mailer.options); 171 | var mailOptions = { 172 | to: user.email, 173 | from: config.mailer.from, 174 | subject: 'Your password has been changed', 175 | html: emailHTML 176 | }; 177 | 178 | smtpTransport.sendMail(mailOptions, function(err) { 179 | done(err, 'done'); 180 | }); 181 | } 182 | ], function(err) { 183 | if (err) return next(err); 184 | }); 185 | }; 186 | 187 | /** 188 | * Change Password 189 | */ 190 | exports.changePassword = function(req, res) { 191 | // Init Variables 192 | var passwordDetails = req.body; 193 | 194 | if (req.user) { 195 | if (passwordDetails.newPassword) { 196 | User.findById(req.user.id, function(err, user) { 197 | if (!err && user) { 198 | if (user.authenticate(passwordDetails.currentPassword)) { 199 | if (passwordDetails.newPassword === passwordDetails.verifyPassword) { 200 | user.password = passwordDetails.newPassword; 201 | 202 | user.save(function(err) { 203 | if (err) { 204 | return res.status(400).send({ 205 | message: errorHandler.getErrorMessage(err) 206 | }); 207 | } else { 208 | req.login(user, function(err) { 209 | if (err) { 210 | res.status(400).send(err); 211 | } else { 212 | res.send({ 213 | message: 'Password changed successfully' 214 | }); 215 | } 216 | }); 217 | } 218 | }); 219 | } else { 220 | res.status(400).send({ 221 | message: 'Passwords do not match' 222 | }); 223 | } 224 | } else { 225 | res.status(400).send({ 226 | message: 'Current password is incorrect' 227 | }); 228 | } 229 | } else { 230 | res.status(400).send({ 231 | message: 'User is not found' 232 | }); 233 | } 234 | }); 235 | } else { 236 | res.status(400).send({ 237 | message: 'Please provide a new password' 238 | }); 239 | } 240 | } else { 241 | res.status(400).send({ 242 | message: 'User is not signed in' 243 | }); 244 | } 245 | }; -------------------------------------------------------------------------------- /app/tests/products.server.routes.tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'), 4 | request = require('supertest'), 5 | app = require('../../server'), 6 | productApi = require('./models.server.routes.tests.api')(app, 'Product','/products/'), 7 | categoryApi = require('./models.server.routes.tests.api')(app, 'Category', '/categories/'), 8 | mongoose = require('mongoose'), 9 | User = mongoose.model('User'); 10 | 11 | /** 12 | * Unit tests 13 | */ 14 | describe('Product API', function() { 15 | 16 | before(function(done) { 17 | var user = new User({ 18 | firstName: 'Full', 19 | lastName: 'Name', 20 | displayName: 'Full Name', 21 | email: 'test@test.com', 22 | username: 'username', 23 | password: 'password', 24 | provider: 'local' 25 | }); 26 | 27 | user.save(done); 28 | }); 29 | 30 | after(function(done) { 31 | User.remove().exec(); 32 | done(); 33 | }); 34 | 35 | describe('authenticated crete request with', function() { 36 | var product = { 37 | name: 'Chai', 38 | quantityPerUnit: '10 boxes x 20 bags', 39 | unitPrice: 18, 40 | unitsInStock: 39, 41 | unitsOnOrder: 0, 42 | discontinued: false 43 | }; 44 | 45 | var category = { 46 | name: 'Beverages', 47 | description: 'Soft drinks, coffees, teas, beers, and ales' 48 | }; 49 | 50 | describe('valid product', function(done) { 51 | 52 | var response = {}; 53 | 54 | before(function(done) { 55 | categoryApi.create(category, function(catRes) { 56 | category = catRes.body; 57 | product.category = category._id; 58 | productApi.create(product, function(prodRes) { 59 | response = prodRes; 60 | done(); 61 | }); 62 | }); 63 | }); 64 | 65 | it('returns success status', function() { 66 | response.statusCode.should.equal(201); 67 | }); 68 | 69 | it('returns product details including new id', function() { 70 | response.body.should.have.property('_id'); 71 | 72 | for (var property in product) { 73 | response.body.should.have.property(property, product[property]); 74 | } 75 | }); 76 | 77 | it('is saved in database', function(done) { 78 | productApi.get(response.body._id, function(res) { 79 | res.statusCode.should.equal(200); 80 | response.body.should.have.property('_id', response.body._id); 81 | for (var property in product) { 82 | response.body.should.have.property(property, product[property]); 83 | } 84 | done(); 85 | }); 86 | }); 87 | 88 | after(function(done) { 89 | productApi.clear(categoryApi.clear(done)); 90 | }); 91 | }); 92 | 93 | describe('empty name', function() { 94 | 95 | var response = {}; 96 | var product = { 97 | quantityPerUnit: '10 boxes x 20 bags', 98 | unitPrice: 18, 99 | unitsInStock: 39, 100 | unitsOnOrder: 0, 101 | discontinued: false 102 | }; 103 | 104 | before(function(done) { 105 | productApi.create(product, function(res) { 106 | response = res; 107 | done(); 108 | }); 109 | }); 110 | 111 | it('returns invalid status', function() { 112 | response.statusCode.should.equal(400); 113 | }); 114 | it('returns validation message', function() { 115 | response.body.message.should.equal('name cannot be blank'); 116 | }); 117 | }); 118 | 119 | describe('name longer than 40 chars in length', function() { 120 | 121 | var response = {}; 122 | var product = { 123 | name: 'Soft drinks, coffees, teas, beers, and ales' 124 | }; 125 | 126 | before(function(done) { 127 | productApi.create(product, function(res) { 128 | response = res; 129 | done(); 130 | }); 131 | }); 132 | 133 | it('returns invalid status', function() { 134 | response.statusCode.should.equal(400); 135 | }); 136 | it('returns validation message', function() { 137 | response.body.message.should.equal('name must be 40 chars in length or less'); 138 | }); 139 | }); 140 | }); 141 | 142 | describe('authenticated get request with', function() { 143 | 144 | describe('no parameters', function() { 145 | var products = []; 146 | 147 | before(function(done) { 148 | productApi.create({ name: 'Chai' }, function() { 149 | productApi.create({ name: 'Boston Crab Meat' }, function () { 150 | productApi.create({ name: 'Aniseed Syrup' }, function () { 151 | productApi.list(function(res) { 152 | products = res.body; 153 | done(); 154 | }); 155 | }); 156 | }); 157 | }); 158 | }); 159 | 160 | it('lists all products in alphabetical order', function() { 161 | products.should.have.length(3); 162 | products[0].name.should.equal('Aniseed Syrup'); 163 | products[1].name.should.equal('Boston Crab Meat'); 164 | products[2].name.should.equal('Chai'); 165 | }); 166 | 167 | after(function(done) { 168 | productApi.clear(categoryApi.clear(done)); 169 | }); 170 | }); 171 | 172 | describe('valid product id', function() { 173 | 174 | var response = {}; 175 | var product = {}; 176 | 177 | before(function(done) { 178 | productApi.create({ name: 'Aniseed Syrup' }, function (p) { 179 | product = p.body; 180 | productApi.get(product._id, function(res) { 181 | response = res; 182 | done(); 183 | }); 184 | }); 185 | }); 186 | 187 | it('returns success status', function() { 188 | response.statusCode.should.equal(200); 189 | }); 190 | 191 | it('returns the expected product', function() { 192 | response.body._id.should.equal(product._id); 193 | response.body.name.should.equal(product.name); 194 | }); 195 | 196 | after(function(done) { 197 | productApi.clear(categoryApi.clear(done)); 198 | }); 199 | }); 200 | 201 | describe('invalid product id', function() { 202 | var response = {}; 203 | 204 | before(function(done) { 205 | productApi.get('54c53e9171fde48e4a16008e', function(res) { 206 | response = res; 207 | done(); 208 | }); 209 | }); 210 | 211 | it('returns not found status', function() { 212 | response.statusCode.should.equal(404); 213 | }); 214 | }); 215 | }); 216 | 217 | describe('authenticated update request with', function() { 218 | 219 | var product = { 220 | name: 'Chai' 221 | }; 222 | 223 | var product2 = { 224 | name: 'Boston Crab Meat' 225 | }; 226 | 227 | before(function(done) { 228 | productApi.create(product, function(res) { 229 | product = res.body; 230 | productApi.create(product2, function(res2) { 231 | product2 = res2.body; 232 | done(); 233 | }); 234 | }); 235 | }); 236 | 237 | describe('valid product', function() { 238 | 239 | var response = {}; 240 | 241 | before(function(done) { 242 | product.name = 'Aniseed Syrup'; 243 | productApi.update(product, function(res) { 244 | response = res; 245 | done(); 246 | }); 247 | }); 248 | 249 | it('returns success status', function() { 250 | response.statusCode.should.equal(200); 251 | }); 252 | 253 | it('returns product details', function() { 254 | response.body._id.should.equal(product._id); 255 | response.body.name.should.equal(product.name); 256 | }); 257 | 258 | it('is updated in database', function(done) { 259 | productApi.get(product._id, function(res) { 260 | res.statusCode.should.equal(200); 261 | res.body.should.have.property('name', product.name); 262 | done(); 263 | }); 264 | }); 265 | 266 | it('only updates specified record', function(done) { 267 | productApi.get(product2._id, function(res) { 268 | res.statusCode.should.equal(200); 269 | res.body.should.have.property('name', product2.name); 270 | done(); 271 | }); 272 | }); 273 | }); 274 | 275 | describe('empty product name', function() { 276 | 277 | var response = {}; 278 | 279 | before(function(done) { 280 | product.name = ''; 281 | productApi.update(product, function(res) { 282 | response = res; 283 | done(); 284 | }); 285 | }); 286 | 287 | it('returns invalid status', function() { 288 | response.statusCode.should.equal(400); 289 | }); 290 | 291 | it('returns validation message', function() { 292 | response.body.message.should.equal('name cannot be blank'); 293 | }); 294 | }); 295 | 296 | describe('product name longer than 40 chars in length', function() { 297 | 298 | var response = {}; 299 | 300 | before(function(done) { 301 | product.name = 'Soft drinks, coffees, teas, beers, and ales'; 302 | productApi.update(product, function(res) { 303 | response = res; 304 | done(); 305 | }); 306 | }); 307 | 308 | it('returns invalid status', function() { 309 | response.statusCode.should.equal(400); 310 | }); 311 | 312 | it('returns validation message', function() { 313 | response.body.message.should.equal('name must be 40 chars in length or less'); 314 | }); 315 | }); 316 | 317 | after(function(done) { 318 | productApi.clear(categoryApi.clear(done)); 319 | }); 320 | }); 321 | 322 | describe('authenticated delete request with', function() { 323 | 324 | var products = []; 325 | 326 | before(function(done) { 327 | productApi.create({ name: 'Aniseed Syrup' }, function() { 328 | productApi.create({ name: 'Boston Crab Meat' }, function () { 329 | productApi.create({ name: 'Chai' }, function () { 330 | productApi.list(function(res) { 331 | products = res.body; 332 | done(); 333 | }); 334 | }); 335 | }); 336 | }); 337 | }); 338 | 339 | describe('valid product id', function() { 340 | 341 | var response = {}; 342 | 343 | before(function(done) { 344 | productApi.delete(products[1]._id, function(res) { 345 | response = res; 346 | done(); 347 | }); 348 | }); 349 | 350 | it('returns success status', function() { 351 | response.statusCode.should.equal(200); 352 | }); 353 | 354 | it('returns product details', function() { 355 | response.body._id.should.equal(products[1]._id); 356 | response.body.name.should.equal(products[1].name); 357 | }); 358 | 359 | it('is deleted from database', function(done) { 360 | productApi.get(products[1]._id, function(res) { 361 | res.statusCode.should.equal(404); 362 | productApi.list(function(listRes) { 363 | listRes.body.length.should.equal(2); 364 | done(); 365 | }); 366 | }); 367 | }); 368 | }); 369 | 370 | describe('invalid product id', function() { 371 | 372 | var response = {}; 373 | 374 | before(function(done) { 375 | productApi.delete('54c53e9171fde48e4a16008e', function(res) { 376 | response = res; 377 | done(); 378 | }); 379 | }); 380 | 381 | it('returns not found status', function() { 382 | response.statusCode.should.equal(404); 383 | }); 384 | }); 385 | 386 | after(function(done) { 387 | productApi.clear(categoryApi.clear(done)); 388 | }); 389 | }); 390 | }); 391 | -------------------------------------------------------------------------------- /app/tests/categories.server.routes.tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'), 4 | request = require('supertest'), 5 | app = require('../../server'), 6 | api = require('./models.server.routes.tests.api')(app, 'Category', '/categories/'), 7 | mongoose = require('mongoose'), 8 | User = mongoose.model('User'); 9 | 10 | /** 11 | * Unit tests 12 | */ 13 | describe('Category API', function() { 14 | 15 | before(function(done) { 16 | var user = new User({ 17 | firstName: 'Full', 18 | lastName: 'Name', 19 | displayName: 'Full Name', 20 | email: 'test@test.com', 21 | username: 'username', 22 | password: 'password', 23 | provider: 'local' 24 | }); 25 | 26 | user.save(done); 27 | }); 28 | 29 | after(function(done) { 30 | User.remove().exec(); 31 | done(); 32 | }); 33 | 34 | describe('authenticated create request with', function(done) { 35 | 36 | var category = { 37 | name: 'Beverages', 38 | description: 'Soft drinks, coffees, teas, beers, and ales' 39 | }; 40 | 41 | describe('valid category', function(done) { 42 | 43 | var response = {}; 44 | 45 | before(function(done) { 46 | api.create(category, function(res) { 47 | response = res; 48 | done(); 49 | }); 50 | }); 51 | 52 | it('returns success status', function() { 53 | response.statusCode.should.equal(201); 54 | }); 55 | 56 | it('returns category details including new id', function() { 57 | response.body.should.have.property('_id'); 58 | response.body.should.have.property('name', category.name); 59 | response.body.should.have.property('description', category.description); 60 | }); 61 | 62 | it('is saved in database', function(done) { 63 | api.get(response.body._id, function(res) { 64 | res.statusCode.should.equal(200); 65 | res.body.should.have.property('name', category.name); 66 | res.body.should.have.property('description', category.description); 67 | done(); 68 | }); 69 | }); 70 | 71 | after(function(done) { 72 | api.clear(done); 73 | }); 74 | }); 75 | 76 | describe('empty name', function() { 77 | 78 | var response = {}; 79 | 80 | before(function(done) { 81 | api.create({ description: 'Drinks' }, function(res) { 82 | response = res; 83 | done(); 84 | }); 85 | }); 86 | 87 | it('returns invalid status', function() { 88 | response.statusCode.should.equal(400); 89 | }); 90 | it('returns validation message', function() { 91 | response.body.message.should.equal('name cannot be blank'); 92 | }); 93 | }); 94 | 95 | describe('name longer than 15 chars in length', function() { 96 | 97 | var response = {}; 98 | 99 | before(function(done) { 100 | api.create({ name : 'Beverages and Drinks' }, function(res) { 101 | response = res; 102 | done(); 103 | }); 104 | }); 105 | 106 | it('returns invalid status', function() { 107 | response.statusCode.should.equal(400); 108 | }); 109 | 110 | it('returns validation message', function() { 111 | response.body.message.should.equal('name must be 15 chars in length or less'); 112 | }); 113 | }); 114 | 115 | describe('duplicate name', function() { 116 | 117 | var response = {}; 118 | 119 | before(function(done) { 120 | api.create(category, function() { 121 | // make second call with duplicate name 122 | api.create(category, function (res) { 123 | response = res; 124 | done(); 125 | }); 126 | }); 127 | }); 128 | 129 | it('returns invalid status', function() { 130 | response.statusCode.should.equal(400); 131 | }); 132 | 133 | it('returns validation message', function() { 134 | response.body.message.should.equal('Name already exists'); 135 | }); 136 | }); 137 | 138 | after(function(done) { 139 | api.clear(done); 140 | }); 141 | }); 142 | 143 | describe('authenticated get request with', function() { 144 | 145 | var categories = []; 146 | 147 | before(function(done) { 148 | api.create({ name: 'Condiments' }, function() { 149 | api.create({ name: 'Beverages' }, function () { 150 | api.create({ name: 'Ales' }, function () { 151 | api.list(function(res) { 152 | categories = res.body; 153 | done(); 154 | }); 155 | }); 156 | }); 157 | }); 158 | }); 159 | 160 | describe('no parameters', function() { 161 | it('lists all categories in alphabetical order', function() { 162 | categories.should.have.length(3); 163 | categories[0].name.should.equal('Ales'); 164 | categories[1].name.should.equal('Beverages'); 165 | categories[2].name.should.equal('Condiments'); 166 | }); 167 | }); 168 | 169 | describe('valid category id', function() { 170 | 171 | var response = {}; 172 | 173 | before(function(done) { 174 | api.get(categories[0]._id, function(res) { 175 | response = res; 176 | done(); 177 | }); 178 | }); 179 | 180 | it('returns success status', function() { 181 | response.statusCode.should.equal(200); 182 | }); 183 | 184 | it('returns the expected category', function() { 185 | response.body._id.should.equal(categories[0]._id); 186 | response.body.name.should.equal(categories[0].name); 187 | response.body.description.should.equal(categories[0].description); 188 | }); 189 | }); 190 | 191 | describe('invalid category id', function() { 192 | var response = {}; 193 | 194 | before(function(done) { 195 | api.get('54c53e9171fde48e4a16008e', function(res) { 196 | response = res; 197 | done(); 198 | }); 199 | }); 200 | 201 | it('returns not found status', function() { 202 | response.statusCode.should.equal(404); 203 | }); 204 | }); 205 | 206 | after(function(done) { 207 | api.clear(done); 208 | }); 209 | }); 210 | 211 | describe('authenticated update request with', function() { 212 | 213 | var category = { 214 | name: 'Beverages', 215 | description: 'Soft drinks, coffees, teas, beers, and ales' 216 | }; 217 | 218 | var category2 = { 219 | name: 'Condiments', 220 | description: 'Sweet and savory sauces, relishes, spreads, and seasonings' 221 | }; 222 | 223 | before(function(done) { 224 | api.create(category, function(res) { 225 | category = res.body; 226 | api.create(category2, function(res2) { 227 | category2 = res2.body; 228 | done(); 229 | }); 230 | }); 231 | }); 232 | 233 | describe('valid category', function() { 234 | 235 | var response = {}; 236 | 237 | before(function(done) { 238 | category.name = 'Drinks'; 239 | category.description = 'Beers and ales'; 240 | api.update(category, function(res) { 241 | response = res; 242 | done(); 243 | }); 244 | }); 245 | 246 | it('returns success status', function() { 247 | response.statusCode.should.equal(200); 248 | }); 249 | 250 | it('returns category details', function() { 251 | response.body._id.should.equal(category._id); 252 | response.body.name.should.equal(category.name); 253 | response.body.description.should.equal(category.description); 254 | }); 255 | 256 | it('is updated in database', function(done) { 257 | api.get(category._id, function(res) { 258 | res.statusCode.should.equal(200); 259 | res.body.should.have.property('name', category.name); 260 | res.body.should.have.property('description', category.description); 261 | done(); 262 | }); 263 | }); 264 | 265 | it('only updates specified record', function(done) { 266 | api.get(category2._id, function(res) { 267 | res.statusCode.should.equal(200); 268 | res.body.should.have.property('name', category2.name); 269 | res.body.should.have.property('description', category2.description); 270 | done(); 271 | }); 272 | }); 273 | }); 274 | 275 | describe('empty category name', function() { 276 | 277 | var response = {}; 278 | 279 | before(function(done) { 280 | category.name = ''; 281 | api.update(category, function(res) { 282 | response = res; 283 | done(); 284 | }); 285 | }); 286 | 287 | it('returns invalid status', function() { 288 | response.statusCode.should.equal(400); 289 | }); 290 | 291 | it('returns validation message', function() { 292 | response.body.message.should.equal('name cannot be blank'); 293 | }); 294 | }); 295 | 296 | describe('category name longer than 15 chars in length', function() { 297 | 298 | var response = {}; 299 | 300 | before(function(done) { 301 | category.name = 'Beverages and Drinks'; 302 | api.update(category, function(res) { 303 | response = res; 304 | done(); 305 | }); 306 | }); 307 | 308 | it('returns invalid status', function() { 309 | response.statusCode.should.equal(400); 310 | }); 311 | 312 | it('returns validation message', function() { 313 | response.body.message.should.equal('name must be 15 chars in length or less'); 314 | }); 315 | }); 316 | 317 | describe('duplicate category name', function() { 318 | 319 | var response = {}; 320 | 321 | before(function(done) { 322 | category.name = 'Condiments'; 323 | api.update(category, function(res) { 324 | response = res; 325 | done(); 326 | }); 327 | }); 328 | 329 | it('returns invalid status', function() { 330 | response.statusCode.should.equal(400); 331 | }); 332 | 333 | it('returns validation message', function() { 334 | response.body.message.should.equal('Name already exists'); 335 | }); 336 | }); 337 | 338 | after(function(done) { 339 | api.clear(done); 340 | }); 341 | }); 342 | 343 | describe('authenticated delete request with', function() { 344 | 345 | var categories = []; 346 | 347 | before(function(done) { 348 | api.create({ name: 'Condiments' }, function() { 349 | api.create({ name: 'Beverages' }, function () { 350 | api.create({ name: 'Ales' }, function () { 351 | api.list(function(res) { 352 | categories = res.body; 353 | done(); 354 | }); 355 | }); 356 | }); 357 | }); 358 | }); 359 | 360 | describe('valid category id', function() { 361 | 362 | var response = {}; 363 | 364 | before(function(done) { 365 | api.delete(categories[1]._id, function(res) { 366 | response = res; 367 | done(); 368 | }); 369 | }); 370 | 371 | it('returns success status', function() { 372 | response.statusCode.should.equal(200); 373 | }); 374 | 375 | it('returns category details', function() { 376 | response.body._id.should.equal(categories[1]._id); 377 | response.body.name.should.equal(categories[1].name); 378 | response.body.description.should.equal(categories[1].description); 379 | }); 380 | 381 | it('is deleted from database', function(done) { 382 | api.get(categories[1]._id, function(res) { 383 | res.statusCode.should.equal(404); 384 | api.list(function(listRes) { 385 | listRes.body.length.should.equal(2); 386 | done(); 387 | }); 388 | }); 389 | }); 390 | }); 391 | 392 | describe('invalid category id', function() { 393 | 394 | var response = {}; 395 | 396 | before(function(done) { 397 | api.delete('54c53e9171fde48e4a16008e', function(res) { 398 | response = res; 399 | done(); 400 | }); 401 | }); 402 | 403 | it('returns not found status', function() { 404 | response.statusCode.should.equal(404); 405 | }); 406 | }); 407 | 408 | after(function(done) { 409 | api.clear(done); 410 | }); 411 | }); 412 | 413 | }); 414 | --------------------------------------------------------------------------------