├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── Procfile ├── README.md ├── app.js ├── app.json ├── app ├── controllers │ ├── articles.js │ ├── index.js │ └── users.js ├── models │ ├── article.js │ └── user.js ├── routes │ ├── articles.js │ ├── index.js │ └── users.js └── views │ ├── 404.jade │ ├── 500.jade │ ├── includes │ ├── foot.jade │ └── head.jade │ ├── index.jade │ └── layouts │ └── default.jade ├── bower.json ├── config ├── config.js ├── env │ ├── all.json5 │ ├── development.json5.sample │ ├── production.json5.sample │ ├── test.json5.sample │ └── travis.json ├── express.js ├── middlewares │ └── session.js ├── passport.js ├── sequelize.js └── winston.js ├── gruntfile.js ├── package.json ├── pm2-ecosystem.json ├── pm2-main.js ├── public ├── css │ ├── common.css │ └── views │ │ └── articles.css ├── humans.txt ├── img │ ├── .gitignore │ ├── apple │ │ ├── apple-touch-icon-114x114-precomposed.png │ │ ├── apple-touch-icon-144x144-precomposed.png │ │ ├── apple-touch-icon-57x57-precomposed.png │ │ ├── apple-touch-icon-72x72-precomposed.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── apple-touch-icon.png │ │ ├── splash.png │ │ └── splash2x.png │ ├── icons │ │ ├── facebook.png │ │ ├── favicon.ico │ │ ├── github.png │ │ ├── google.png │ │ └── twitter.png │ ├── loaders │ │ └── loader.gif │ ├── m_logo.png │ └── sprites │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png ├── js │ ├── FbSdk.js │ ├── app.js │ ├── config.js │ ├── controllers │ │ ├── articles.js │ │ ├── header.js │ │ ├── index.js │ │ └── users │ │ │ ├── auth.js │ │ │ ├── signIn.js │ │ │ └── signUp.js │ ├── directives.js │ ├── filters.js │ ├── init.js │ └── services │ │ ├── articles.js │ │ ├── authenticate.js │ │ └── global.js ├── robots.txt └── views │ ├── 404.html │ ├── articles │ ├── create.html │ ├── edit.html │ ├── list.html │ └── view.html │ ├── header.html │ ├── index.html │ └── users │ ├── auth.html │ ├── signin.html │ └── signup.html └── test ├── karma ├── karma.conf.js └── unit │ └── controllers │ ├── articles.spec.js │ ├── headers.spec.js │ └── index.spec.js └── mocha ├── controllers ├── articleControllerSpec.js └── usersControllerSpec.js └── models └── userModelSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Github Node Default 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | node_modules 18 | 19 | # From Mean Stack 20 | bower_components/ 21 | .DS_Store 22 | .nodemonignore 23 | .sass-cache/ 24 | node_modules/ 25 | public/lib 26 | test/coverage/ 27 | migrations/ 28 | 29 | # Configuration Files 30 | /config/env/development.json5 31 | /config/env/production.json5 32 | /config/env/test.json5 33 | 34 | *.local 35 | 36 | # IDE files 37 | .idea 38 | /.*.md.html 39 | .settings 40 | .project 41 | .classpath 42 | *~ 43 | *.swp 44 | *.swo 45 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 4, // {int} Number of spaces to use for indentation 16 | "latedef" : false, // true: Require variables/functions to be defined before being used 17 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : true, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` and `--` 23 | "quotmark" : false, // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : false, // Unused variables: 30 | // true : all variables, last function parameter 31 | // "vars" : all variables only 32 | // "strict" : all variables, all function parameters 33 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 34 | "maxparams" : false, // {int} Max number of formal params allowed per function 35 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 36 | "maxstatements" : false, // {int} Max number statements per function 37 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 38 | "maxlen" : false, // {int} Max number of characters per line 39 | "varstmt" : false, // true: Disallow any var statements. Only `let` and `const` are allowed. 40 | 41 | // Relaxing 42 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 43 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 44 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 45 | "eqnull" : true, // true: Tolerate use of `== null` 46 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 47 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 48 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 49 | // (ex: `for each`, multiple try/catch, function expression…) 50 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 51 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 52 | "funcscope" : true, // true: Tolerate defining variables inside control statements 53 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 54 | "iterator" : false, // true: Tolerate using the `__iterator__` property 55 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 56 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 57 | "laxcomma" : false, // true: Tolerate comma-first style coding 58 | "loopfunc" : false, // true: Tolerate functions being defined in loops 59 | "multistr" : false, // true: Tolerate multi-line strings 60 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 61 | "notypeof" : false, // true: Tolerate invalid typeof operator values 62 | "proto" : false, // true: Tolerate using the `__proto__` property 63 | "scripturl" : false, // true: Tolerate script-targeted URLs 64 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 65 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 66 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 67 | "validthis" : false, // true: Tolerate using this in a non-constructor function 68 | 69 | // Environments 70 | "browser" : true, // Web Browser (window, document, etc) 71 | "browserify" : false, // Browserify (node.js code in the browser) 72 | "couch" : false, // CouchDB 73 | "devel" : true, // Development/debugging (alert, confirm, etc) 74 | "dojo" : false, // Dojo Toolkit 75 | "jasmine" : true, // Jasmine 76 | "jquery" : false, // jQuery 77 | "mocha" : true, // Mocha 78 | "mootools" : false, // MooTools 79 | "node" : true, // Node.js 80 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 81 | "phantom" : false, // PhantomJS 82 | "prototypejs" : false, // Prototype and Scriptaculous 83 | "qunit" : false, // QUnit 84 | "rhino" : false, // Rhino 85 | "shelljs" : false, // ShellJS 86 | "typed" : false, // Globals for typed array constructions 87 | "worker" : false, // Web Workers 88 | "wsh" : false, // Windows Scripting Host 89 | "yui" : false, // Yahoo User Interface 90 | 91 | // Custom Globals 92 | "globals" : { 93 | "angular": true, 94 | "describe": true, 95 | "it": true, 96 | "expect": true, 97 | "beforeEach": true, 98 | "afterEach": true, 99 | "module": true, 100 | "inject": true 101 | } // additional predefined global variables 102 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | env: 5 | - NODE_ENV=travis 6 | services: 7 | - mongodb 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2015 Jeff Potter, Chaudhry Junaid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node pm2-main.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MEAN Stack Relational ![Mean Stack Build Status](https://travis-ci.org/jpotts18/mean-stack-relational.png) 2 | ===================== 3 | 4 | ### Please use for reference only! No support or updates planned. 5 | 6 | The main idea for this repository is shamelessly stolen from [http://mean.io](http://mean.io). It says: 7 | 8 | > MEAN is a boilerplate that provides a nice starting point for [MySQL], Express, Node.js, and AngularJS based applications. It is designed to give you quick and organized way to start developing of MEAN based web apps with useful modules like sequelize and passport pre-bundled and configured. We mainly try to take care of the connection points between existing popular frameworks and solve common integration problems. 9 | 10 | 11 | The MongoDB ORM, [Mongoose](http://mongoosejs.com/), has been replaced with [Sequelize](http://sequelizejs.com/). Switching from mongoose to sequelize allows developers easy access to MySQL, MariaDB, SQLite or PostgreSQL databases by mapping database entries to objects and vice versa. 12 | 13 | [Addy Osmani's Blog](http://addyosmani.com/blog/full-stack-javascript-with-mean-and-yeoman/) explains SQL databases, being strongly typed in nature are great at enforcing a level of consistency, ensuring many kinds of bad data simply don’t get recorded. By using SQL databases MEAN Stack Relational favors reliability over the performance gains of NoSQL databases. 14 | 15 | # Demo 16 | 17 | Deploy to your Heroku account for a demo: 18 | 19 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 20 | 21 | Note: Deploy from main repository view to avoid missing app.json error. 22 | 23 | # Getting Started 24 | 25 | Alright now the fun begins. First clone or download the repo to your computer. 26 | 27 | 1. Clone the repository ```git clone git@github.com:jpotts18/mean-stack-relational.git```. 28 | 1. Go into the repository ```cd mean-stack-relational/```. 29 | 1. Install dependencies with NPM ```npm install```. This will copy development.json5, and production.json5 from respective sample files in the config/env folder and run the grunt copy task to copy frontend lib files to their destination. 30 | 1. Plug in your private and public keys for working with FB and Twitter into ```/config/env/development.json5``` and/or ```/config/env/production.json5```. 31 | 1. Wire up the database connection found in ```/config/env/development.json5``` and/or ```/config/env/production.json5```. 32 | 1. Run in production mode with: ```pm2 start pm2-ecosystem.json --env production``` (Run ```sudo npm install -g pm2``` if it's not installed.), or 33 | 1. Run in development mode with grunt: ```grunt``` 34 | 1. Make something awesome! 35 | 36 | Thats all! Now go and open up your browser at [http://localhost:3000](http://localhost:3000), and tweet [@jpotts18](http://twitter.com/jpotts18) to say thanks! 37 | 38 | 39 | ## Prerequisites 40 | - Node.js - Download and Install Node.js. You can also follow [this gist](https://gist.github.com/isaacs/579814) for a quick and easy way to install Node.js and npm 41 | - MySQL - Download and Install MySQL - Make sure it's running on the default port (3306). 42 | 43 | ### Tool Prerequisites 44 | - NPM - Node.js package manager, should be installed when you install node.js. NPM (Node Package Manager) will look at the [package.json](https://github.com/jpotts18/mean-stack-relational/blob/master/package.json) file in the root of the project and download all of the necessary dependencies and put them in a folder called ```node_modules``` 45 | 46 | - Bower - Web package manager, installing Bower is simple when you have npm: 47 | ``` npm install -g bower ``` 48 | 49 | ### NPM Modules Used 50 | - [Passport](http://passportjs.org/) - Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more. 51 | - [Express](http://expressjs.com/) - Express is a minimal and flexible node.js web application framework, providing a robust set of features for building single and multi-page, and hybrid web applications. 52 | - [Sequelize](http://sequelizejs.com/) - The Sequelize library provides easy access to MySQL, MariaDB, SQLite or PostgreSQL databases by mapping database entries to objects and vice versa. To put it in a nutshell, it's an ORM (Object-Relational-Mapper). The library is written entirely in JavaScript and can be used in the Node.JS environment. 53 | 54 | ### Javascript Tools Used 55 | - [Grunt](http://gruntjs.com/) - In one word: automation. The less work you have to do when performing repetitive tasks like minification, compilation, unit testing, linting, etc, the easier your job becomes. After you've configured it, a Grunt can do most of that mundane work for you—and your team—with basically zero effort. 56 | 57 | 1. It [watches](https://github.com/jpotts18/mean-stack-relational/blob/master/gruntfile.js#L5) your filesystem and when it detects a change it will livereload your changes. 58 | 59 | 2. It runs [jshint](https://github.com/jpotts18/mean-stack-relational/blob/master/gruntfile.js#L32) which looks through your javascript files and ensures coding standards. 60 | 61 | 3. It runs [nodemon](https://github.com/jpotts18/mean-stack-relational/blob/master/gruntfile.js#L35) which watches changes in specific folders and recompiles the app when necessary. No running ```node app.js``` every 2 minutes. 62 | 63 | 4. It can also run tests like mocha and karma for you. 64 | 65 | - [Bower](http://bower.io/) - Bower is a package manager for the web. It offers a generic, unopinionated solution to the problem of front-end package management, while exposing the package dependency model via an API that can be consumed by a more opinionated build stack. There are no system wide dependencies, no dependencies are shared between different apps, and the dependency tree is flat. 66 | 67 | ### Front-End Tools Used 68 | - [Angular.js](http://angularjs.org) - AngularJS is an open-source JavaScript framework, maintained by Google, that assists with running single-page applications. Its goal is to augment browser-based applications with model–view–controller (MVC) capability, in an effort to make both development and testing easier. 69 | - [Twitter Bootstrap](http://getbootstrap.com/) - Sleek, intuitive, and powerful mobile first front-end framework for faster and easier web development. 70 | - [UI Bootstrap](http://angular-ui.github.io/bootstrap/) - Bootstrap components written in pure AngularJS by the AngularUI Team 71 | 72 | # Project Roadmap 73 | 74 | Following is a list of items detailing future direction for MEAN Stack Relational: 75 | 76 | ## Purpose 77 | - Demonstrate several login strategies using passport.js 78 | - Demonstrate environment configuration best practices 79 | - Demonstrate how to use Sequelize to query a single table and to accomplish a join. 80 | 81 | ## Additions 82 | - Demonstrate testing for Express routes and javascript classes using Mocha, Sinon, Proxyquire and more 83 | - Demonstrating modularity by using javascript classes for complex backend functionality 84 | - Yeoman generator to compete with MEAN 85 | 86 | 87 | # Troubleshooting and Contact 88 | 89 | During install some of you may encounter some issues feel free to contact me (jpotts18) or the co-contributor (chaudhryjunaid), via the repository issue tracker or the links provided below. I am also available on twitter at [@jpotts18](http://twitter.com/jpotts18) and Junaid at [@chjunaidanwar](http://twitter.com/chjunaidanwar). 90 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var express = require('express'); 7 | var fs = require('fs'); 8 | 9 | /** 10 | * Main application entry file. 11 | * Please note that the order of loading is important. 12 | */ 13 | 14 | // Load Configurations 15 | var config = require('./config/config'); 16 | var winston = require('./config/winston'); 17 | 18 | winston.info('Starting '+config.app.name+'...'); 19 | winston.info('Config loaded: '+config.NODE_ENV); 20 | winston.debug('Accepted Config:',config); 21 | 22 | var db = require('./config/sequelize'); 23 | var passport = require('./config/passport'); 24 | 25 | var app = express(); 26 | 27 | //Initialize Express 28 | require('./config/express')(app, passport); 29 | 30 | //Start the app by listening on 31 | app.listen(config.PORT); 32 | winston.info('Express app started on port ' + config.PORT); 33 | 34 | //expose app 35 | module.exports = app; 36 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mean-stack-relational", 3 | "description": "M*EAN - A Modern Stack: MySQL(Postgres for Heroku), ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).", 4 | "repository": "https://github.com/jpotts18/mean-stack-relational.git", 5 | "logo": "https://raw.githubusercontent.com/jpotts18/mean-stack-relational/master/public/img/m_logo.png", 6 | "keywords": ["node", "express", "static"], 7 | "env": { 8 | "WEB_CONCURRENCY": { 9 | "description": "The number of processes to run.", 10 | "value": "1" 11 | } 12 | }, 13 | "addons": [ 14 | "heroku-postgresql:hobby-dev" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /app/controllers/articles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var StandardError = require('standard-error'); 7 | var db = require('../../config/sequelize'); 8 | 9 | /** 10 | * Find article by id 11 | * Note: This is called every time that the parameter :articleId is used in a URL. 12 | * Its purpose is to preload the article on the req object then call the next function. 13 | */ 14 | exports.article = function(req, res, next, id) { 15 | console.log('id => ' + id); 16 | db.Article.find({where: {id: id}, include: [{model:db.User, attributes:['id', 'username', 'name']}]}).then(function(article){ 17 | if(!article) { 18 | return next(new Error('Failed to load article ' + id)); 19 | } else { 20 | req.article = article; 21 | return next(); 22 | } 23 | }).catch(function(err){ 24 | return next(err); 25 | }); 26 | }; 27 | 28 | /** 29 | * Create a article 30 | */ 31 | exports.create = function(req, res) { 32 | // augment the article by adding the UserId 33 | req.body.UserId = req.user.id; 34 | // save and return and instance of article on the res object. 35 | db.Article.create(req.body).then(function(article){ 36 | if(!article){ 37 | return res.send('users/signup', {errors: new StandardError('Article could not be created')}); 38 | } else { 39 | return res.jsonp(article); 40 | } 41 | }).catch(function(err){ 42 | return res.send('users/signup', { 43 | errors: err, 44 | status: 500 45 | }); 46 | }); 47 | }; 48 | 49 | /** 50 | * Update a article 51 | */ 52 | exports.update = function(req, res) { 53 | 54 | // create a new variable to hold the article that was placed on the req object. 55 | var article = req.article; 56 | 57 | article.updateAttributes({ 58 | title: req.body.title, 59 | content: req.body.content 60 | }).then(function(a){ 61 | return res.jsonp(a); 62 | }).catch(function(err){ 63 | return res.render('error', { 64 | error: err, 65 | status: 500 66 | }); 67 | }); 68 | }; 69 | 70 | /** 71 | * Delete an article 72 | */ 73 | exports.destroy = function(req, res) { 74 | 75 | // create a new variable to hold the article that was placed on the req object. 76 | var article = req.article; 77 | 78 | article.destroy().then(function(){ 79 | return res.jsonp(article); 80 | }).catch(function(err){ 81 | return res.render('error', { 82 | error: err, 83 | status: 500 84 | }); 85 | }); 86 | }; 87 | 88 | /** 89 | * Show an article 90 | */ 91 | exports.show = function(req, res) { 92 | // Sending down the article that was just preloaded by the articles.article function 93 | // and saves article on the req object. 94 | return res.jsonp(req.article); 95 | }; 96 | 97 | /** 98 | * List of Articles 99 | */ 100 | exports.all = function(req, res) { 101 | db.Article.findAll({include: [{model:db.User, attributes: ['id', 'username', 'name']}]}).then(function(articles){ 102 | return res.jsonp(articles); 103 | }).catch(function(err){ 104 | return res.render('error', { 105 | error: err, 106 | status: 500 107 | }); 108 | }); 109 | }; 110 | 111 | /** 112 | * Article authorizations routing middleware 113 | */ 114 | exports.hasAuthorization = function(req, res, next) { 115 | if (req.article.User.id !== req.user.id) { 116 | return res.send(401, 'User is not authorized'); 117 | } 118 | next(); 119 | }; 120 | -------------------------------------------------------------------------------- /app/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var _ = require('lodash'); 7 | 8 | 9 | exports.render = function(req, res) { 10 | res.render('index', { 11 | user: req.user ? JSON.stringify(req.user) : "null" 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /app/controllers/users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var db = require('../../config/sequelize'), 7 | request = require('request'), 8 | qs = require('querystring'), 9 | config = require('../../config/config'), 10 | passport = require('passport'); 11 | 12 | /** 13 | * Auth callback 14 | */ 15 | exports.authCallback = function (req, res, next) { 16 | res.redirect('/'); 17 | }; 18 | 19 | /** 20 | * Show login form 21 | */ 22 | exports.signin = function (req, res) { 23 | res.render('users/signin', { 24 | title: 'Signin', 25 | message: req.flash('error') 26 | }); 27 | }; 28 | 29 | /** 30 | * Show sign up form 31 | */ 32 | exports.signup = function (req, res) { 33 | res.render('users/signup', { 34 | title: 'Sign up', 35 | }); 36 | }; 37 | 38 | /** 39 | * Logout 40 | */ 41 | exports.signout = function (req, res) { 42 | console.log('Logout: { id: ' + req.user.id + ', username: ' + req.user.username + '}'); 43 | req.logout(); 44 | return res.send({status: 'success', message: 'User logout successfully.'}); 45 | }; 46 | 47 | /** 48 | * Session 49 | */ 50 | exports.session = function (req, res) { 51 | return res.send({status: 'success', message: 'User login successfully.'}) 52 | // res.redirect('/'); 53 | }; 54 | 55 | /** 56 | * Create user 57 | */ 58 | exports.create = function (req, res, next) { 59 | var message = null; 60 | 61 | var user = db.User.build(req.body); 62 | 63 | user.provider = 'local'; 64 | user.salt = user.makeSalt(); 65 | user.hashedPassword = user.encryptPassword(req.body.password, user.salt); 66 | console.log('New User (local) : { id: ' + user.id + ' username: ' + user.username + ' }'); 67 | 68 | user.save().then(function () { 69 | req.login(user, function (err) { 70 | if (err) { 71 | return next(err); 72 | } 73 | return res.send({status: 'success', message: 'User signup successfully.'}) 74 | // res.redirect('/'); 75 | }); 76 | }).catch(function (err) { 77 | res.render('users/signup', { 78 | message: message, 79 | user: user 80 | }); 81 | }); 82 | }; 83 | 84 | /** 85 | * Send User 86 | */ 87 | exports.me = function (req, res) { 88 | res.jsonp(req.user || null); 89 | }; 90 | 91 | /** 92 | * Find user by id 93 | */ 94 | exports.user = function (req, res, next, id) { 95 | db.User.find({where: {id: id}}).then(function (user) { 96 | if (!user) { 97 | return next(new Error('Failed to load User ' + id)); 98 | } 99 | req.profile = user; 100 | next(); 101 | }).catch(function (err) { 102 | next(err); 103 | }); 104 | }; 105 | 106 | /** 107 | * Generic require login routing middleware 108 | */ 109 | exports.requiresLogin = function (req, res, next) { 110 | if (!req.isAuthenticated()) { 111 | return res.status(401).send('User is not authorized'); 112 | } 113 | next(); 114 | }; 115 | 116 | /** 117 | * User authorizations routing middleware 118 | */ 119 | exports.hasAuthorization = function (req, res, next) { 120 | if (req.profile.id !== req.user.id) { 121 | return res.status(401).send('User is not authorized'); 122 | } 123 | next(); 124 | }; 125 | 126 | // function to authenticate and create user 127 | function authenticateSocialUser(profile, done) { 128 | var searchQuery = { 129 | email: profile.email 130 | }; 131 | if (profile.name) { 132 | searchQuery = { 133 | twitterKey: profile.id 134 | } 135 | } 136 | 137 | db.User.find({where: searchQuery}).then(function (user) { 138 | 139 | if (!user) { 140 | var userObj = {}; 141 | if (profile.name) { 142 | userObj = { 143 | name: profile.name || '', 144 | username: profile.name || '', 145 | provider: 'twitter', 146 | twitterKey: profile.id, 147 | email: profile.id + "@twitter.com" 148 | } 149 | } else { 150 | userObj = { 151 | name: profile.given_name || '', 152 | email: profile.email, 153 | username: profile.given_name || profile.name || '', 154 | provider: 'google', 155 | googleUserId: profile.sub 156 | }; 157 | } 158 | db.User.create(userObj).then(function (u) { 159 | done(null, u); 160 | }) 161 | } else { 162 | done(null, user); 163 | } 164 | }).catch(function (err) { 165 | done(err, null, err); 166 | }); 167 | } 168 | 169 | exports.facebookUser = function (req, res, next) { 170 | 171 | function sendResponse(err, user, info) { 172 | 173 | if (err) { 174 | return res.send({status: "error", error: err}); 175 | } 176 | if (!user) { 177 | return res.send({status: "error", error: info}); 178 | } 179 | req.login(user, function (err) { 180 | if (err) { 181 | return res.send({status: "error", error: {message: 'Login failed'}}); 182 | } 183 | return res.send({status: "success", data: {user: user}}); 184 | }); 185 | } 186 | 187 | passport.authenticate('facebook-token', {scope: ['email', 'user_about_me', 'phone']}, sendResponse)(req, res, next); 188 | } 189 | 190 | exports.twitterSocialUser = function (req, res) { 191 | var requestTokenUrl = 'https://api.twitter.com/oauth/request_token'; 192 | var accessTokenUrl = 'https://api.twitter.com/oauth/access_token'; 193 | var profileUrl = 'https://api.twitter.com/1.1/account/verify_credentials.json'; 194 | 195 | function sendResponse(err, user, info) { 196 | if (err) { 197 | return res.send({status: "error", error: err}); 198 | } 199 | if (!user) { 200 | return res.send({status: "error", error: info}); 201 | } 202 | 203 | req.login(user, function (err) { 204 | if (err) { 205 | return res.send({status: "error", error: {message: 'Login failed'}}); 206 | } 207 | return res.send({status: "success", data: {user: user}}); 208 | }); 209 | 210 | } 211 | 212 | function getUserProfile(err, response, profile) { 213 | if (err) { 214 | return res.send({status: "error", error: err}); 215 | } 216 | // Step 5a. Link user accounts. 217 | authenticateSocialUser(profile, sendResponse); 218 | 219 | } 220 | 221 | function verifyAccesToken(err, response, accessToken) { 222 | 223 | accessToken = qs.parse(accessToken); 224 | 225 | var profileOauth = { 226 | consumer_key: config.twitter.clientID, 227 | consumer_secret: config.twitter.clientSecret, 228 | token: accessToken.oauth_token, 229 | token_secret: accessToken.oauth_token_secret, 230 | }; 231 | 232 | // Step 4. Retrieve user's profile information and email address. 233 | request.get({ 234 | url: profileUrl, 235 | qs: {include_email: true}, 236 | oauth: profileOauth, 237 | json: true 238 | }, getUserProfile); 239 | } 240 | 241 | function obtainRequestToken() { 242 | var requestTokenOauth = { 243 | consumer_key: config.twitter.clientID, 244 | consumer_secret: config.twitter.clientSecret, 245 | callback: req.body.redirectUri 246 | }; 247 | // Step 1. Obtain request token for the authorization popup. 248 | request.post({url: requestTokenUrl, oauth: requestTokenOauth}, function (err, response, body) { 249 | var oauthToken = qs.parse(body); 250 | // Step 2. Send OAuth token back to open the authorization screen. 251 | res.send(oauthToken); 252 | }); 253 | } 254 | 255 | function exchangeOauthToken() { 256 | var accessTokenOauth = { 257 | consumer_key: config.twitter.clientID, 258 | consumer_secret: config.twitter.clientSecret, 259 | token: req.body.oauth_token, 260 | verifier: req.body.oauth_verifier 261 | }; 262 | // Step 3. Exchange oauth token and oauth verifier for access token. 263 | request.post({url: accessTokenUrl, oauth: accessTokenOauth}, verifyAccesToken); 264 | } 265 | 266 | // Part 1 of 2: Initial request from Satellizer. 267 | if (!req.body.oauth_token || !req.body.oauth_verifier) { 268 | obtainRequestToken(); 269 | } else { 270 | // Part 2 of 2: Second request after Authorize app is clicked. 271 | exchangeOauthToken(); 272 | } 273 | } 274 | 275 | exports.googleSocailUser = function (req, res) { 276 | var accessTokenUrl = 'https://accounts.google.com/o/oauth2/token'; 277 | var peopleApiUrl = 'https://www.googleapis.com/plus/v1/people/me/openIdConnect'; 278 | var params = { 279 | code: req.body.code, 280 | client_id: req.body.clientId, 281 | client_secret: config.google.clientSecret, 282 | redirect_uri: req.body.redirectUri, 283 | grant_type: 'authorization_code' 284 | }; 285 | 286 | function sendResponse(err, user, info) { 287 | if (err) { 288 | return res.send({status: "error", error: err}); 289 | } 290 | if (!user) { 291 | return res.send({status: "error", error: info}); 292 | } 293 | req.login(user, function (err) { 294 | if (err) { 295 | return res.send({status: "error", error: {message: 'Login failed'}}); 296 | } 297 | return res.send({status: "success", data: {user: user}}); 298 | }); 299 | } 300 | 301 | function retrivedInfo(err, response, profile) { 302 | if (profile.error) { 303 | return res.status(500).send({message: profile.error.message}); 304 | } 305 | // Authenticate User 306 | authenticateSocialUser(profile, sendResponse); 307 | } 308 | 309 | function getAccessToken(err, response, token) { 310 | var accessToken = token.access_token; 311 | var headers = {Authorization: 'Bearer ' + accessToken}; 312 | 313 | // Step 2. Retrieve profile information about the current user. 314 | request.get({url: peopleApiUrl, headers: headers, json: true}, retrivedInfo); 315 | } 316 | 317 | // Step 1. Exchange authorization code for access token. 318 | request.post(accessTokenUrl, {json: true, form: params}, getAccessToken); 319 | } 320 | -------------------------------------------------------------------------------- /app/models/article.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(sequelize, DataTypes) { 4 | 5 | var Article = sequelize.define('Article', { 6 | title: DataTypes.STRING, 7 | content: DataTypes.TEXT 8 | }, 9 | { 10 | associate: function(models){ 11 | Article.belongsTo(models.User); 12 | } 13 | } 14 | ); 15 | 16 | return Article; 17 | }; 18 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * User Model 5 | */ 6 | 7 | var crypto = require('crypto'); 8 | 9 | module.exports = function(sequelize, DataTypes) { 10 | 11 | var User = sequelize.define('User', 12 | { 13 | name: DataTypes.STRING, 14 | email: DataTypes.STRING, 15 | username: DataTypes.STRING, 16 | hashedPassword: DataTypes.STRING, 17 | provider: DataTypes.STRING, 18 | salt: DataTypes.STRING, 19 | facebookUserId: DataTypes.INTEGER, 20 | twitterUserId: DataTypes.INTEGER, 21 | twitterKey: DataTypes.STRING, 22 | twitterSecret: DataTypes.STRING, 23 | github: DataTypes.STRING, 24 | openId: DataTypes.STRING 25 | }, 26 | { 27 | instanceMethods: { 28 | toJSON: function () { 29 | var values = this.get(); 30 | delete values.hashedPassword; 31 | delete values.salt; 32 | return values; 33 | }, 34 | makeSalt: function() { 35 | return crypto.randomBytes(16).toString('base64'); 36 | }, 37 | authenticate: function(plainText){ 38 | return this.encryptPassword(plainText, this.salt) === this.hashedPassword; 39 | }, 40 | encryptPassword: function(password, salt) { 41 | if (!password || !salt) { 42 | return ''; 43 | } 44 | salt = new Buffer(salt, 'base64'); 45 | return crypto.pbkdf2Sync(password, salt, 10000, 64).toString('base64'); 46 | } 47 | }, 48 | associate: function(models) { 49 | User.hasMany(models.Article); 50 | } 51 | } 52 | ); 53 | 54 | return User; 55 | }; 56 | -------------------------------------------------------------------------------- /app/routes/articles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var users = require('../../app/controllers/users'), 7 | articles = require('../../app/controllers/articles'); 8 | 9 | module.exports = function(app) { 10 | // Article Routes 11 | app.route('/articles') 12 | .get(articles.all) 13 | .post(users.requiresLogin, articles.create); 14 | app.route('/articles/:articleId') 15 | .get(articles.show) 16 | .put(users.requiresLogin, articles.hasAuthorization, articles.update) 17 | .delete(users.requiresLogin, articles.hasAuthorization, articles.destroy); 18 | 19 | // Finish with setting up the articleId param 20 | // Note: the articles.article function will be called everytime then it will call the next function. 21 | app.param('articleId', articles.article); 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /app/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(app) { 4 | // Home route 5 | var index = require('../../app/controllers/index'); 6 | app.get('/', index.render); 7 | }; 8 | 9 | -------------------------------------------------------------------------------- /app/routes/users.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'); 11 | 12 | // User Routes 13 | app.get('/signout', users.signout); 14 | app.get('/users/me', users.me); 15 | 16 | // Setting up the users api 17 | app.post('/users', users.create); 18 | 19 | // Setting the local strategy route 20 | app.post('/users/session', passport.authenticate('local', { 21 | failureRedirect: '/signin', 22 | failureFlash: true 23 | }), users.session); 24 | 25 | 26 | // Setting social authentication routes 27 | 28 | // Setting the facebook oauth route 29 | 30 | app.post('/auth/facebook/token', users.facebookUser); 31 | 32 | 33 | app.post('/auth/google', users.googleSocailUser); 34 | 35 | // Setting the twitter oauth route 36 | app.post('/auth/twitter', users.twitterSocialUser); 37 | 38 | // Finish with setting up the userId param 39 | app.param('userId', users.user); 40 | 41 | 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /app/views/404.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block main 4 | h1 Oops something went wrong 5 | br 6 | span 404 7 | 8 | block content 9 | #error-message-box 10 | #error-stack-trace 11 | pre 12 | code!= error 13 | 14 | -------------------------------------------------------------------------------- /app/views/500.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block main 4 | h1 Oops something went wrong 5 | br 6 | span 500 7 | 8 | block content 9 | #error-message-box 10 | #error-stack-trace 11 | pre 12 | code!= error 13 | -------------------------------------------------------------------------------- /app/views/includes/foot.jade: -------------------------------------------------------------------------------- 1 | //AngularJS 2 | script(type='text/javascript', src='/lib/angular/angular.js') 3 | script(type='text/javascript', src='/lib/angular-cookies/angular-cookies.js') 4 | script(type='text/javascript', src='/lib/angular-resource/angular-resource.js') 5 | script(type='text/javascript', src='/lib/angular-ui-router/release/angular-ui-router.min.js') 6 | 7 | //Angular UI 8 | script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap.js') 9 | script(type='text/javascript', src='/lib/angular-bootstrap/ui-bootstrap-tpls.js') 10 | script(type='text/javascript', src='/lib/angular-ui-utils/modules/route/route.js') 11 | script(type='text/javascript', src='/lib/satellizer/satellizer.min.js') 12 | script(type='text/javascript', src='/js/FbSdk.js') 13 | script(type='text/javascript', src='/lib/social/angular-fblogin.js') 14 | 15 | //Application Init 16 | script(type='text/javascript', src='/js/app.js') 17 | script(type='text/javascript', src='/js/config.js') 18 | script(type='text/javascript', src='/js/directives.js') 19 | script(type='text/javascript', src='/js/filters.js') 20 | 21 | //Application Services 22 | script(type='text/javascript', src='/js/services/global.js') 23 | script(type='text/javascript', src='/js/services/articles.js') 24 | script(type='text/javascript', src='/js/services/authenticate.js') 25 | 26 | //Application Controllers 27 | script(type='text/javascript', src='/js/controllers/articles.js') 28 | script(type='text/javascript', src='/js/controllers/index.js') 29 | script(type='text/javascript', src='/js/controllers/header.js') 30 | script(type='text/javascript', src='/js/controllers/users/auth.js') 31 | script(type='text/javascript', src='/js/controllers/users/signIn.js') 32 | script(type='text/javascript', src='/js/controllers/users/signUp.js') 33 | script(type='text/javascript', src='/js/init.js') 34 | 35 | 36 | if (process.env.NODE_ENV == 'development') 37 | //Livereload script rendered 38 | script(type='text/javascript', src='http://localhost:35729/livereload.js') 39 | -------------------------------------------------------------------------------- /app/views/includes/head.jade: -------------------------------------------------------------------------------- 1 | head 2 | meta(charset='utf-8') 3 | meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1') 4 | meta(name='viewport', content='width=device-width,initial-scale=1') 5 | 6 | title= appName+' - '+title 7 | meta(http-equiv='Content-type', content='text/html;charset=UTF-8') 8 | meta(name="keywords", content="node.js, express, mongoose, mongodb, angularjs") 9 | meta(name="description", content="MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).") 10 | 11 | link(href='/img/icons/favicon.ico', rel='shortcut icon', type='image/x-icon') 12 | 13 | meta(property='fb:app_id', content='APP_ID') 14 | meta(property='og:title', content='#{appName} - #{title}') 15 | meta(property='og:description', content='MEAN - A Modern Stack: MongoDB, ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).') 16 | meta(property='og:type', content='website') 17 | meta(property='og:url', content='APP_URL') 18 | meta(property='og:image', content='APP_LOGO') 19 | meta(property='og:site_name', content='MEAN - A Modern Stack') 20 | meta(property='fb:admins', content='APP_ADMIN') 21 | 22 | link(rel='stylesheet', href='/lib/bootstrap/docs/assets/css/bootstrap.css') 23 | //- link(rel='stylesheet', href='/lib/bootstrap/dist/css/bootstrap-responsive.css') 24 | link(rel='stylesheet', href='/css/common.css') 25 | 26 | link(rel='stylesheet', href='/css/views/articles.css') 27 | 28 | //if lt IE 9 29 | script(src='http://html5shim.googlecode.com/svn/trunk/html5.js') 30 | -------------------------------------------------------------------------------- /app/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block content 4 | section(ui-view) 5 | script(type="text/javascript"). 6 | window.user = !{user}; 7 | -------------------------------------------------------------------------------- /app/views/layouts/default.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en', xmlns='http://www.w3.org/1999/xhtml', xmlns:fb='https://www.facebook.com/2008/fbml', itemscope='itemscope', itemtype='http://schema.org/Product') 3 | base(href='/') 4 | include ../includes/head 5 | body 6 | .navbar.navbar-inverse.navbar-fixed-top(data-ng-include="'views/header.html'", data-role="navigation") 7 | section.content 8 | section.container 9 | block content 10 | include ../includes/foot 11 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mean", 3 | "version": "0.1.0", 4 | "dependencies": { 5 | "angular": "latest", 6 | "angular-resource": "latest", 7 | "angular-cookies": "latest", 8 | "angular-mocks": "latest", 9 | "angular-route": "latest", 10 | "bootstrap": "2.3.2", 11 | "angular-bootstrap": "0.7.0", 12 | "angular-ui-utils": "0.0.4", 13 | "angular-ui-router": "~0.3.1", 14 | "satellizer": "^0.15.5", 15 | "bower": "*", 16 | "install": "^1.0.4", 17 | "angular-fblogin": "*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var nconf = require('nconf'), 4 | json5 = require('json5'), 5 | _ = require('lodash'), 6 | glob = require('glob'), 7 | path = require('path'), 8 | fs = require('fs'), 9 | StandardError = require('standard-error'); 10 | 11 | 12 | var rootPath = path.normalize(__dirname + '/..'); 13 | 14 | // Load app configuration 15 | var computedConfig = { 16 | root: rootPath, 17 | modelsDir : rootPath + '/app/models' 18 | }; 19 | 20 | // 21 | // Setup nconf to use (in-order): 22 | // 1. Locally computed config 23 | // 2. Command-line arguments 24 | // 3. Some Environment variables 25 | // 4. Some defaults 26 | // 5. Environment specific config file located at './env/.json' 27 | // 6. Shared config file located at './env/all.json' 28 | // 29 | nconf.argv() 30 | .env(['PORT','NODE_ENV','FORCE_DB_SYNC','forceSequelizeSync'])// Load select environment variables 31 | .defaults({store:{ 32 | NODE_ENV:'development' 33 | }}); 34 | var envConfigPath = rootPath + '/config/env/'+nconf.get('NODE_ENV')+'.json5'; 35 | try{ 36 | if(!fs.statSync(envConfigPath).isFile()){ 37 | throw new Error(); // throw error to trigger catch 38 | } 39 | } 40 | catch(err){ 41 | throw new StandardError('Environment specific config file not found! Throwing up! (NODE_ENV=' 42 | +nconf.get('NODE_ENV')+')'); 43 | } 44 | nconf.file(nconf.get('NODE_ENV'),{ file: envConfigPath, type:'file', format:json5 }) 45 | .file('shared',{ file: rootPath+ '/config/env/all.json5', type:'file', format:json5 }) 46 | .add('base-defaults',{type:'literal', store:{ 47 | PORT:5555 48 | }}) 49 | .overrides({store:computedConfig}); 50 | 51 | module.exports = nconf.get(); 52 | /** 53 | * Get files by glob patterns 54 | */ 55 | module.exports.getGlobbedFiles = function(globPatterns, removeRoot) { 56 | // For context switching 57 | var _this = this; 58 | 59 | // URL paths regex 60 | var urlRegex = new RegExp('^(?:[a-z]+:)?\/\/', 'i'); 61 | 62 | // The output array 63 | var output = []; 64 | 65 | // If glob pattern is array so we use each pattern in a recursive way, otherwise we use glob 66 | if (_.isArray(globPatterns)) { 67 | globPatterns.forEach(function(globPattern) { 68 | output = _.union(output, _this.getGlobbedFiles(globPattern, removeRoot)); 69 | }); 70 | } else if (_.isString(globPatterns)) { 71 | if (urlRegex.test(globPatterns)) { 72 | output.push(globPatterns); 73 | } else { 74 | var files = glob(globPatterns, { sync: true }); 75 | 76 | if (removeRoot) { 77 | files = files.map(function(file) { 78 | return file.replace(removeRoot, ''); 79 | }); 80 | } 81 | 82 | output = _.union(output, files); 83 | } 84 | } 85 | 86 | return output; 87 | }; 88 | 89 | /** 90 | * Get the modules JavaScript files 91 | */ 92 | module.exports.getJavaScriptAssets = function(includeTests) { 93 | var output = this.getGlobbedFiles(this.assets.lib.js.concat(this.assets.js), 'public/'); 94 | 95 | // To include tests 96 | if (includeTests) { 97 | output = _.union(output, this.getGlobbedFiles(this.assets.tests)); 98 | } 99 | 100 | return output; 101 | }; 102 | 103 | /** 104 | * Get the modules CSS files 105 | */ 106 | module.exports.getCSSAssets = function() { 107 | var output = this.getGlobbedFiles(this.assets.lib.css.concat(this.assets.css), 'public/'); 108 | return output; 109 | }; 110 | -------------------------------------------------------------------------------- /config/env/all.json5: -------------------------------------------------------------------------------- 1 | { 2 | PORT: 3000, // capital PORT allows auto override by common env variable if it exists 3 | FORCE_DB_SYNC: "false", // must be string to allow environment variable override 4 | enableSequelizeLog: "true", // type chosen as string for no particular reason 5 | expressSessionSecret: '$uper$ecret$e$$ionKey' // replace with your own in production 6 | } 7 | -------------------------------------------------------------------------------- /config/env/development.json5.sample: -------------------------------------------------------------------------------- 1 | { 2 | // This is your MYSQL Database configuration 3 | db: { 4 | name: "mean_relational", 5 | password: "", 6 | username: "root", 7 | host: "localhost", 8 | port: 3306 9 | }, 10 | app: { 11 | name: "M*EAN Stack Relational - Development" 12 | }, 13 | // You will need to get your own client id's before this will work properly 14 | facebook: { 15 | clientID: "", 16 | clientSecret: "", 17 | callbackURL: "http://localhost:3000/auth/facebook/callback" 18 | }, 19 | twitter: { 20 | clientID: "", 21 | clientSecret: "", 22 | callbackURL: "http://localhost:3000/auth/twitter/callback" 23 | }, 24 | google: { 25 | realm: "http://localhost:3000/", 26 | callbackURL: "http://localhost:3000/auth/google/callback" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /config/env/production.json5.sample: -------------------------------------------------------------------------------- 1 | { 2 | // This is your MYSQL Database configuration 3 | db: { 4 | name: "mean_relational", 5 | password: "", 6 | username: "root", 7 | host:"localhost", 8 | port:3306 9 | }, 10 | app: { 11 | name: "M*EAN Stack Relational - Production" 12 | }, 13 | // You will need to get your own client id's before this will work properly 14 | facebook: { 15 | clientID: "", 16 | clientSecret: "", 17 | callbackURL: "http://localhost:3000/auth/facebook/callback" 18 | }, 19 | twitter: { 20 | clientID: "", 21 | clientSecret: "", 22 | callbackURL: "http://localhost:3000/auth/twitter/callback" 23 | }, 24 | google: { 25 | realm: "http://localhost:3000/", 26 | callbackURL: "http://localhost:3000/auth/google/callback" 27 | } 28 | } -------------------------------------------------------------------------------- /config/env/test.json5.sample: -------------------------------------------------------------------------------- 1 | { 2 | PORT: 3001, 3 | // This is your MYSQL test database configuration 4 | db: { 5 | name: "mean_relational_test", 6 | password: "", 7 | username: "root", 8 | host: "localhost", 9 | port: 3306 10 | }, 11 | app: { 12 | name: "M*EAN Stack Relational - Test" 13 | }, 14 | // You will need to get your own client id's before this will work properly 15 | facebook: { 16 | clientID: "", 17 | clientSecret: "", 18 | callbackURL: "http://localhost:3000/auth/facebook/callback" 19 | }, 20 | twitter: { 21 | clientID: "", 22 | clientSecret: "", 23 | callbackURL: "http://localhost:3000/auth/twitter/callback" 24 | }, 25 | google: { 26 | realm: "http://localhost:3000/", 27 | callbackURL: "http://localhost:3000/auth/google/callback" 28 | } 29 | } -------------------------------------------------------------------------------- /config/env/travis.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": "mongodb://localhost/mean-travis", 3 | "port": 3001, 4 | "app": { 5 | "name": "MEAN - A Modern Stack - Test on travis" 6 | }, 7 | "facebook": { 8 | "clientID": "APP_ID", 9 | "clientSecret": "APP_SECRET", 10 | "callbackURL": "http://localhost:3000/auth/facebook/callback" 11 | }, 12 | "twitter": { 13 | "clientID": "CONSUMER_KEY", 14 | "clientSecret": "CONSUMER_SECRET", 15 | "callbackURL": "http://localhost:3000/auth/twitter/callback" 16 | }, 17 | "github": { 18 | "clientID": "APP_ID", 19 | "clientSecret": "APP_SECRET", 20 | "callbackURL": "http://localhost:3000/auth/github/callback" 21 | }, 22 | "google": { 23 | "clientID": "APP_ID", 24 | "clientSecret": "APP_SECRET", 25 | "callbackURL": "http://localhost:3000/auth/google/callback" 26 | } 27 | } -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var express = require('express'); 7 | var flash = require('connect-flash'); 8 | var helpers = require('view-helpers'); 9 | var compression = require('compression'); 10 | var favicon = require('serve-favicon'); 11 | var logger = require('morgan'); 12 | var cookieParser = require('cookie-parser'); 13 | var bodyParser = require('body-parser'); 14 | var methodOverride = require('method-override'); 15 | var path = require('path'); 16 | var sessionMiddleware = require('./middlewares/session'); 17 | var config = require('./config'); 18 | var winston = require('./winston'); 19 | 20 | module.exports = function(app, passport) { 21 | 22 | winston.info('Initializing Express'); 23 | 24 | app.set('showStackError', true); 25 | 26 | //Prettify HTML 27 | app.locals.pretty = true; 28 | 29 | //Should be placed before express.static 30 | app.use(compression({ 31 | filter: function(req, res) { 32 | return (/json|text|javascript|css/).test(res.getHeader('Content-Type')); 33 | }, 34 | level: 9 35 | })); 36 | 37 | //Setting the fav icon and static folder 38 | app.use(favicon(config.root + '/public/img/icons/favicon.ico')); 39 | app.use(express.static(config.root + '/public')); 40 | 41 | //Don't use logger for test env 42 | if (config.NODE_ENV !== 'test') { 43 | app.use(logger('dev', { "stream": winston.stream })); 44 | } 45 | 46 | //Set views path, template engine and default layout 47 | app.set('views', config.root + '/app/views'); 48 | app.set('view engine', 'jade'); 49 | 50 | //Enable jsonp 51 | app.enable("jsonp callback"); 52 | 53 | //cookieParser should be above session 54 | app.use(cookieParser()); 55 | 56 | // request body parsing middleware should be above methodOverride 57 | app.use(bodyParser.urlencoded({ extended: true })); 58 | app.use(bodyParser.json()); 59 | app.use(methodOverride()); 60 | 61 | //express session configuration 62 | app.use(sessionMiddleware); 63 | 64 | //connect flash for flash messages 65 | app.use(flash()); 66 | 67 | //dynamic helpers 68 | app.use(helpers(config.app.name)); 69 | 70 | //use passport session 71 | app.use(passport.initialize()); 72 | app.use(passport.session()); 73 | 74 | // Globbing routing files 75 | config.getGlobbedFiles('./app/routes/**/*.js').forEach(function(routePath) { 76 | require(path.resolve(routePath))(app); 77 | }); 78 | 79 | app.get('*', function (req, res, next) { 80 | res.render('index'); 81 | }); 82 | 83 | app.use(function(err, req, res, next) { 84 | 85 | //Log it 86 | winston.error(err); 87 | 88 | //Error page 89 | res.status(500).render('500', { 90 | error: err.stack 91 | }); 92 | }); 93 | 94 | }; 95 | -------------------------------------------------------------------------------- /config/middlewares/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('./../config'), 4 | session = require('express-session'), 5 | db = require('./../sequelize') ; 6 | 7 | var sequelizeStore = require('express-sequelize-session')(session.Store); 8 | 9 | var sessionMiddleware = session({ 10 | resave: true, 11 | saveUninitialized: true, 12 | store: new sequelizeStore(db.sequelize), 13 | cookie:{maxAge:1000*3600*24*7}, //remember for 7 days 14 | secret: config.expressSessionSecret/*||'$uper$ecret$e$$ionKey'*/ 15 | }); 16 | 17 | module.exports = sessionMiddleware; 18 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var passport = require('passport'), 4 | _ = require('lodash'); 5 | // These are different types of authentication strategies that can be used with Passport. 6 | var LocalStrategy = require('passport-local').Strategy, 7 | FacebookTokenStrategy = require('passport-facebook-token'), 8 | config = require('./config'), 9 | db = require('./sequelize'), 10 | winston = require('./winston'); 11 | 12 | //Serialize sessions 13 | passport.serializeUser(function(user, done) { 14 | done(null, user.id); 15 | }); 16 | 17 | passport.deserializeUser(function(id, done) { 18 | db.User.find({where: {id: id}}).then(function(user){ 19 | if(!user){ 20 | winston.warn('Logged in user not in database, user possibly deleted post-login'); 21 | return done(null, false); 22 | } 23 | winston.info('Session: { id: ' + user.id + ', username: ' + user.username + ' }'); 24 | done(null, user); 25 | }).catch(function(err){ 26 | done(err, null); 27 | }); 28 | }); 29 | 30 | //Use local strategy 31 | passport.use(new LocalStrategy({ 32 | usernameField: 'email', 33 | passwordField: 'password' 34 | }, 35 | function(email, password, done) { 36 | db.User.find({ where: { email: email }}).then(function(user) { 37 | if (!user) { 38 | done(null, false, { message: 'Unknown user' }); 39 | } else if (!user.authenticate(password)) { 40 | done(null, false, { message: 'Invalid password'}); 41 | } else { 42 | winston.info('Login (local) : { id: ' + user.id + ', username: ' + user.username + ' }'); 43 | done(null, user); 44 | } 45 | }).catch(function(err){ 46 | done(err); 47 | }); 48 | } 49 | )); 50 | 51 | passport.use(new FacebookTokenStrategy({ 52 | clientID: config.facebook.clientID, 53 | clientSecret: config.facebook.clientSecret, 54 | profileFields: ['id', 'first_name', 'last_name', 'email', 'photos'] 55 | }, function (accessToken, refreshToken, profile, done) { 56 | 57 | db.User.find({where : {email: profile.emails[0].value}}).then(function(user){ 58 | if(!user){ 59 | db.User.create({ 60 | name: profile.name.givenName || '', 61 | email: profile.emails[0].value, 62 | username: profile.name.givenName || '', 63 | provider: 'facebook', 64 | facebookUserId: profile.id 65 | }).then(function(u){ 66 | winston.info('New User (facebook) : { id: ' + u.id + ', username: ' + u.username + ' }'); 67 | done(null, u); 68 | }) 69 | } else { 70 | winston.info('Login (facebook) : { id: ' + user.id + ', username: ' + user.username + ' }'); 71 | done(null, user); 72 | } 73 | }).catch(function(err){ 74 | done(err, null); 75 | }); 76 | 77 | } 78 | 79 | )); 80 | 81 | module.exports = passport; 82 | 83 | -------------------------------------------------------------------------------- /config/sequelize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var Sequelize = require('sequelize'); 6 | var _ = require('lodash'); 7 | var config = require('./config'); 8 | var winston = require('./winston'); 9 | var db = {}; 10 | 11 | 12 | winston.info('Initializing Sequelize...'); 13 | 14 | // create your instance of sequelize 15 | var onHeroku = !!process.env.DYNO; 16 | winston.info('Checking if running on Heroku: ',onHeroku); 17 | 18 | var sequelize = onHeroku ? 19 | new Sequelize(process.env.DATABASE_URL, { 20 | dialect: 'postgres', 21 | protocol: 'postgres', 22 | dialectOptions: { 23 | ssl: true 24 | } 25 | }) 26 | : 27 | new Sequelize(config.db.name, config.db.username, config.db.password, { 28 | host: config.db.host, 29 | port: config.db.port, 30 | dialect: 'mysql', 31 | storage: config.db.storage, 32 | logging: config.enableSequelizeLog === 'true' ? winston.verbose : false 33 | }); 34 | 35 | // loop through all files in models directory ignoring hidden files and this file 36 | fs.readdirSync(config.modelsDir) 37 | .filter(function (file) { 38 | return (file.indexOf('.') !== 0) && (file !== 'index.js') 39 | }) 40 | // import model files and save model names 41 | .forEach(function (file) { 42 | winston.info('Loading model file ' + file); 43 | var model = sequelize.import(path.join(config.modelsDir, file)); 44 | db[model.name] = model; 45 | }); 46 | 47 | // invoke associations on each of the models 48 | Object.keys(db).forEach(function (modelName) { 49 | if (db[modelName].options.hasOwnProperty('associate')) { 50 | db[modelName].options.associate(db) 51 | } 52 | }); 53 | 54 | // Synchronizing any model changes with database. 55 | // set FORCE_DB_SYNC=true in the environment, or the program parameters to drop the database, 56 | // and force model changes into it, if required; 57 | // Caution: Do not set FORCE_DB_SYNC to true for every run to avoid losing data with restarts 58 | sequelize 59 | .sync({ 60 | force: config.FORCE_DB_SYNC === 'true', 61 | logging: config.enableSequelizeLog === 'true' ? winston.verbose : false 62 | }) 63 | .then(function () { 64 | winston.info("Database " + (config.FORCE_DB_SYNC === 'true' ? "*DROPPED* and " : "") + "synchronized"); 65 | }).catch(function (err) { 66 | winston.error("An error occurred: ", err); 67 | }); 68 | 69 | // assign the sequelize variables to the db object and returning the db. 70 | module.exports = _.extend({ 71 | sequelize: sequelize, 72 | Sequelize: Sequelize 73 | }, db); -------------------------------------------------------------------------------- /config/winston.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Created by Junaid Anwar on 5/28/15. 5 | */ 6 | var winston = require('winston'); 7 | var logger = new (winston.Logger)(); 8 | 9 | logger.add(winston.transports.Console, { 10 | level: 'verbose', 11 | prettyPrint: true, 12 | colorize: true, 13 | silent: false, 14 | timestamp: false 15 | }); 16 | 17 | logger.stream = { 18 | write: function(message, encoding){ 19 | logger.info(message); 20 | } 21 | }; 22 | 23 | module.exports = logger; 24 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | // Project Configuration 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | watch: { 8 | jade: { 9 | files: ['app/views/**'], 10 | options: { 11 | livereload: true, 12 | }, 13 | }, 14 | js: { 15 | files: ['public/js/**', 'app/**/*.js', 'config/**/*.js'], 16 | tasks: ['jshint'], 17 | options: { 18 | livereload: true, 19 | }, 20 | }, 21 | html: { 22 | files: ['public/views/**'], 23 | options: { 24 | livereload: true, 25 | }, 26 | }, 27 | css: { 28 | files: ['public/css/**'], 29 | options: { 30 | livereload: true 31 | } 32 | } 33 | }, 34 | jshint: { 35 | all:['gruntfile.js', 'public/js/**/*.js', 'test/mocha/**/*.js', 'test/karma/**/*.js', 'app/**/*.js'], 36 | options: { 37 | jshintrc: '.jshintrc', 38 | reporter: require('jshint-stylish') 39 | } 40 | }, 41 | copy: { 42 | options: { 43 | punctuation: '' 44 | }, 45 | js: { 46 | files: [ 47 | {cwd: 'bower_components/angular-bootstrap', src: ['**/*.js'], dest: 'public/lib/angular-bootstrap', expand: true}, 48 | {cwd: 'bower_components/angular-cookies', src: ['angular-cookies*'], dest: 'public/lib/angular-cookies', expand: true}, 49 | {cwd: 'bower_components/angular-mocks', src: ['**/*.js'], dest: 'public/lib/angular-mocks', expand: true}, 50 | {cwd: 'bower_components/angular-resource', src: ['angular-resource*'], dest: 'public/lib/angular-resource', expand: true}, 51 | {cwd: 'bower_components/angular-route', src: ['angular-route*'], dest: 'public/lib/angular-route', expand: true}, 52 | {cwd: 'bower_components/angular', src: ['angular*'], dest: 'public/lib/angular', expand: true}, 53 | {cwd: 'bower_components/angular-ui-utils/demo', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/demo', expand: true}, 54 | {cwd: 'bower_components/angular-ui-utils/test', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/test', expand: true}, 55 | {cwd: 'bower_components/angular-ui-utils/modules', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules', expand: true}, 56 | {cwd: 'bower_components/angular-ui-utils/modules/event', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/event', expand: true}, 57 | {cwd: 'bower_components/angular-ui-utils/modules/format', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/format', expand: true}, 58 | {cwd: 'bower_components/angular-ui-utils/modules/highlight', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/highlight', expand: true}, 59 | {cwd: 'bower_components/angular-ui-utils/modules/ie-shiv', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/ie-shiv', expand: true}, 60 | {cwd: 'bower_components/angular-ui-utils/modules/indeterminate', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/indeterminate', expand: true}, 61 | {cwd: 'bower_components/angular-ui-utils/modules/inflector', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/inflector', expand: true}, 62 | {cwd: 'bower_components/angular-ui-utils/modules/jq', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/jq', expand: true}, 63 | {cwd: 'bower_components/angular-ui-utils/modules/keypress', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/keypress', expand: true}, 64 | {cwd: 'bower_components/angular-ui-utils/modules/mask', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/mask', expand: true}, 65 | {cwd: 'bower_components/angular-ui-utils/modules/reset', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/reset', expand: true}, 66 | {cwd: 'bower_components/angular-ui-utils/modules/reset/stylesheets', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/reset/stylesheets', expand: true}, 67 | {cwd: 'bower_components/angular-ui-utils/modules/route', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/route', expand: true}, 68 | {cwd: 'bower_components/angular-ui-utils/modules/scrollfix', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/scrollfix', expand: true}, 69 | {cwd: 'bower_components/angular-ui-utils/modules/showhide', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/showhide', expand: true}, 70 | {cwd: 'bower_components/angular-ui-utils/modules/unique', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/unique', expand: true}, 71 | {cwd: 'bower_components/angular-ui-utils/modules/validate', src: ['**/*.js'], dest: 'public/lib/angular-ui-utils/modules/validate', expand: true}, 72 | {cwd: 'bower_components/bootstrap/js', src: ['*.js'], dest: 'public/lib/bootstrap/js', expand: true}, 73 | {cwd: 'bower_components/bootstrap/less', src: ['*.less'], dest: 'public/lib/bootstrap/less', expand: true}, 74 | {cwd: 'bower_components/bootstrap/docs/assets/css', src: ['*.*'], dest: 'public/lib/bootstrap/docs/assets/css', expand: true}, 75 | {cwd: 'bower_components/bootstrap/docs/assets/ico', src: ['*.*'], dest: 'public/lib/bootstrap/docs/assets/ico', expand: true}, 76 | {cwd: 'bower_components/bootstrap/docs/assets/img', src: ['*.*'], dest: 'public/lib/bootstrap/docs/assets/img', expand: true}, 77 | {cwd: 'bower_components/bootstrap/docs/assets/js', src: ['*.*'], dest: 'public/lib/bootstrap/docs/assets/js', expand: true}, 78 | {cwd: 'bower_components/satellizer/dist', src: ['**/*.js'], dest: 'public/lib/satellizer', expand: true}, 79 | {cwd: 'bower_components/angular-fblogin/dist', src: ['**/*.js'], dest: 'public/lib/social', expand: true}, 80 | {cwd: 'bower_components/jquery', src: ['jquery*'], dest: 'public/lib/jquery', expand: true}, 81 | {cwd: 'bower_components/angular-ui-router', src: ['release/*.js'], dest: 'public/lib/angular-ui-router', expand: true} 82 | 83 | ] 84 | } 85 | }, 86 | nodemon: { 87 | dev: { 88 | script: 'app.js', 89 | options: { 90 | args: ['--color'], 91 | ignore: ['README.md', 'node_modules/**', '.DS_Store'], 92 | ext: 'js', 93 | watch: ['app', 'config', 'app.js', 'gruntfile.js'], 94 | delay: 1000, 95 | env: { 96 | PORT: 3000 97 | }, 98 | cwd: __dirname 99 | } 100 | } 101 | }, 102 | concurrent: { 103 | tasks: ['nodemon', 'watch'], 104 | options: { 105 | logConcurrentOutput: true 106 | } 107 | }, 108 | mochaTest: { 109 | options: { 110 | reporter: 'spec' 111 | }, 112 | src: ['test/mocha/**/*.js'] 113 | }, 114 | env: { 115 | test: { 116 | NODE_ENV: 'test' 117 | } 118 | }, 119 | karma: { 120 | unit: { 121 | configFile: 'test/karma/karma.conf.js' 122 | } 123 | } 124 | }); 125 | 126 | //Load NPM tasks 127 | grunt.loadNpmTasks('grunt-contrib-watch'); 128 | grunt.loadNpmTasks('grunt-contrib-jshint'); 129 | grunt.loadNpmTasks('grunt-contrib-nodemon'); 130 | grunt.loadNpmTasks('grunt-concurrent'); 131 | grunt.loadNpmTasks('grunt-mocha-test'); 132 | grunt.loadNpmTasks('grunt-karma'); 133 | grunt.loadNpmTasks('grunt-env'); 134 | grunt.loadNpmTasks('grunt-copy'); 135 | 136 | //Making grunt default to force in order not to break the project. 137 | grunt.option('force', true); 138 | 139 | //Default task(s). 140 | grunt.registerTask('default', ['copy', 'jshint', 'concurrent']); 141 | 142 | //Test task. 143 | grunt.registerTask('test', ['env:test', 'mochaTest', 'karma:unit']); 144 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mean-stack-relational", 3 | "description": "M*EAN - A Modern Stack: MySQL(Postgres for Heroku), ExpressJS, AngularJS, NodeJS. (BONUS: Passport User Support).", 4 | "version": "0.2.2", 5 | "private": false, 6 | "author": "Jeff Potter", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/jpotts18/mean-stack-relational.git" 11 | }, 12 | "engines": { 13 | "node": "4.2.x", 14 | "npm": "2.14.x" 15 | }, 16 | "scripts": { 17 | "start": "node node_modules/grunt-cli/bin/grunt", 18 | "test": "node node_modules/grunt-cli/bin/grunt test", 19 | "postinstall": "bower install && cp config/env/development.json5.sample config/env/development.json5 && cp config/env/production.json5.sample config/env/production.json5 && node node_modules/grunt-cli/bin/grunt copy" 20 | }, 21 | "dependencies": { 22 | "async": "latest", 23 | "body-parser": "^1.14.1", 24 | "bower": "latest", 25 | "compression": "^1.6.0", 26 | "connect-flash": "^0.1.1", 27 | "cookie-parser": "^1.4.0", 28 | "express": "^4.13.3", 29 | "express-sequelize-session": "^0.4.0", 30 | "express-session": "^1.12.1", 31 | "glob": "^6.0.1", 32 | "grunt": "^0.4.5", 33 | "grunt-cli": "^0.1.13", 34 | "grunt-copy": "latest", 35 | "grunt-env": "latest", 36 | "jade": "^1.11.0", 37 | "jshint-stylish": "^2.1.0", 38 | "json5": "^0.4.0", 39 | "lodash": "latest", 40 | "method-override": "^2.3.5", 41 | "morgan": "^1.6.1", 42 | "mysql": "^2.9.0", 43 | "nconf": "^0.8.2", 44 | "passport": "^0.3.2", 45 | "passport-facebook": "^2.0.0", 46 | "passport-google": "latest", 47 | "passport-local": "^1.0.0", 48 | "passport-twitter": "^1.0.3", 49 | "pg": "^4.4.3", 50 | "pm2": "^1.0.0", 51 | "proxyquire": "^1.7.3", 52 | "sequelize": "^3.13.0", 53 | "serve-favicon": "^2.3.0", 54 | "standard-error": "^1.1.0", 55 | "view-helpers": "latest", 56 | "winston": "latest", 57 | "request": "2.75.0", 58 | "qs": "6.2.1", 59 | "passport-facebook-token": "3.3.0" 60 | }, 61 | "devDependencies": { 62 | "chai": "^3.2.0", 63 | "grunt-concurrent": "latest", 64 | "grunt-contrib-jshint": "latest", 65 | "grunt-contrib-nodemon": "^0.5.2", 66 | "grunt-contrib-watch": "latest", 67 | "grunt-karma": "~0.6.2", 68 | "grunt-mocha-test": "latest", 69 | "karma": "~0.10.4", 70 | "karma-chrome-launcher": "~0.1.0", 71 | "karma-coffee-preprocessor": "~0.1.0", 72 | "karma-coverage": "~0.1.0", 73 | "karma-firefox-launcher": "~0.1.0", 74 | "karma-html2js-preprocessor": "~0.1.0", 75 | "karma-jasmine": "~0.1.3", 76 | "karma-phantomjs-launcher": "~0.1.0", 77 | "karma-requirejs": "~0.2.0", 78 | "karma-script-launcher": "~0.1.0", 79 | "mocha": "^2.2.5", 80 | "supertest": "latest" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pm2-ecosystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps" : [{ 3 | // mean-stack-relational app 4 | "name" : "msr", 5 | "script" : "app.js", 6 | // by default, the app runs in fork mode, uncomment below to run in cluster mode 7 | //"instances" : 4, 8 | //"exec_mode" : "cluster_mode", // defaults to fork 9 | "args" : ["--color"], 10 | "watch" : true, 11 | "ignore_watch" : ["pids", "logs", "node_modules", "bower_components"], 12 | "merge_logs" : true, // merge logs from all instances in cluster mode 13 | "cwd" : ".", 14 | "error_file" : "./logs/msr.log", 15 | "out_file" : "./logs/msr.log", 16 | "pid_file" : "./pids/msr.pid", 17 | "min_uptime" : "30s", // defaults to 1000s 18 | "max_restarts" : 30, // defaults to 15 19 | "restart_delay" : 1000, 20 | "max_memory_restart" : "8G", // restart app if it reaches an 8GB memory footprint 21 | "env": { 22 | "NODE_ENV": "development" 23 | }, 24 | "env_production" : { 25 | "NODE_ENV": "production" 26 | } 27 | }] 28 | } 29 | -------------------------------------------------------------------------------- /pm2-main.js: -------------------------------------------------------------------------------- 1 | var pm2 = require('pm2'); 2 | 3 | var instances = process.env.WEB_CONCURRENCY || 1; // Set by Heroku or 1 (set -1 to scale to max cpu core - 1) 4 | var maxMemory = process.env.WEB_MEMORY || 512; // 512 is the maximum free Dyno memory on Heroku 5 | 6 | pm2.connect(function() { 7 | pm2.start({ 8 | script : 'app.js', 9 | name : 'msr', // ----> THESE ATTRIBUTES ARE OPTIONAL: 10 | exec_mode : 'cluster', // set to 'cluster' for cluster execution 11 | instances : instances, 12 | max_memory_restart : maxMemory + 'M', // Auto restart if process taking more than XXmo 13 | env: { // If needed declare some environment variables 14 | "NODE_ENV": "production" 15 | } 16 | }, function(err) { 17 | if (err) return console.error('Error while launching applications', err.stack || err); 18 | console.log('PM2 and application has been successfully started'); 19 | 20 | // Display logs in standard output 21 | pm2.launchBus(function(err, bus) { 22 | console.log('[PM2] Log streaming started'); 23 | 24 | bus.on('log:out', function(packet) { 25 | console.log('[App:%s] %s', packet.process.name, packet.data); 26 | }); 27 | 28 | bus.on('log:err', function(packet) { 29 | console.error('[App:%s][Err] %s', packet.process.name, packet.data); 30 | }); 31 | }); 32 | 33 | }); 34 | }); 35 | 36 | -------------------------------------------------------------------------------- /public/css/common.css: -------------------------------------------------------------------------------- 1 | .navbar .nav>li>a.brand { 2 | padding-left:20px; 3 | margin-left:0 4 | } 5 | 6 | .content { 7 | margin-top:50px; 8 | width:100% 9 | } 10 | 11 | footer { 12 | position:fixed; 13 | left:0px; 14 | bottom:0px; 15 | height:30px; 16 | width:100%; 17 | background:#ddd; 18 | -webkit-box-shadow:0 8px 6px 6px black; 19 | -moz-box-shadow:0 8px 6px 6px black; 20 | box-shadow:0 8px 6px 6px black 21 | } 22 | 23 | footer p { 24 | padding:5px 0 12px 10px 25 | } -------------------------------------------------------------------------------- /public/css/views/articles.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | text-align:center 3 | } 4 | 5 | ul.articles li:not(:last-child) { 6 | border-bottom:1px solid #ccc 7 | } -------------------------------------------------------------------------------- /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/img/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/.gitignore -------------------------------------------------------------------------------- /public/img/apple/apple-touch-icon-114x114-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/apple/apple-touch-icon-114x114-precomposed.png -------------------------------------------------------------------------------- /public/img/apple/apple-touch-icon-144x144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/apple/apple-touch-icon-144x144-precomposed.png -------------------------------------------------------------------------------- /public/img/apple/apple-touch-icon-57x57-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/apple/apple-touch-icon-57x57-precomposed.png -------------------------------------------------------------------------------- /public/img/apple/apple-touch-icon-72x72-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/apple/apple-touch-icon-72x72-precomposed.png -------------------------------------------------------------------------------- /public/img/apple/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/apple/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /public/img/apple/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/apple/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/apple/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/apple/splash.png -------------------------------------------------------------------------------- /public/img/apple/splash2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/apple/splash2x.png -------------------------------------------------------------------------------- /public/img/icons/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/icons/facebook.png -------------------------------------------------------------------------------- /public/img/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/icons/favicon.ico -------------------------------------------------------------------------------- /public/img/icons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/icons/github.png -------------------------------------------------------------------------------- /public/img/icons/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/icons/google.png -------------------------------------------------------------------------------- /public/img/icons/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/icons/twitter.png -------------------------------------------------------------------------------- /public/img/loaders/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/loaders/loader.gif -------------------------------------------------------------------------------- /public/img/m_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/m_logo.png -------------------------------------------------------------------------------- /public/img/sprites/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/sprites/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /public/img/sprites/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/img/sprites/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | angular.module('mean', ['ngCookies', 'ngResource', 'ui.router', 'ui.bootstrap', 'ui.route', 'mean.system', 'mean.articles', 'mean.auth','satellizer','angularFblogin']) 2 | .config(function ($authProvider) { 3 | 4 | $authProvider.twitter({ 5 | url: '/auth/twitter', 6 | authorizationEndpoint: 'https://api.twitter.com/oauth/authenticate', 7 | redirectUri: 'http://localhost:3000/auth/twitter/callback', 8 | oauthType: '1.0', 9 | popupOptions: { width: 495, height: 645 } 10 | }); 11 | 12 | $authProvider.google({ 13 | clientId: 'your google client id here', // google client id 14 | url: '/auth/google', 15 | redirectUri: 'http://localhost:3000/auth/google/callback' 16 | }); 17 | 18 | }); 19 | 20 | angular.module('mean.system', []); 21 | angular.module('mean.articles', []); 22 | angular.module('mean.auth', []); -------------------------------------------------------------------------------- /public/js/config.js: -------------------------------------------------------------------------------- 1 | //Setting up route 2 | angular.module('mean').config(['$stateProvider','$urlRouterProvider', function($stateProvider,$urlRouterProvider) { 3 | 4 | $urlRouterProvider.otherwise(function($injector, $location){ 5 | $injector.invoke(['$state', function($state) { 6 | $state.go('404'); 7 | }]); 8 | }); 9 | $stateProvider 10 | .state('home',{ 11 | url : '/', 12 | controller : 'IndexController', 13 | templateUrl: 'views/index.html' 14 | }) 15 | .state('SignIn',{ 16 | url : '/signin', 17 | templateUrl: 'views/users/signin.html' 18 | }) 19 | .state('SignUp',{ 20 | url : '/signup', 21 | templateUrl: 'views/users/signup.html' 22 | }) 23 | .state('articles',{ 24 | url : '/article', 25 | controller : 'ArticlesController', 26 | templateUrl: 'views/articles/list.html' 27 | }) 28 | .state('createArticle',{ 29 | url : '/article/create', 30 | controller : 'ArticlesController', 31 | templateUrl: 'views/articles/create.html' 32 | }) 33 | .state('editArticles',{ 34 | url : '/article/{articleId}/edit', 35 | controller : 'ArticlesController', 36 | templateUrl: 'views/articles/edit.html' 37 | }) 38 | .state('viewArticle',{ 39 | url : '/article/{articleId}', 40 | controller : 'ArticlesController', 41 | templateUrl: 'views/articles/view.html' 42 | }) 43 | .state('404',{ 44 | templateUrl: 'views/404.html' 45 | }) 46 | } 47 | ]); 48 | 49 | //Setting HTML5 Location Mode 50 | angular.module('mean').config(['$locationProvider', function ($locationProvider) { 51 | $locationProvider.html5Mode(true); 52 | 53 | }]); -------------------------------------------------------------------------------- /public/js/controllers/articles.js: -------------------------------------------------------------------------------- 1 | angular.module('mean.articles').controller('ArticlesController', ['$scope', '$stateParams', 'Global', 'Articles', '$state', function ($scope, $stateParams, Global, Articles, $state) { 2 | $scope.global = Global; 3 | 4 | $scope.create = function() { 5 | var article = new Articles({ 6 | title: this.title, 7 | content: this.content 8 | }); 9 | 10 | article.$save(function(response) { 11 | $state.go('viewArticle',{articleId : response.id}) 12 | }); 13 | 14 | this.title = ""; 15 | this.content = ""; 16 | }; 17 | 18 | $scope.remove = function(article) { 19 | if (article) { 20 | article.$remove(); 21 | 22 | for (var i in $scope.articles) { 23 | if ($scope.articles[i] === article) { 24 | $scope.articles.splice(i, 1); 25 | } 26 | } 27 | } 28 | else { 29 | $scope.article.$remove(); 30 | $state.go('articles'); 31 | } 32 | }; 33 | 34 | $scope.update = function() { 35 | var article = $scope.article; 36 | if (!article.updated) { 37 | article.updated = []; 38 | } 39 | article.updated.push(new Date().getTime()); 40 | article.$update(function() { 41 | $state.go('viewArticle',{articleId : article.id}) 42 | 43 | }); 44 | }; 45 | 46 | $scope.find = function() { 47 | Articles.query(function(articles) { 48 | $scope.articles = articles; 49 | }); 50 | }; 51 | 52 | $scope.findOne = function() { 53 | Articles.get({ 54 | articleId: $stateParams.articleId 55 | }, function(article) { 56 | $scope.article = article; 57 | }); 58 | }; 59 | $scope.find = function() { 60 | Articles.query(function(articles) { 61 | $scope.articles = articles; 62 | }); 63 | }; 64 | 65 | 66 | 67 | }]); -------------------------------------------------------------------------------- /public/js/controllers/header.js: -------------------------------------------------------------------------------- 1 | angular.module('mean.system').controller('HeaderController', ['$scope', 'Global', 'SignOut', '$state', function ($scope, Global, SignOut, $state) { 2 | $scope.global = Global; 3 | 4 | $scope.menu = [{ 5 | "title": "Articles", 6 | "state": "articles" 7 | }, { 8 | "title": "Create New Article", 9 | "state": "createArticle" 10 | }]; 11 | 12 | $scope.isCollapsed = false; 13 | 14 | $scope.SignOut = function(){ 15 | SignOut.get(function(response){ 16 | if(response.status === 'success'){ 17 | $scope.global = null; 18 | $state.go('home'); 19 | } 20 | }); 21 | } 22 | 23 | 24 | }]); -------------------------------------------------------------------------------- /public/js/controllers/index.js: -------------------------------------------------------------------------------- 1 | angular.module('mean.system').controller('IndexController', ['$scope', 'Global', function ($scope, Global) { 2 | $scope.global = Global; 3 | }]); -------------------------------------------------------------------------------- /public/js/controllers/users/auth.js: -------------------------------------------------------------------------------- 1 | angular.module('mean.auth').controller('socialAuth', ['$scope', 'Global','$state', '$fblogin', 'SocialAuth','$window','$auth', function ($scope, Global, $state, $fblogin, SocialAuth, $window, $auth) { 2 | $scope.global = Global; 3 | 4 | $scope.menu = [{ 5 | "title": "Articles", 6 | "state": "articles" 7 | }, { 8 | "title": "Create New Article", 9 | "state": "createArticle" 10 | }]; 11 | 12 | $scope.isCollapsed = false; 13 | 14 | $scope.fbAuth = function(){ 15 | $fblogin({ 16 | fbId: "102551953548872", 17 | permissions: 'email,user_birthday', 18 | fields: 'first_name,last_name,email,birthday,picture' 19 | }).then(function () { 20 | SocialAuth.FbLogin(FB.getAuthResponse()).then(function (response) { 21 | if(response.status === 'success' || 200){ 22 | $window.location.href = '/'; 23 | } 24 | }); 25 | }).catch(function () { 26 | $window.location.reload(); 27 | }) 28 | }; 29 | $scope.twitterAuth = function(){ 30 | $auth.authenticate('twitter').then(function(response) { 31 | if(response.status === 'success' || 200){ 32 | $window.location.href = '/'; 33 | } 34 | }); 35 | }; 36 | 37 | $scope.googleAuth = function(){ 38 | 39 | $auth.authenticate('google').then(function(response) { 40 | if(response.status === 'success' || 200){ 41 | $window.location.href = '/'; 42 | } 43 | }); 44 | }; 45 | 46 | 47 | }]); -------------------------------------------------------------------------------- /public/js/controllers/users/signIn.js: -------------------------------------------------------------------------------- 1 | angular.module('mean.auth').controller('signIn', ['$scope', '$window', 'Global', '$state', 'LogIn', function ($scope, $window, Global, $state, LogIn) { 2 | $scope.global = Global; 3 | 4 | 5 | $scope.signIn = function(user) { 6 | 7 | var logIn = new LogIn({ 8 | email: user.email, 9 | password: user.password 10 | }); 11 | 12 | logIn.$save(function(response) { 13 | if(response.status === 'success'){ 14 | $window.location.href = '/'; 15 | } 16 | }); 17 | }; 18 | 19 | 20 | }]); -------------------------------------------------------------------------------- /public/js/controllers/users/signUp.js: -------------------------------------------------------------------------------- 1 | angular.module('mean.auth').controller('signUp', ['$scope', '$window', 'Global','$state', 'SignUp', function ($scope, $window, Global, $state, SignUp) { 2 | $scope.global = Global; 3 | 4 | 5 | $scope.signUp = function(user) { 6 | 7 | var signUp = new SignUp({ 8 | name: user.name, 9 | email: user.email, 10 | username : user.userName, 11 | password : user.password 12 | }); 13 | 14 | signUp.$save(function(response) { 15 | if(response.status === 'success'){ 16 | $window.location.href = '/'; 17 | } 18 | }); 19 | }; 20 | 21 | 22 | }]); -------------------------------------------------------------------------------- /public/js/directives.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/js/directives.js -------------------------------------------------------------------------------- /public/js/filters.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpotts18/mean-stack-relational/14458849606dfe7864956a4265987084070358fa/public/js/filters.js -------------------------------------------------------------------------------- /public/js/init.js: -------------------------------------------------------------------------------- 1 | angular.element(document).ready(function() { 2 | //Fixing facebook bug with redirect 3 | if (window.location.hash === "#_=_") { 4 | window.location.hash = "#!"; 5 | } 6 | 7 | //Then init the app 8 | angular.bootstrap(document, ['mean']); 9 | }); -------------------------------------------------------------------------------- /public/js/services/articles.js: -------------------------------------------------------------------------------- 1 | //Articles service used for articles REST endpoint 2 | angular.module('mean.articles').factory("Articles", ['$resource', function($resource) { 3 | return $resource('articles/:articleId', { 4 | articleId: '@id' 5 | }, { 6 | update: { 7 | method: 'PUT' 8 | } 9 | }); 10 | }]); -------------------------------------------------------------------------------- /public/js/services/authenticate.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('mean.auth').factory("SocialAuth", ['$http', function ($http) { 3 | return { 4 | FbLogin: function (token) { 5 | return $http.post('/auth/facebook/token', {"access_token": token.accessToken}) 6 | .then(function (res) { 7 | return res; 8 | }); 9 | } 10 | } 11 | }]); 12 | angular.module('mean.auth').service("SignOut", ['$resource', function($resource) { 13 | return $resource('/signout'); 14 | }]); 15 | angular.module('mean.auth').service("LogIn", ['$resource', function($resource) { 16 | return $resource('/users/session'); 17 | }]); 18 | angular.module('mean.auth').service("SignUp", ['$resource', function($resource) { 19 | return $resource('/users'); 20 | }]); -------------------------------------------------------------------------------- /public/js/services/global.js: -------------------------------------------------------------------------------- 1 | //Global service for global variables 2 | angular.module('mean.system').factory("Global", [ 3 | function() { 4 | var _this = this; 5 | _this._data = { 6 | user: window.user, 7 | authenticated: !! window.user 8 | }; 9 | return _this._data; 10 | } 11 | ]); 12 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org/ 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /public/views/404.html: -------------------------------------------------------------------------------- 1 |
2 |

ERROR #404

3 |

The link you followed may be broken, or the page may have been removed.

4 |
-------------------------------------------------------------------------------- /public/views/articles/create.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 | 7 |
8 |
9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 |
17 | 18 |
19 |
20 |
21 |
-------------------------------------------------------------------------------- /public/views/articles/edit.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 |
15 | 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |
-------------------------------------------------------------------------------- /public/views/articles/list.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 | {{article.updatedAt | date:'medium'}} / 5 | {{article.User.name}} 6 |

    {{article.title}}

    7 |
    {{article.content}}
    8 |
  • 9 |
10 |

No articles yet.
Why don't you Create One?

11 |
-------------------------------------------------------------------------------- /public/views/articles/view.html: -------------------------------------------------------------------------------- 1 |
2 | {{article.updatedAt | date:'medium'}}/ 3 | {{article.User.name}} 4 |

{{article.title}}

5 |
6 | Edit 7 | Delete 8 |
9 |
{{article.content}}
10 |
11 | -------------------------------------------------------------------------------- /public/views/header.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/views/index.html: -------------------------------------------------------------------------------- 1 |
2 |

This is the home view

3 |
-------------------------------------------------------------------------------- /public/views/users/auth.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
-------------------------------------------------------------------------------- /public/views/users/signin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 25 | -------------------------------------------------------------------------------- /public/views/users/signup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 32 |
-------------------------------------------------------------------------------- /test/karma/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Sat Oct 05 2013 22:00:14 GMT+0700 (ICT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '../../', 9 | 10 | 11 | // frameworks to use 12 | frameworks: ['jasmine'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | 'public/lib/angular/angular.js', 18 | 'public/lib/angular-mocks/angular-mocks.js', 19 | 'public/lib/angular-cookies/angular-cookies.js', 20 | 'public/lib/angular-resource/angular-resource.js', 21 | 'public/lib/angular-route/angular-route.js', 22 | 'public/lib/angular-bootstrap/ui-bootstrap-tpls.js', 23 | 'public/lib/angular-bootstrap/ui-bootstrap.js', 24 | 'public/lib/angular-ui-utils/modules/route/route.js', 25 | 'public/js/app.js', 26 | 'public/js/config.js', 27 | 'public/js/directives.js', 28 | 'public/js/filters.js', 29 | 'public/js/services/global.js', 30 | 'public/js/services/articles.js', 31 | 'public/js/controllers/articles.js', 32 | 'public/js/controllers/index.js', 33 | 'public/js/controllers/header.js', 34 | 'public/js/init.js', 35 | 'test/karma/unit/**/*.js' 36 | ], 37 | 38 | 39 | // list of files to exclude 40 | exclude: [ 41 | 42 | ], 43 | 44 | 45 | // test results reporter to use 46 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 47 | //reporters: ['progress'], 48 | reporters: ['progress', 'coverage'], 49 | 50 | // coverage 51 | preprocessors: { 52 | // source files, that you wanna generate coverage for 53 | // do not include tests or libraries 54 | // (these files will be instrumented by Istanbul) 55 | 'public/js/controllers/*.js': ['coverage'], 56 | 'public/js/services/*.js': ['coverage'] 57 | }, 58 | 59 | coverageReporter: { 60 | type: 'html', 61 | dir: 'test/coverage/' 62 | }, 63 | 64 | // web server port 65 | port: 9876, 66 | 67 | 68 | // enable / disable colors in the output (reporters and logs) 69 | colors: true, 70 | 71 | 72 | // level of logging 73 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 74 | logLevel: config.LOG_INFO, 75 | 76 | 77 | // enable / disable watching file and executing tests whenever any file changes 78 | autoWatch: true, 79 | 80 | 81 | // Start these browsers, currently available: 82 | // - Chrome 83 | // - ChromeCanary 84 | // - Firefox 85 | // - Opera 86 | // - Safari (only Mac) 87 | // - PhantomJS 88 | // - IE (only Windows) 89 | browsers: ['PhantomJS'], 90 | 91 | 92 | // If browser does not capture in given timeout [ms], kill it 93 | captureTimeout: 60000, 94 | 95 | 96 | // Continuous Integration mode 97 | // if true, it capture browsers, run tests and exit 98 | singleRun: true 99 | }); 100 | }; -------------------------------------------------------------------------------- /test/karma/unit/controllers/articles.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | // Articles Controller Spec 5 | describe('MEAN controllers', function() { 6 | 7 | describe('ArticlesController', function() { 8 | 9 | // The $resource service augments the response object with methods for updating and deleting the resource. 10 | // If we were to use the standard toEqual matcher, our tests would fail because the test values would not match 11 | // the responses exactly. To solve the problem, we use a newly-defined toEqualData Jasmine matcher. 12 | // When the toEqualData matcher compares two objects, it takes only object properties into 13 | // account and ignores methods. 14 | beforeEach(function() { 15 | this.addMatchers({ 16 | toEqualData: function(expected) { 17 | return angular.equals(this.actual, expected); 18 | } 19 | }); 20 | }); 21 | 22 | // Load the controllers module 23 | beforeEach(module('mean')); 24 | 25 | // Initialize the controller and a mock scope 26 | var ArticlesController, 27 | scope, 28 | $httpBackend, 29 | $routeParams, 30 | $location; 31 | 32 | // The injector ignores leading and trailing underscores here (i.e. _$httpBackend_). 33 | // This allows us to inject a service but then attach it to a variable 34 | // with the same name as the service. 35 | beforeEach(inject(function($controller, $rootScope, _$location_, _$routeParams_, _$httpBackend_) { 36 | 37 | scope = $rootScope.$new(); 38 | 39 | ArticlesController = $controller('ArticlesController', { 40 | $scope: scope 41 | }); 42 | 43 | $routeParams = _$routeParams_; 44 | 45 | $httpBackend = _$httpBackend_; 46 | 47 | $location = _$location_; 48 | 49 | })); 50 | 51 | it('$scope.find() should create an array with at least one article object ' + 52 | 'fetched from XHR', function() { 53 | 54 | // test expected GET request 55 | $httpBackend.expectGET('articles').respond([{ 56 | title: 'An Article about MEAN', 57 | content: 'MEAN rocks!' 58 | }]); 59 | 60 | // run controller 61 | scope.find(); 62 | $httpBackend.flush(); 63 | 64 | // test scope value 65 | expect(scope.articles).toEqualData([{ 66 | title: 'An Article about MEAN', 67 | content: 'MEAN rocks!' 68 | }]); 69 | 70 | }); 71 | 72 | it('$scope.findOne() should create an array with one article object fetched ' + 73 | 'from XHR using a articleId URL parameter', function() { 74 | // fixture URL parament 75 | $routeParams.articleId = '525a8422f6d0f87f0e407a33'; 76 | 77 | // fixture response object 78 | var testArticleData = function() { 79 | return { 80 | title: 'An Article about MEAN', 81 | content: 'MEAN rocks!' 82 | }; 83 | }; 84 | 85 | // test expected GET request with response object 86 | $httpBackend.expectGET(/articles\/([0-9a-fA-F]{24})$/).respond(testArticleData()); 87 | 88 | // run controller 89 | scope.findOne(); 90 | $httpBackend.flush(); 91 | 92 | // test scope value 93 | expect(scope.article).toEqualData(testArticleData()); 94 | 95 | }); 96 | 97 | it('$scope.create() with valid form data should send a POST request ' + 98 | 'with the form input values and then ' + 99 | 'locate to new object URL', function() { 100 | 101 | // fixture expected POST data 102 | var postArticleData = function() { 103 | return { 104 | title: 'An Article about MEAN', 105 | content: 'MEAN rocks!' 106 | }; 107 | }; 108 | 109 | // fixture expected response data 110 | var responseArticleData = function() { 111 | return { 112 | _id: '525cf20451979dea2c000001', 113 | title: 'An Article about MEAN', 114 | content: 'MEAN rocks!' 115 | }; 116 | }; 117 | 118 | // fixture mock form input values 119 | scope.title = 'An Article about MEAN'; 120 | scope.content = 'MEAN rocks!'; 121 | 122 | // test post request is sent 123 | $httpBackend.expectPOST('articles', postArticleData()).respond(responseArticleData()); 124 | 125 | // Run controller 126 | scope.create(); 127 | $httpBackend.flush(); 128 | 129 | // test form input(s) are reset 130 | expect(scope.title).toEqual(''); 131 | expect(scope.content).toEqual(''); 132 | 133 | // test URL location to new object 134 | expect($location.path()).toBe('/articles/' + responseArticleData()._id); 135 | }); 136 | 137 | it('$scope.update() should update a valid article', inject(function(Articles) { 138 | 139 | // fixture rideshare 140 | var putArticleData = function() { 141 | return { 142 | _id: '525a8422f6d0f87f0e407a33', 143 | title: 'An Article about MEAN', 144 | to: 'MEAN is great!' 145 | }; 146 | }; 147 | 148 | // mock article object from form 149 | var article = new Articles(putArticleData()); 150 | 151 | // mock article in scope 152 | scope.article = article; 153 | 154 | // test PUT happens correctly 155 | $httpBackend.expectPUT(/articles\/([0-9a-fA-F]{24})$/).respond(); 156 | 157 | // testing the body data is out for now until an idea for testing the dynamic updated array value is figured out 158 | //$httpBackend.expectPUT(/articles\/([0-9a-fA-F]{24})$/, putArticleData()).respond(); 159 | /* 160 | Error: Expected PUT /articles\/([0-9a-fA-F]{24})$/ with different data 161 | EXPECTED: {"_id":"525a8422f6d0f87f0e407a33","title":"An Article about MEAN","to":"MEAN is great!"} 162 | GOT: {"_id":"525a8422f6d0f87f0e407a33","title":"An Article about MEAN","to":"MEAN is great!","updated":[1383534772975]} 163 | */ 164 | 165 | // run controller 166 | scope.update(); 167 | $httpBackend.flush(); 168 | 169 | // test URL location to new object 170 | expect($location.path()).toBe('/articles/' + putArticleData()._id); 171 | 172 | })); 173 | 174 | it('$scope.remove() should send a DELETE request with a valid articleId' + 175 | 'and remove the article from the scope', inject(function(Articles) { 176 | 177 | // fixture rideshare 178 | var article = new Articles({ 179 | _id: '525a8422f6d0f87f0e407a33' 180 | }); 181 | 182 | // mock rideshares in scope 183 | scope.articles = []; 184 | scope.articles.push(article); 185 | 186 | // test expected rideshare DELETE request 187 | $httpBackend.expectDELETE(/articles\/([0-9a-fA-F]{24})$/).respond(204); 188 | 189 | // run controller 190 | scope.remove(article); 191 | $httpBackend.flush(); 192 | 193 | // test after successful delete URL location articles lis 194 | //expect($location.path()).toBe('/articles'); 195 | expect(scope.articles.length).toBe(0); 196 | 197 | })); 198 | 199 | }); 200 | 201 | }); 202 | }()); -------------------------------------------------------------------------------- /test/karma/unit/controllers/headers.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | describe('MEAN controllers', function() { 5 | 6 | describe('HeaderController', function() { 7 | 8 | // Load the controllers module 9 | beforeEach(module('mean')); 10 | 11 | var scope, 12 | HeaderController; 13 | 14 | beforeEach(inject(function($controller, $rootScope) { 15 | scope = $rootScope.$new(); 16 | 17 | HeaderController = $controller('HeaderController', { 18 | $scope: scope 19 | }); 20 | })); 21 | 22 | it('should expose some global scope', function() { 23 | 24 | expect(scope.global).toBeTruthy(); 25 | 26 | }); 27 | 28 | }); 29 | 30 | }); 31 | 32 | })(); -------------------------------------------------------------------------------- /test/karma/unit/controllers/index.spec.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | describe('MEAN controllers', function() { 5 | 6 | describe('IndexController', function() { 7 | 8 | // Load the controllers module 9 | beforeEach(module('mean')); 10 | 11 | var scope, 12 | IndexController; 13 | 14 | beforeEach(inject(function($controller, $rootScope) { 15 | scope = $rootScope.$new(); 16 | 17 | IndexController = $controller('IndexController', { 18 | $scope: scope 19 | }); 20 | })); 21 | 22 | it('should expose some global scope', function() { 23 | 24 | expect(scope.global).toBeTruthy(); 25 | 26 | }); 27 | 28 | }); 29 | 30 | }); 31 | 32 | })(); -------------------------------------------------------------------------------- /test/mocha/controllers/articleControllerSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Ahmed Hassan on 12/11/15. 3 | * Set of tests demonstrating how controllers can be tested by stubbing out database dependency 4 | * The tests are pretty basic, intended for demonstration of technique 5 | */ 6 | "use strict"; 7 | 8 | var proxyquire = require('proxyquire').noCallThru(), 9 | dbStub = {}, 10 | 11 | article = proxyquire('../../../app/controllers/articles', { 12 | '../../config/sequelize': dbStub 13 | }), 14 | chai = require('chai'), 15 | Promise = require('sequelize').Promise; 16 | 17 | chai.should(); 18 | var expect = chai.expect; 19 | 20 | var req = {}; 21 | var res = {}; 22 | 23 | describe('Articles Controller', function () { 24 | 25 | describe('Create Article', function () { 26 | 27 | beforeEach(function () { 28 | 29 | req.user = {id: 1}; 30 | req.body = { 31 | id: 1, 32 | title: 'test title', 33 | content: 'this is test content to test functionality of Create Article' 34 | }; 35 | 36 | }); 37 | 38 | it('should return article on successful creation', function (done) { 39 | 40 | dbStub.Article = { 41 | create: function (obj) { 42 | return Promise.resolve(obj); 43 | } 44 | }; 45 | 46 | res.jsonp = function (article) { 47 | 48 | 49 | article.should.have.property('id'); 50 | article.should.have.property('title'); 51 | article.should.have.property('content'); 52 | article.should.have.property('UserId'); 53 | done(); 54 | }; 55 | 56 | article.create(req, res); 57 | 58 | }); 59 | 60 | it('should return error if Article could not be created', function (done) { 61 | 62 | dbStub.Article = { 63 | create: function (obj) { 64 | return Promise.resolve(); 65 | } 66 | }; 67 | 68 | res.send = function (path, err) { 69 | 70 | path.should.equal('users/signup'); 71 | err.should.have.property('errors'); 72 | 73 | done(); 74 | }; 75 | 76 | article.create(req, res); 77 | 78 | }); 79 | 80 | it('should return error if error occurs while executing query', function (done) { 81 | 82 | dbStub.Article = { 83 | create: function (obj) { 84 | return Promise.reject('error while executing query'); 85 | } 86 | }; 87 | 88 | res.send = function (path, err) { 89 | 90 | path.should.equal('users/signup'); 91 | err.should.have.property('errors'); 92 | err.should.have.property('status'); 93 | err.status.should.equal(500); 94 | 95 | done(); 96 | }; 97 | 98 | article.create(req, res); 99 | 100 | }); 101 | 102 | 103 | }); 104 | 105 | describe('Update Article', function () { 106 | 107 | beforeEach(function () { 108 | req = { 109 | article: { 110 | id: 1, 111 | title: 'test title', 112 | content: 'this is test content to test functionality of Create Article', 113 | UserId: 1, 114 | updateAttributes: function () { 115 | 116 | var updatedArticle = { 117 | id: 1, 118 | title: 'test title updated', 119 | content: 'this is updated test content to test functionality of Create Article', 120 | UserId: 1 121 | }; 122 | 123 | return Promise.resolve(updatedArticle); 124 | } 125 | 126 | 127 | }, 128 | 129 | body: { 130 | 131 | title: 'test title updated', 132 | content: 'this is updated test content to test functionality of Create Article' 133 | } 134 | 135 | } 136 | 137 | 138 | }); 139 | 140 | it('should return updated article on success', function (done) { 141 | 142 | 143 | res.jsonp = function (updatedArticle) { 144 | updatedArticle.should.have.property('id'); 145 | updatedArticle.should.have.property('title'); 146 | updatedArticle.title.should.equal('test title updated'); 147 | updatedArticle.should.have.property('content'); 148 | updatedArticle.content.should.equal('this is updated test content to test functionality of Create Article'); 149 | updatedArticle.should.have.property('UserId'); 150 | done(); 151 | }; 152 | 153 | article.update(req, res); 154 | 155 | }); 156 | 157 | it('should return error if error occurs while executing query', function (done) { 158 | req.article.updateAttributes = function () { 159 | // note: the rejection value here is symbolic, 160 | // the key thing is that the same error should be propagated ahead 161 | return Promise.reject('error occurred while executing query'); 162 | }; 163 | 164 | 165 | res.render = function (err, obj) { 166 | 167 | 168 | err.should.equal('error'); 169 | obj.should.have.property('error'); 170 | obj.should.have.property('status'); 171 | obj.error.should.equal('error occurred while executing query'); 172 | obj.status.should.equal(500); 173 | 174 | done(); 175 | }; 176 | 177 | article.update(req, res); 178 | 179 | }); 180 | 181 | }); 182 | 183 | 184 | describe('Destroy Article', function () { 185 | 186 | beforeEach(function () { 187 | req = { 188 | article: { 189 | id: 1, 190 | title: 'test title', 191 | content: 'this is test content to test functionality of destroy Article', 192 | UserId: 1, 193 | destroy: function () { 194 | return Promise.resolve(); 195 | } 196 | } 197 | } 198 | }); 199 | 200 | it('should return destroyed article on successful destroy', function (done) { 201 | 202 | res.jsonp = function (article) { 203 | 204 | article.should.have.property('id'); 205 | article.should.have.property('title'); 206 | article.title.should.equal('test title'); 207 | article.should.have.property('content'); 208 | article.content.should.equal('this is test content to test functionality of destroy Article'); 209 | article.should.have.property('UserId'); 210 | done(); 211 | }; 212 | 213 | article.destroy(req, res); 214 | 215 | }); 216 | 217 | it('should return error if error occurs while executing query', function (done) { 218 | req.article.destroy = function () { 219 | return Promise.reject('error occurred while executing query'); 220 | }; 221 | 222 | res.render = function (err, obj) { 223 | err.should.equal('error'); 224 | obj.should.have.property('error'); 225 | obj.should.have.property('status'); 226 | obj.error.should.equal('error occurred while executing query'); 227 | obj.status.should.equal(500); 228 | 229 | done(); 230 | }; 231 | 232 | article.destroy(req, res); 233 | 234 | }); 235 | 236 | }); 237 | 238 | 239 | describe('Fetch all articles', function () { 240 | 241 | 242 | it('should return the received list of articles on successful query', function (done) { 243 | 244 | dbStub.Article = { 245 | findAll: function (obj) { 246 | var Articles = [{ 247 | id: 1, 248 | title: 'test', 249 | content: 'this is test content', 250 | UserId: 1 251 | },{ 252 | id: 2, 253 | title: 'test', 254 | content: 'this is test content', 255 | UserId: 1 256 | }]; 257 | 258 | return Promise.resolve(Articles); 259 | } 260 | }; 261 | dbStub.User = {id: 1}; 262 | 263 | res.jsonp = function (articles) { 264 | articles[0].should.have.property('id'); 265 | articles[0].should.have.property('title'); 266 | articles[0].title.should.equal('test'); 267 | articles[0].should.have.property('content'); 268 | articles[0].content.should.equal('this is test content'); 269 | articles[0].should.have.property('UserId'); 270 | done(); 271 | }; 272 | 273 | article.all(req, res); 274 | 275 | }); 276 | it('should return error if error occurs while executing query', function (done) { 277 | 278 | dbStub.Article = { 279 | findAll: function (obj) { 280 | return Promise.reject('error occurred while executing query'); 281 | } 282 | }; 283 | dbStub.User = {id: 1}; 284 | 285 | res.render = function (err, obj) { 286 | err.should.equal('error'); 287 | obj.should.have.property('error'); 288 | obj.should.have.property('status'); 289 | obj.error.should.equal('error occurred while executing query'); 290 | obj.status.should.equal(500); 291 | 292 | done(); 293 | }; 294 | 295 | article.all(req, res); 296 | 297 | }); 298 | 299 | }); 300 | 301 | describe('Has Authorization', function () { 302 | 303 | 304 | it('should give error if user is not authorized', function (done) { 305 | 306 | req = { 307 | article: { 308 | User: { 309 | id: 1 310 | } 311 | }, 312 | user: { 313 | id: 2 314 | } 315 | 316 | }, 317 | res.send = function (httpStatus, msg) { 318 | 319 | 320 | httpStatus.should.equal(401); 321 | msg.should.equal('User is not authorized'); 322 | 323 | done(); 324 | }; 325 | 326 | article.hasAuthorization(req, res); 327 | 328 | }); 329 | 330 | }); 331 | 332 | 333 | }); 334 | -------------------------------------------------------------------------------- /test/mocha/controllers/usersControllerSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Users controller tests */ -------------------------------------------------------------------------------- /test/mocha/models/userModelSpec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var chai = require('chai'), 7 | expect = chai.expect, 8 | _ = require('lodash'), 9 | winston = require('../../../config/winston'), 10 | User = require('../../../app/models/user'); 11 | 12 | chai.should(); 13 | 14 | //The tests 15 | describe('User model', function() { 16 | 17 | var userModel; 18 | before(function() { 19 | 20 | var sequelizeStub = { 21 | define: function(modelName, fields, properties){ 22 | return { 23 | name : modelName, 24 | fields: fields, 25 | properties: properties 26 | }; 27 | } 28 | }; 29 | 30 | var datatypesStub = { 31 | STRING: 'string', 32 | INTEGER: 'integer' 33 | }; 34 | 35 | userModel = User(sequelizeStub, datatypesStub); 36 | 37 | }); 38 | 39 | describe('name', function(){ 40 | it('should be equal to: User', function(){ 41 | userModel.name.should.equal('User'); 42 | }); 43 | }); 44 | 45 | describe('data', function() { 46 | it('should have username', function() { 47 | userModel.fields.username.should.exist.and.equal('string'); 48 | }); 49 | it('should have email', function() { 50 | userModel.fields.email.should.exist.and.equal('string'); 51 | }); 52 | it('should have hashedPassword', function() { 53 | userModel.fields.hashedPassword.should.exist.and.equal('string'); 54 | }); 55 | }); 56 | 57 | describe('properties', function(){ 58 | describe('instance methods', function(){ 59 | describe('makeSalt', function(){ 60 | it('should generate salt of length 16', function(){ 61 | var salt = userModel.properties.instanceMethods.makeSalt(); 62 | salt.should.be.a('string').with.length(24); 63 | }); 64 | }); 65 | describe('encryptPassword', function(){ 66 | it('should return empty if password is undefined', function(){ 67 | var encryptedPassword = userModel.properties.instanceMethods.encryptPassword(undefined,'salt'); 68 | encryptedPassword.should.be.a('string').with.length(0); 69 | }); 70 | it('should return empty if salt is undefined', function(){ 71 | var encryptedPassword = userModel.properties.instanceMethods.encryptPassword('password'); 72 | encryptedPassword.should.be.a('string').with.length(0); 73 | }); 74 | it('should return encrypted password if both password and salt are supplied', function(){ 75 | var encryptedPassword = userModel.properties.instanceMethods.encryptPassword('password','salt'); 76 | encryptedPassword.should.be.a('string').with.length(88); 77 | }); 78 | }); 79 | describe('authenticate', function(){ 80 | it('should return true if password is correct', function(){ 81 | var authResult = userModel.properties.instanceMethods.authenticate.call({ 82 | salt: 'salt', 83 | hashedPassword: userModel.properties.instanceMethods.encryptPassword('password','salt'), 84 | encryptPassword: userModel.properties.instanceMethods.encryptPassword 85 | }, 'password'); 86 | authResult.should.equal(true); 87 | }); 88 | it('should return false if password is incorrect', function(){ 89 | var authResult = userModel.properties.instanceMethods.authenticate.call({ 90 | salt: 'salt', 91 | hashedPassword: userModel.properties.instanceMethods.encryptPassword('password','salt'), 92 | encryptPassword: userModel.properties.instanceMethods.encryptPassword 93 | }, 'NOTpassword'); 94 | authResult.should.equal(false); 95 | }); 96 | }); 97 | }); 98 | }); 99 | 100 | after(function(done) { 101 | done(); 102 | }); 103 | 104 | }); 105 | --------------------------------------------------------------------------------