├── .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 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------