├── .bowerrc ├── .gitignore ├── run.js ├── angular ├── routes │ ├── welcome │ │ ├── welcome.html │ │ ├── WelcomeController.js │ │ └── welcome.sass │ └── login │ │ ├── login.html │ │ ├── login.sass │ │ └── LoginController.js ├── app.sass ├── services │ └── User.js └── app.js ├── app ├── views │ ├── error.jade │ └── index.jade ├── routes │ ├── api.js │ └── passport.js ├── config.js └── models │ └── User.js ├── README.md ├── bower.json ├── package.json ├── config.coffee └── server.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /bower_components 3 | /public 4 | /.idea -------------------------------------------------------------------------------- /run.js: -------------------------------------------------------------------------------- 1 | module.exports = (function() { 2 | require('./server.js').startServer(8080, null, function() {}); 3 | })(); -------------------------------------------------------------------------------- /angular/routes/welcome/welcome.html: -------------------------------------------------------------------------------- 1 |

2 | {{ welcomeText }}! 3 |
4 | Log out? 5 |

-------------------------------------------------------------------------------- /app/views/error.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en", ng-app="anOutlandishWebApp") 3 | head 4 | title Page Not Found 5 | body 6 | p 404 - Page Not Found 7 | p Uh oh! Looks like you found a page that doesn't exist? -------------------------------------------------------------------------------- /angular/app.sass: -------------------------------------------------------------------------------- 1 | * 2 | box-sizing: border-box 3 | 4 | html, 5 | body 6 | width: 100% 7 | height: 100% 8 | padding: 0 9 | border: 0 10 | margin: 0 11 | background-color: white 12 | color: black 13 | 14 | .wrapper 15 | width: 100% 16 | height: 100% -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## An Outlandish MEAN Web Application using Brunch, Sass, and Passport 2 | 3 | Please visit [our website](http://outlandish.com/blog/creating-a-node-js-mean-web-application-with-brunch-sass-and-passport "Creating a Node JS MEAN Web Application with Brunch, Sass, and Passport") to see more details. -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "an-outlandish-mean-app", 3 | "version": "1.0.0", 4 | "description": "A super-simple, super-easy MEAN example app.", 5 | "main": "run.js", 6 | "license": "MIT", 7 | "private": true, 8 | "dependencies": { 9 | "angular": "~1.3.8", 10 | "angular-route": "~1.3.6" 11 | } 12 | } -------------------------------------------------------------------------------- /angular/routes/welcome/WelcomeController.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('anOutlandishWebApp') 3 | .controller('WelcomeController', function($scope, $rootScope) { 4 | "use strict"; 5 | 6 | $rootScope.className = 'welcome'; 7 | $rootScope.title = 'Welcome!'; 8 | 9 | $scope.welcomeText = 'Welcome, ' + $rootScope.user.email; 10 | }); -------------------------------------------------------------------------------- /angular/routes/login/login.html: -------------------------------------------------------------------------------- 1 |
2 |

Hey there!

3 |

Please login or sign up:

4 |
5 | 6 | 7 | 8 | 9 |
-------------------------------------------------------------------------------- /app/views/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en", ng-app="anOutlandishWebApp") 3 | head 4 | title {{ title }} 5 | base(href="/") 6 | link(rel="stylesheet", href="/css/app.css") 7 | body 8 | div(ng-view, class="wrapper {{ className }}") 9 | script(src="/js/vendor.js", type="text/javascript") 10 | script(src="/js/templates.js", type="text/javascript") 11 | script(src="/js/app.js", type="text/javascript") -------------------------------------------------------------------------------- /angular/routes/welcome/welcome.sass: -------------------------------------------------------------------------------- 1 | .wrapper.welcome 2 | background-color: black 3 | 4 | h1 5 | font-size: 50px 6 | position: absolute 7 | top: 50% 8 | left: 50% 9 | transform: translate(-50%, -50%) 10 | color: white 11 | text-align: center 12 | 13 | a 14 | font-size: 25px 15 | color: white 16 | text-decoration: none 17 | border-bottom: 1px dotted white 18 | 19 | &:hover 20 | border-bottom: 1px solid white -------------------------------------------------------------------------------- /app/routes/api.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | var mongoose = require('mongoose'), 3 | User = mongoose.model('User'); 4 | 5 | app.get('/api/user', function(req, res) { 6 | // Check if user is authenticated. If not, disallow access to the API: 7 | if(!req.isAuthenticated()) { 8 | res.status(401).send(); 9 | return; 10 | } 11 | 12 | // User is authenticated so send them their `req.user` session object: 13 | res.send(req.user); 14 | }); 15 | }; -------------------------------------------------------------------------------- /app/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dev: { 3 | sessionSecret: 'developmentSessionSecret', 4 | db: { 5 | name: 'anOutlandishDatabase', 6 | host: 'localhost', 7 | username: '', 8 | password: '' 9 | } 10 | }, 11 | 12 | prod: { 13 | sessionSecret: 'productionSessionSecret', 14 | db: { 15 | name: 'anOutlandishDatabase', 16 | host: 'productionHost', 17 | username: 'productionDatabaseUsername', 18 | password: 'productionDatabasePassword' 19 | } 20 | } 21 | }; -------------------------------------------------------------------------------- /app/models/User.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require('bcrypt-nodejs'), 2 | mongoose = require('mongoose'); 3 | 4 | var Schema = mongoose.Schema; 5 | 6 | var UserSchema = new Schema({ 7 | email: { type: String, index: true, required: true }, 8 | password: { type: String, required: true } 9 | }); 10 | 11 | UserSchema.methods.generateHash = function(password) { 12 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 13 | }; 14 | 15 | UserSchema.methods.validPassword = function(password) { 16 | return bcrypt.compareSync(password, this.password); 17 | }; 18 | 19 | var User = mongoose.model('User', UserSchema); 20 | 21 | module.exports = User; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "an-outlandish-mean-app", 3 | "version": "1.0.0", 4 | "description": "A super-simple, super-easy MEAN example app.", 5 | "private": true, 6 | "main": "run.js", 7 | "dependencies": { 8 | "angularjs-templates-brunch": "0.0.3", 9 | "assetsmanager-brunch": "^1.8.1", 10 | "auto-reload-brunch": ">=1.7.5", 11 | "autoprefixer-brunch": "^1.8.3", 12 | "autoprefixer-core": "^3.1.0", 13 | "bcrypt-nodejs": "0.0.3", 14 | "body-parser": "^1.8.1", 15 | "connect-mongo": "^0.4.1", 16 | "cookie-parser": "^1.3.2", 17 | "express": "^4.10.4", 18 | "express-session": "^1.9.2", 19 | "jade": "^1.7.0", 20 | "javascript-brunch": ">=1.7.1", 21 | "jshint-brunch": "^1.7.0", 22 | "mongoose": "^6.5.2", 23 | "sass-brunch": "^1.8.8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /angular/routes/login/login.sass: -------------------------------------------------------------------------------- 1 | .wrapper.login 2 | background-color: grey 3 | 4 | h1, 5 | p 6 | margin: 0 7 | margin-bottom: 10px 8 | color: black 9 | text-align: center 10 | 11 | h1 12 | text-shadow: 0 2px 0 white 13 | 14 | form 15 | width: 400px 16 | position: absolute 17 | top: 50% 18 | left: 50% 19 | transform: translate(-50%, -50%) 20 | padding: 10px 21 | border: 1px solid black 22 | border-radius: 15px 23 | background-color: lightgrey 24 | 25 | input 26 | width: 100% 27 | float: left 28 | clear: left 29 | padding: 10px 30 | margin-bottom: 10px 31 | font-size: 25px 32 | 33 | &:last-child 34 | margin: 0 35 | 36 | &[type="text"], 37 | &[type="password"] 38 | border: 0 39 | 40 | &[type="submit"] 41 | cursor: pointer -------------------------------------------------------------------------------- /config.coffee: -------------------------------------------------------------------------------- 1 | exports.config = 2 | modules: 3 | definition: false 4 | wrapper: false 5 | paths: 6 | public: 'public' 7 | watched: ['angular'] 8 | notifications: true 9 | files: 10 | stylesheets: 11 | joinTo: 12 | 'css/app.css': /^(angular|vendor|bower_components)/ 13 | order: 14 | before: ['angular/app.sass'] 15 | templates: 16 | joinTo: 17 | 'js/templates.js': /^angular/ 18 | javascripts: 19 | joinTo: 20 | 'js/app.js': /^angular/ 21 | 'js/vendor.js': /^bower_components/ 22 | order: 23 | before: ['angular/app.js'] 24 | server: 25 | path: 'server.js' 26 | port: 8080 27 | watcher: 28 | usePolling: true 29 | plugins: 30 | sass: 31 | mode: 'ruby' 32 | autoprefixer: 33 | browsers: ["last 1 version", "> 1%", "ie 8", "ie 7"] 34 | cascade: false 35 | minify: true -------------------------------------------------------------------------------- /angular/routes/login/LoginController.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('anOutlandishWebApp') 3 | .controller('LoginController', function($scope, $rootScope, $location, User) { 4 | "use strict"; 5 | 6 | $rootScope.className = 'login'; 7 | $rootScope.title = 'Login'; 8 | 9 | $scope.signUp = function() { 10 | User.create($scope.email, $scope.password, function(err) { 11 | if(err) { 12 | alert("Could not sign up. Please try again later."); 13 | return; 14 | } 15 | 16 | // User created successfully. Now sign them in automatically: 17 | $scope.logIn(); 18 | }); 19 | }; 20 | 21 | $scope.logIn = function() { 22 | User.authenticate($scope.email, $scope.password, function(err) { 23 | if(err) { 24 | alert("Could not log you in. Did you enter the correct credentials?"); 25 | return; 26 | } 27 | 28 | // Redirect the user on successful authentication: 29 | $location.path('/welcome'); 30 | }); 31 | }; 32 | }); -------------------------------------------------------------------------------- /angular/services/User.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('anOutlandishWebApp') 3 | .factory('User', function($rootScope, $http) { 4 | "use strict"; 5 | 6 | var methods = {}, 7 | _user = null; 8 | 9 | methods.authenticate = function(email, password, done) { 10 | $http.post('/auth/local/signin', { 11 | email: email, 12 | password: password 13 | }).then(function(res) { 14 | methods.load(done); 15 | }, function(err) { 16 | done(err); 17 | }) 18 | }; 19 | 20 | methods.create = function(email, password, done) { 21 | $http.post('/auth/local/signup', { 22 | email: email, 23 | password: password 24 | }).then(function(res) { 25 | done(); 26 | }, function(err) { 27 | done(err); 28 | }); 29 | }; 30 | 31 | methods.load = function(done) { 32 | $http.get('/api/user').then(function(res) { 33 | _user = res.data; 34 | $rootScope.user = res.data; 35 | done(null, _user); 36 | }, function(err) { 37 | done(err); 38 | }); 39 | }; 40 | 41 | methods.get = function() { 42 | return _user; 43 | }; 44 | 45 | return methods; 46 | }); -------------------------------------------------------------------------------- /angular/app.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('anOutlandishWebApp', ['ngRoute', 'templates']) 3 | .config(function($routeProvider, $locationProvider) { 4 | var baseUrl = 'angular/routes'; 5 | 6 | var resolvers = { 7 | fetchUser: function($q, $location, User) { 8 | var defer = $q.defer(); 9 | 10 | User.load(function(err) { 11 | if(err) { 12 | $location.path('/login'); 13 | return; 14 | } 15 | 16 | defer.resolve(); 17 | }); 18 | 19 | return defer.promise; 20 | }, 21 | redirectWithUser: function($q, $location, User) { 22 | var defer = $q.defer(); 23 | 24 | User.load(function(err, user) { 25 | if(err || !user) { 26 | defer.resolve(); 27 | return; 28 | } 29 | 30 | $location.path('/welcome'); 31 | }); 32 | 33 | return defer.promise; 34 | } 35 | }; 36 | 37 | $routeProvider 38 | .when('/login', { 39 | templateUrl: baseUrl + '/login/login.html', 40 | controller: 'LoginController', 41 | resolve: { redirectWithUser: resolvers.redirectWithUser } 42 | }) 43 | .when('/welcome', { 44 | templateUrl: baseUrl + '/welcome/welcome.html', 45 | controller: 'WelcomeController', 46 | authenticate: true, 47 | resolve: { fetchUser: resolvers.fetchUser } 48 | }) 49 | .otherwise({ 50 | redirectTo: '/login' 51 | }); 52 | 53 | $locationProvider.html5Mode(true); 54 | }); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | module.exports.startServer = function(PORT, PATH, CALLBACK) { 2 | var ENV = process.env.NODE_ENV || 'prod'; // Assume production env. for safety 3 | 4 | var config = require('./app/config.js')[ENV]; 5 | 6 | var express = require('express'), 7 | session = require('express-session'), 8 | bodyParser = require('body-parser'), 9 | passport = require('passport'), 10 | mongoose = require('mongoose'), 11 | path = require('path'), 12 | fs = require('fs'), 13 | MongoStore = require('connect-mongo')(session), 14 | app = express(), 15 | server = app.listen(PORT, CALLBACK), 16 | cookieParser = require('cookie-parser')(config.sessionSecret), 17 | sessionStore = new MongoStore({db: config.db.name, auto_reconnect: true}); 18 | 19 | /******************************************************************************************************************* 20 | * MONGOOSE CONFIG * 21 | *******************/ 22 | 23 | var connect = function() { 24 | var db = config.db; 25 | var conn = 'mongodb://' + db.username + ':' + db.password + '@' + db.host; 26 | (ENV === 'dev') ? mongoose.connect(db.host, db.name) : mongoose.connect(conn); 27 | }; 28 | connect(); 29 | 30 | mongoose.connection.on('error', function (err) { 31 | console.log(err); 32 | }); 33 | 34 | if(ENV === 'prod') { 35 | mongoose.connection.on('disconnected', function () { 36 | connect(); 37 | }); 38 | } 39 | 40 | // 'require' all of our Mongo models: 41 | var models = __dirname + '/app/models'; 42 | fs.readdirSync(models).forEach(function (model) { 43 | if (model.indexOf('.js') > -1) { 44 | require(models + '/' + model); 45 | } 46 | }); 47 | 48 | /******************************************************************************************************************* 49 | * EXPRESS CONFIG * 50 | ******************/ 51 | 52 | var sessionOpts = { 53 | secret: config.sessionSecret, 54 | resave: true, 55 | saveUninitialized: true, 56 | store: sessionStore 57 | }; 58 | 59 | // Define where our static files will be fetched from: 60 | app.use(express.static(path.join(__dirname, 'public'))); 61 | 62 | app.use(bodyParser.json()); 63 | app.use(bodyParser.urlencoded({ extended: true })); 64 | app.use(cookieParser); 65 | app.use(session(sessionOpts)); 66 | 67 | // Push Passport middleware into flow: 68 | app.use(passport.initialize()); 69 | app.use(passport.session()); 70 | 71 | // Tell Express where our server views (Jade files) are kept. 72 | // Then we can do render('NAME_OF_VIEW') inside an Express route request. e.g. render('index') 73 | app.set('views', path.join(__dirname, 'app/views')); 74 | app.set('view engine', 'jade'); 75 | 76 | /******************************************************************************************************************* 77 | * ROUTE CONFIG * 78 | ****************/ 79 | 80 | require('./app/routes/passport')(app, config); 81 | require('./app/routes/api')(app); 82 | 83 | app.get('/log-out', function(req, res) { 84 | req.logout(); // Log the user out 85 | res.redirect('/login'); // Send them back to the login page 86 | }); 87 | 88 | app.get('/*', function (req, res) { 89 | // Render index and pass route handling to Angular 90 | res.render('index'); 91 | }); 92 | 93 | app.all('/*', function(req, res) { 94 | // Client is lost... render error page! 95 | res.render('error'); 96 | }); 97 | 98 | console.log('Application running in ' + ENV + ' environment...'); 99 | }; -------------------------------------------------------------------------------- /app/routes/passport.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app, config) { 2 | var mongoose = require('mongoose'), 3 | passport = require('passport'); 4 | 5 | var LocalStrategy = require('passport-local').Strategy; 6 | 7 | var User = mongoose.model('User'); 8 | 9 | passport.serializeUser(function(user, done) { 10 | done(null, user); 11 | }); 12 | 13 | passport.deserializeUser(function(obj, done) { 14 | done(null, obj); 15 | }); 16 | 17 | /******************************************************************************************************************* 18 | * LOCAL AUTH * 19 | **************/ 20 | 21 | passport.use('local_signup', new LocalStrategy({ 22 | // Define what form 'name' fields to use as 'username' and 'password' equivalents: 23 | usernameField: 'email', 24 | passwordField: 'password' 25 | }, 26 | function(email, password, done) { 27 | process.nextTick(function() { 28 | // Attempt to find a user with this email: 29 | User.findOne({ 30 | email: email 31 | }, function(err, user) { 32 | if(err) { 33 | done(err); 34 | return; 35 | } 36 | 37 | if(user) { 38 | done(null, false, { message: 'Email already in use.' }); 39 | return; 40 | } 41 | 42 | // User does not exist with this email, so create it: 43 | var newUser = new User(); 44 | 45 | newUser.email = email; 46 | newUser.password = newUser.generateHash(password); // Call instance method 'generateHash' to produce password hash 47 | 48 | // Save this user to our database and return it to Passport by passing it as the second 49 | // parameter of the 'done' callback: 50 | newUser.save(function(err) { 51 | if(err) { 52 | done(err); 53 | return; 54 | } 55 | 56 | done(null, newUser); 57 | }); 58 | }) 59 | }); 60 | })); 61 | 62 | passport.use('local_signin', new LocalStrategy({ 63 | usernameField: 'email', 64 | passwordField: 'password' 65 | }, 66 | function(email, password, done) { 67 | User.findOne({ 68 | email: email 69 | }, function(err, user) { 70 | if(err) { 71 | done(err); 72 | return; 73 | } 74 | 75 | if(!user) { 76 | done(null, false, {message: 'User not found.'}); 77 | return; 78 | } 79 | 80 | if(!user.validPassword(password)) { 81 | done(null, false, {message: 'Invalid password.'}); 82 | return; 83 | } 84 | 85 | done(null, user); 86 | }); 87 | })); 88 | 89 | app.post('/auth/local/signup', function(req, res) { 90 | function handleError(status) { 91 | res.status(status).send(); 92 | } 93 | 94 | // Call on Passport to authenticate using our 'local_signup' strategy. The second parameter will 95 | // be called on completion of the user creation, etc. 96 | passport.authenticate('local_signup', function(err, user) { 97 | if(err) { 98 | handleError(500); 99 | return; 100 | } 101 | 102 | if(!user) { 103 | handleError(401); 104 | return; 105 | } 106 | 107 | // Authentication was successful, 'login' this new user by creating a session with `req.login`: 108 | req.login(user, function(err) { 109 | if(err) { 110 | handleError(500); 111 | return; 112 | } 113 | 114 | res.send({ success: true }); 115 | }); 116 | })(req, res); 117 | }); 118 | 119 | app.post('/auth/local/signin', function(req, res) { 120 | function handleError(status) { 121 | res.status(status).send(); 122 | } 123 | 124 | passport.authenticate('local_signin', function(err, user) { 125 | if(err) { 126 | handleError(500); 127 | return; 128 | } 129 | 130 | if(!user) { 131 | handleError(401); 132 | return; 133 | } 134 | 135 | req.login(user, function(err) { 136 | if(err) { 137 | handleError(500); 138 | return; 139 | } 140 | 141 | res.send({ success: true }); 142 | }); 143 | })(req, res); 144 | }); 145 | 146 | /******************************************************************************************************************* 147 | * UTILITY * 148 | ***********/ 149 | 150 | /** 151 | * Find a user in the database. If it does not exist, create a document for that user. 152 | * @param profile social network profile 153 | * @param done 154 | */ 155 | function findOrCreate(profile, done) { 156 | if(!profile.email) { 157 | done('No email provided.'); 158 | return; 159 | } 160 | 161 | User.findOne({ 162 | email: profile.email 163 | }, function(err, res) { 164 | if(err) { 165 | done(err); 166 | return; 167 | } 168 | 169 | if(res) { 170 | // Return found user: 171 | done(null, res); 172 | } else { 173 | // Create new user: 174 | var user = new User({ 175 | email: profile.email, 176 | socialNetworkProfile: profile 177 | }); 178 | 179 | user.save(function(err, user) { 180 | if(err) { 181 | done(err); 182 | return; 183 | } 184 | 185 | done(null, user); 186 | }); 187 | } 188 | }); 189 | } 190 | }; --------------------------------------------------------------------------------