4 HackHall

Summary

The HachHall app is a REST API server with a front-end client that is written in Backbone.js and Underscore. For the purpose of this book, we’ll illustrate how to use Express.js with MongoDB via Mongoose for the back-end REST API server. In addition, the project utilizes OAuth and sessions, and Mocha for TDD.

Example

The HackHall source code is in the public GitHub repository.

The live demo is accessible at hackhall.com, either with AngelList or pre-filled email (1@1.com) and password (1).

4.1 What is HackHall

HackHall (ex-Accelerator.IO) is an open-source invite-only social network and collaboration tool for hackers, hipsters, entrepreneurs and pirates (just kidding). HackHall is akin to Reddit, plus Hacker News, plus Facebook Groups with curation.

The HackHall project is in its early stage and roughly a beta. We plan to extend the code base in the future and bring in more people to share skills, wisdom and passion for programming.

In this chapter, we’ll cover the 1.0 release which has:

  • OAuth 1.0 with oauth modules and AngelList API
  • Email and password authentication
  • Mongoose models and schemas
  • Express.js structure with routes in modules
  • JSON REST API
  • Express.js error handling
  • Front-end client Backbone.js app (for more info on Backbone.js, download/read online our Rapid Prototyping with JS tutorials)
  • Environmental variables with Foreman’s .env
  • TDD with Mocha
  • Basic Makefile setup

4.2 Running HackHall

To get the source code, you can navigate to the hackhall folder or clone it from GitHub:

1 $ git clone git@github.com:azat-co/hackhall
2 $ git checkout 1.0
3 $ npm install

If you plan to test an AngelList (optional), HackHall is using a Heroku and Foreman setup for AngelList API keys, storing them in environmental variables, so we need to add an .evn file like this (below are fake values):

1 ANGELLIST_CLIENT_ID=254C0335-5F9A-4607-87C0
2 ANGELLIST_CLIENT_SECRET=99F5C1AC-C5F7-44E6-81A1-8DF4FC42B8D9

The keys can be found at angel.co/api after someone creates and registers his/her AngelList app.

Download and install MongoDB if you don’t have it already. The databases and third-party libraries are outside of the scope of this book. However, you can find enough materials online and in Rapid Prototyping with JS.

To start the MongoDB server, open a new terminal window and run:

1 $ mongod

Go back to the project folder and run:

1 $ foreman start

After MongoDB is running on localhost with default port 27017, it’s possible to seed the database hackhall with a default admin user by running seed.js MongoDB script:

1 $ mongo localhost:27017/hackhall seed.js

Feel free to modify seed.js to your liking (beware that it erases all previous data!):

 1 db.dropDatabase();
 2 var seedUser ={
 3   firstName:'Azat',
 4   lastName:"Mardanov", 
 5   displayName:"Azat Mardanov",
 6   password:'1',
 7   email:'1@1.com',
 8   role:'admin',
 9   approved: true, 
10   admin: true
11 };
12 db.users.save(seedUser);

If you open your browser at http://localhost:5000, you should see the login screen.

The HackHall login page running locally.

The HackHall login page running locally.

Enter username and password to get in (the ones from the seed.js file).

The HackHall login page with seed data.

The HackHall login page with seed data.

After successful authentication, users are redirected to the Posts page:

The HackHall Posts page.

The HackHall Posts page.

where they can create a post (e.g., a question):

The HackHall Posts page.

The HackHall Posts page.

Save the post:

The HackHall Posts page with a saved post.

The HackHall Posts page with a saved post.

Like posts:

The HackHall Posts page with a liked post.

The HackHall Posts page with a liked post.

Visit other users’ profiles:

The HackHall People page.

The HackHall People page.

If they have admin rights, users can approve applicants:

The HackHall People page with admin rights.

The HackHall People page with admin rights.

and manage their account on the Profile page:

The HackHall Profile page.

The HackHall Profile page.

4.3 Structure

Here are what each of the folders and files contain:

  • /api: app-shared routes
  • /models: Mongoose models
  • /public: Backbone app, static files like front-end JavaScript, CSS, HTML
  • /routes: REST API routes
  • /tests: Mocha tests
  • .gitignore: list of files that git should ignore
  • Makefile: make file to run tests
  • Procfile: Cedar stack file needed for Heroku deployment
  • package.json: NPM dependencies and HackHall metadata
  • readme.md: description
  • server.js: main HackHall server file
Content of the HackHall base folder.

Content of the HackHall base folder.

4.4 Express.js App

Let’s jump straight to the server.js file and learn how it’s implemented. Firstly, we declare dependencies:

1 var express = require('express'),
2   routes = require('./routes'),
3   http = require('http'),
4   util = require('util'),
5   oauth = require('oauth'),
6   querystring = require('querystring');

Then, we initialize the app and configure middlewares. The process.env.PORT is populated by Heroku, and in the case of a local setup, falls back on 3000.

1 var app = express();
2 app.configure(function(){
3   app.set('port', process.env.PORT || 3000  );
4   app.use(express.favicon());
5   app.use(express.logger('dev'));
6   app.use(express.bodyParser());
7   app.use(express.methodOverride());

The values passed to cookieParser and session middlewares are needed for authentication. Obviously, session secrets are supposed to be private:

1   app.use(express.cookieParser('asd;lfkajs;ldfkj'));
2   app.use(express.session({
3     secret: '<h1>WHEEYEEE</h1>',
4     key: 'sid',
5     cookie: {
6       secret: true,
7       expires: false
8     }
9   }));

This is how we serve our front-end client Backbone.js app and other static files like CSS:

1   app.use(express.static(__dirname + '/public'));
2   app.use(app.router);
3 });

Error handling is broken down into three functions with clientErrorHandler dedicated to AJAX/XHR requests from the Backbone.js app (responds with JSON):

 1 app.configure(function() {
 2   app.use(logErrors);
 3   app.use(clientErrorHandler);
 4   app.use(errorHandler);
 5 });
 6 
 7 function logErrors(err, req, res, next) {
 8   console.error('logErrors', err.toString());
 9   next(err);
10 }
11 
12 function clientErrorHandler(err, req, res, next) {
13   console.error('clientErrors ', err.toString());
14   res.send(500, { error: err.toString()});
15   if (req.xhr) {
16     console.error(err);
17     res.send(500, { error: err.toString()});
18   } else {
19     next(err);
20   }
21 }
22 
23 function errorHandler(err, req, res, next) {
24   console.error('lastErrors ', err.toString());
25   res.send(500, {error: err.toString()});
26 }

In the same way that we determine process.env.PORT and fallback on local setup value 3000, we do a similar thing with a MongoDB connection string:

1 var dbUrl = process.env.MONGOHQ_URL
2   || 'mongodb://@127.0.0.1:27017/hackhall';
3 var mongoose = require('mongoose');
4 var connection = mongoose.createConnection(dbUrl);
5 connection.on('error', console.error.bind(console,
6   'connection error:'));

Sometimes it’s a good idea to log the connection open event:

1 connection.once('open', function () {
2   console.info('connected to database')
3 });

The Mongoose models live in the models folder:

1 var models = require('./models');

This middleware will provide access to two collections within our routes methods:

1 function db (req, res, next) {
2   req.db = {
3     User: connection.model('User', models.User, 'users'),
4     Post: connection.model('Post', models.Post, 'posts')
5   };
6   return next();
7 }

Just a new name for imported auth functions:

1 checkUser = routes.main.checkUser;
2 checkAdmin = routes.main.checkAdmin;
3 checkApplicant = routes.main.checkApplicant;

AngelList OAuth routes:

1 app.get('/auth/angellist', routes.auth.angelList);
2 app.get('/auth/angellist/callback',
3   routes.auth.angelListCallback,
4   routes.auth.angelListLogin,
5   db,
6   routes.users.findOrAddUser);

Main application routes including the api/profile return user session if user is logged in:

1 //MAIN
2 app.get('/api/profile', checkUser, db, routes.main.profile);
3 app.del('/api/profile', checkUser, db, routes.main.delProfile);
4 app.post('/api/login', db, routes.main.login);
5 app.post('/api/logout', routes.main.logout);

The POST requests for creating users and posts:

 1 //POSTS
 2 app.get('/api/posts', checkUser, db, routes.posts.getPosts);
 3 app.post('/api/posts', checkUser, db, routes.posts.add);
 4 app.get('/api/posts/:id', checkUser, db, routes.posts.getPost);
 5 app.put('/api/posts/:id', checkUser, db, routes.posts.updatePost);
 6 app.del('/api/posts/:id', checkUser, db, routes.posts.del);
 7 
 8 //USERS
 9 app.get('/api/users', checkUser, db, routes.users.getUsers);
10 app.get('/api/users/:id', checkUser, db,routes.users.getUser);
11 app.post('/api/users', checkAdmin, db, routes.users.add);
12 app.put('/api/users/:id', checkAdmin, db, routes.users.update);
13 app.del('/api/users/:id', checkAdmin, db, routes.users.del);

These routes are for new members that haven’t been approved yet:

 1 //APPLICATION
 2 app.post('/api/application',
 3   checkAdmin,
 4   db,
 5   routes.application.add);
 6 app.put('/api/application',
 7   checkApplicant,
 8   db,
 9   routes.application.update);
10 app.get('/api/application',
11   checkApplicant,
12   db,
13   routes.application.get);

The catch-all-else route:

1 app.get('*', function(req, res){
2   res.send(404);
3 });

The require.main === module is a clever trick to determine if this file is being executed as a standalone or as an imported module:

 1 http.createServer(app);
 2 if (require.main === module) {
 3   app.listen(app.get('port'), function(){
 4     console.info('Express server listening on port '
 5       + app.get('port'));
 6   });
 7 }
 8 else {
 9   console.info('Running app as a module')
10   exports.app = app;
11 }

The full source code for hackhall/server.js:

  1 var express = require('express'),
  2   routes = require('./routes'),
  3   http = require('http'),
  4   util = require('util'),
  5   oauth = require('oauth'),
  6   querystring = require('querystring');
  7 
  8 var app = express();
  9 app.configure(function(){
 10   app.set('port', process.env.PORT || 3000  );
 11   app.use(express.favicon());
 12   app.use(express.logger('dev'));
 13   app.use(express.bodyParser());
 14   app.use(express.methodOverride());
 15   app.use(express.cookieParser('asd;lfkajs;ldfkj'));
 16   app.use(express.session({
 17     secret: '<h1>WHEEYEEE</h1>',
 18     key: 'sid',
 19     cookie: {
 20       secret: true,
 21       expires: false      
 22     }
 23   }));
 24   // app.use(express.csrf());
 25   // app.use(function(req, res, next) {
 26     // res.locals.csrf = req.session._cstf;
 27     // return next();
 28   // });
 29   app.use(express.static(__dirname + '/public'));
 30   app.use(app.router);
 31 });
 32 
 33 app.configure(function() {
 34   app.use(logErrors);
 35   app.use(clientErrorHandler);
 36   app.use(errorHandler);
 37 });
 38 
 39 function logErrors(err, req, res, next) {
 40   console.error('logErrors', err.toString());
 41   next(err);
 42 }
 43 
 44 function clientErrorHandler(err, req, res, next) {
 45   console.error('clientErrors ', err.toString());
 46   res.send(500, { error: err.toString()});
 47   if (req.xhr) {
 48     console.error(err);
 49     res.send(500, { error: err.toString()});
 50   } else {
 51     next(err);
 52   }
 53 }
 54 
 55 function errorHandler(err, req, res, next) {
 56   console.error('lastErrors ', err.toString());  
 57   res.send(500, {error: err.toString()});
 58 }
 59 
 60 var dbUrl = process.env.MONGOHQ_URL || 'mongodb://@127.0.0.1:27017/hackhall';
 61 var mongoose = require('mongoose');
 62 var connection = mongoose.createConnection(dbUrl);
 63 connection.on('error', console.error.bind(console, 'connection error:'));
 64 connection.once('open', function () {
 65   console.info('connected to database')
 66 });
 67 
 68 var models = require('./models');
 69 function db (req, res, next) {
 70   req.db = {
 71     User: connection.model('User', models.User, 'users'),
 72     Post: connection.model('Post', models.Post, 'posts')
 73   };
 74   return next();
 75 }
 76 checkUser = routes.main.checkUser;
 77 checkAdmin = routes.main.checkAdmin;
 78 checkApplicant = routes.main.checkApplicant;
 79 
 80 app.get('/auth/angellist', routes.auth.angelList);
 81 app.get('/auth/angellist/callback',
 82   routes.auth.angelListCallback, 
 83   routes.auth.angelListLogin,
 84   db, 
 85   routes.users.findOrAddUser);
 86 
 87 //MAIN
 88 app.get('/api/profile', checkUser, db, routes.main.profile);
 89 app.del('/api/profile', checkUser, db, routes.main.delProfile);
 90 app.post('/api/login', db, routes.main.login);
 91 app.post('/api/logout', routes.main.logout);
 92 
 93 //POSTS
 94 app.get('/api/posts', checkUser, db, routes.posts.getPosts);
 95 app.post('/api/posts', checkUser, db, routes.posts.add);
 96 app.get('/api/posts/:id', checkUser, db, routes.posts.getPost);
 97 app.put('/api/posts/:id', checkUser, db, routes.posts.updatePost);
 98 app.del('/api/posts/:id', checkUser, db, routes.posts.del);
 99 
100 //USERS
101 app.get('/api/users', checkUser, db, routes.users.getUsers);
102 app.get('/api/users/:id', checkUser, db,routes.users.getUser);
103 app.post('/api/users', checkAdmin, db, routes.users.add);
104 app.put('/api/users/:id', checkAdmin, db, routes.users.update);
105 app.del('/api/users/:id', checkAdmin, db, routes.users.del);
106 
107 //APPLICATION 
108 app.post('/api/application', checkAdmin, db, routes.application.add); 
109 app.put('/api/application', checkApplicant, db, routes.application.update);
110 app.get('/api/application', checkApplicant, db, routes.application.get);
111 
112 app.get('*', function(req, res){
113   res.send(404);
114 });
115 
116 http.createServer(app);
117 if (require.main === module) {
118   app.listen(app.get('port'), function(){
119     console.info('Express server listening on port ' + app.get('port'));
120   });
121 }
122 else {
123   console.info('Running app as a module')
124   exports.app = app;
125 }

4.5 Routes

The HackHall routes reside in the hackhall/routes folder and are grouped into several modules:

  • hackhall/routes/index.js: bridge between server.js and other routes in the folder
  • hackhall/routes/auth.js: routes that handle OAuth dance with AngelList API
  • hackhall/routes/main.js: login, logout and other routes
  • hackhall/routes/users.js: routes related to users’ REST API
  • hackhall/routes/application.js: submission of application to become a user
  • hackhall/routes/posts.js: routes related to posts’ REST API

4.5.1 index.js

Let’s peek into hackhall/routes/index.js where we’ve included other modules:

1 exports.posts = require('./posts');
2 exports.main = require('./main');
3 exports.users = require('./users');
4 exports.application = require('./application');
5 exports.auth = require('./auth');

4.5.2 auth.js

In this module, we’ll handle OAuth dance with AngelList API. To do so, we’ll have to rely on the https library:

1 var https = require('https');

The AngelList API client ID and client secret are obtained at theangel.co/api website and stored in environment variables:

1 var angelListClientId = process.env.ANGELLIST_CLIENT_ID;
2 var angelListClientSecret = process.env.ANGELLIST_CLIENT_SECRET;

The method will redirect users to the angel.co website for authentication:

1 exports.angelList = function(req, res) {
2   res.redirect('https://angel.co/api/oauth/authorize?client_id=' + angelListClien\
3 tId + '&scope=email&response_type=code');
4 }

After users allow our app to access their information, AngelList sends them back to this route to allow us to make a new (HTTPS) request to retrieve the token:

 1 exports.angelListCallback = function(req, res, next) {
 2   var token;
 3   var buf = '';
 4   var data;
 5   // console.log('/api/oauth/token?client_id='
 6   //+ angelListClientId
 7   //+ '&client_secret='
 8   //+ angelListClientSecret
 9   //+ '&code='
10   //+ req.query.code
11   //+ '&grant_type=authorization_code');
12   var angelReq = https.request({
13       host: 'angel.co',
14       path: '/api/oauth/token?client_id='
15         + angelListClientId
16         + '&client_secret='
17         + angelListClientSecret
18         + '&code='
19         + req.query.code
20         + '&grant_type=authorization_code',
21       port: 443,
22       method: 'POST',
23       headers: {
24         'content-length': 0
25       }
26     },
27     function(angelRes) {
28       angelRes.on('data', function(buffer) {
29         buf += buffer;
30       });
31       angelRes.on('end', function() {
32         try {
33           data = JSON.parse(buf.toString('utf-8'));
34         } catch (e) {
35           if (e) return res.send(e);
36         }
37         if (!data || !data.access_token) return res.send(500);
38         token = data.access_token;
39         req.session.angelListAccessToken = token;
40         if (token) next();
41         else res.send(500);
42       });
43     });
44   angelReq.end();
45   angelReq.on('error', function(e) {
46     console.error(e);
47     next(e);
48   });
49 }

Directly call AngleList API with the token from the previous middleware to get user information:

 1 exports.angelListLogin = function(req, res, next) {
 2   token = req.session.angelListAccessToken;
 3   httpsRequest = https.request({
 4       host: 'api.angel.co',
 5       path: '/1/me?access_token=' + token,
 6       port: 443,
 7       method: 'GET'
 8     },
 9     function(httpsResponse) {
10       httpsResponse.on('data', function(buffer) {
11         data = JSON.parse(buffer.toString('utf-8'));
12         if (data) {
13           req.angelProfile = data;
14           next();
15         }
16       });
17     }
18   );
19   httpsRequest.end();
20   httpsRequest.on('error', function(e) {
21     console.error(e);
22   });
23 };

The full source code for hackhall/routes/auth.js files:

 1 var https = require('https');
 2 
 3 var angelListClientId = process.env.ANGELLIST_CLIENT_ID;
 4 var angelListClientSecret = process.env.ANGELLIST_CLIENT_SECRET;
 5 
 6 exports.angelList = function(req, res) {
 7   res.redirect('https://angel.co/api/oauth/authorize?client_id=' + angelListClien\
 8 tId + '&scope=email&response_type=code');
 9 }
10 exports.angelListCallback = function(req, res, next) {
11   var token;
12   var buf = '';
13   var data;
14   // console.log('/api/oauth/token?client_id=' + angelListClientId + '&client_sec\
15 ret=' + angelListClientSecret + '&code=' + req.query.code + '&grant_type=authoriz\
16 ation_code');
17   var angelReq = https.request({
18       host: 'angel.co',
19       path: '/api/oauth/token?client_id=' + angelListClientId + '&client_secret='\
20  + angelListClientSecret + '&code=' + req.query.code + '&grant_type=authorization\
21 _code',
22       port: 443,
23       method: 'POST',
24       headers: {
25         'content-length': 0
26       }
27     },
28     function(angelRes) {
29 
30       angelRes.on('data', function(buffer) {
31         buf += buffer;
32       });
33       angelRes.on('end', function() {
34         try {
35           data = JSON.parse(buf.toString('utf-8'));
36         } catch (e) {
37           if (e) return res.send(e);
38         }
39         if (!data || !data.access_token) return res.send(500);
40         token = data.access_token;
41         req.session.angelListAccessToken = token;
42         if (token) next();
43         else res.send(500);
44       });
45     });
46   angelReq.end();
47   angelReq.on('error', function(e) {
48     console.error(e);
49     next(e);
50   });
51 }
52 exports.angelListLogin = function(req, res, next) {
53   token = req.session.angelListAccessToken;
54   httpsRequest = https.request({
55       host: 'api.angel.co',
56       path: '/1/me?access_token=' + token,
57       port: 443,
58       method: 'GET'
59     },
60     function(httpsResponse) {
61       httpsResponse.on('data', function(buffer) {
62         data = JSON.parse(buffer.toString('utf-8'));
63         if (data) {
64           req.angelProfile = data;
65           next();
66         }
67       });
68     }
69   );
70   httpsRequest.end();
71   httpsRequest.on('error', function(e) {
72     console.error(e);
73   });
74 };

4.5.3 main.js

The hackhall/routes/main.js file is also interesting.

The checkAdmin() function performs authentication for admin privileges. If the session object doesn’t carry the proper flag, we call Express.js next() function with an error object:

 1 exports.checkAdmin = function(request, response, next) {
 2   if (request.session
 3     && request.session.auth
 4     && request.session.userId
 5     && request.session.admin) {
 6     console.info('Access ADMIN: ' + request.session.userId);
 7     return next();
 8   } else {
 9     next('User is not an administrator.');
10   }
11 };

Similarly, we can check only for the user without checking for admin rights:

1 exports.checkUser = function(req, res, next) {
2   if (req.session && req.session.auth && req.session.userId
3     && (req.session.user.approved || req.session.admin)) {
4     console.info('Access USER: ' + req.session.userId);
5     return next();
6   } else {
7     next('User is not logged in.');
8   }
9 };

If an application is just an unapproved user object, we can also check for that:

1 exports.checkApplicant = function(req, res, next) {
2   if (req.session && req.session.auth && req.session.userId
3     && (!req.session.user.approved || req.session.admin)) {
4     console.info('Access USER: ' + req.session.userId);
5     return next();
6   } else {
7     next('User is not logged in.');
8   }
9 };

In the login function, we search for email and password matches in the database. Upon success, we store the user object in the session and proceed; otherwise the request fails:

 1 exports.login = function(req, res, next) {
 2   req.db.User.findOne({
 3       email: req.body.email,
 4       password: req.body.password
 5     },
 6     null, {
 7       safe: true
 8     },
 9     function(err, user) {
10       if (err) return next(err);
11       if (user) {
12         req.session.auth = true;
13         req.session.userId = user._id.toHexString();
14         req.session.user = user;
15         if (user.admin) {
16           req.session.admin = true;
17         }
18         console.info('Login USER: ' + req.session.userId);
19         res.json(200, {
20           msg: 'Authorized'
21         });
22       } else {
23         next(new Error('User is not found.'));
24       }
25     });
26 };

The logging out process removes any session information:

 1 exports.logout = function(req, res) {
 2   console.info('Logout USER: ' + req.session.userId);
 3   req.session.destroy(function(error) {
 4     if (!error) {
 5       res.send({
 6         msg: 'Logged out'
 7       });
 8     }
 9   });
10 };

This route is used for the Profile page as well as by Backbone.js for user authentication:

 1 exports.profile = function(req, res, next) {
 2   req.db.User.findById(req.session.userId, 'firstName lastName'
 3       + 'displayName headline photoUrl admin'
 4       + 'approved banned role angelUrl twitterUrl'
 5       + 'facebookUrl linkedinUrl githubUrl', function(err, obj) {
 6     if (err) next(err);
 7     if (!obj) next(new Error('User is not found'));
 8     req.db.Post.find({
 9       author: {
10         id: obj._id,
11         name: obj.displayName
12       }
13     }, null, {
14       sort: {
15         'created': -1
16       }
17     }, function(err, list) {
18       if (err) next(err);
19       obj.posts.own = list || [];
20       req.db.Post.find({
21         likes: obj._id
22       }, null, {
23         sort: {
24           'created': -1
25         }

This logic finds posts and comments made by the user:

 1       }, function(err, list) {
 2         if (err) next(err);
 3         obj.posts.likes = list || [];
 4         req.db.Post.find({
 5           watches: obj._id
 6         }, null, {
 7           sort: {
 8             'created': -1
 9           }
10         }, function(err, list) {
11           if (err) next(err);
12           obj.posts.watches = list || [];
13           req.db.Post.find({
14             'comments.author.id': obj._id
15           }, null, {
16             sort: {
17               'created': -1
18             }
19           }, function(err, list) {
20             if (err) next(err);
21             obj.posts.comments = [];
22             list.forEach(function(value, key, list) {
23               obj.posts.comments.push(
24                 value.comments.filter(
25                   function(el, i, arr) {
26                     return (el.author.id.toString() == obj._id.toString());
27                   }
28                 )
29               );
30             });
31             res.json(200, obj);
32           });
33         });
34       });
35     });
36   });
37 };

It’s important to allow users to delete their profiles:

 1 exports.delProfile = function(req, res, next) {
 2   console.log('del profile');
 3   console.log(req.session.userId);
 4   req.db.User.findByIdAndRemove(req.session.user._id, {},
 5     function(err, obj) {
 6       if (err) next(err);
 7       req.session.destroy(function(error) {
 8         if (err) {
 9           next(err)
10         }
11       });
12       res.json(200, obj);
13     }
14   );
15 };

The full source code of hackhall/routes/main.js files:

  1 exports.checkAdmin = function(request, response, next) {
  2   if (request.session && request.session.auth && request.session.userId && reques\
  3 t.session.admin) {
  4     console.info('Access ADMIN: ' + request.session.userId);
  5     return next();
  6   } else {
  7     next('User is not an administrator.');
  8   }
  9 };
 10 
 11 exports.checkUser = function(req, res, next) {
 12   if (req.session && req.session.auth && req.session.userId && (req.session.user.\
 13 approved || req.session.admin)) {
 14     console.info('Access USER: ' + req.session.userId);
 15     return next();
 16   } else {
 17     next('User is not logged in.');
 18   }
 19 };
 20 
 21 exports.checkApplicant = function(req, res, next) {
 22   if (req.session && req.session.auth && req.session.userId && (!req.session.user\
 23 .approved || req.session.admin)) {
 24     console.info('Access USER: ' + req.session.userId);
 25     return next();
 26   } else {
 27     next('User is not logged in.');
 28   }
 29 };
 30 
 31 exports.login = function(req, res, next) {
 32   req.db.User.findOne({
 33       email: req.body.email,
 34       password: req.body.password
 35     },
 36     null, {
 37       safe: true
 38     },
 39     function(err, user) {
 40       if (err) return next(err);
 41       if (user) {
 42         req.session.auth = true;
 43         req.session.userId = user._id.toHexString();
 44         req.session.user = user;
 45         if (user.admin) {
 46           req.session.admin = true;
 47         }
 48         console.info('Login USER: ' + req.session.userId);
 49         res.json(200, {
 50           msg: 'Authorized'
 51         });
 52       } else {
 53         next(new Error('User is not found.'));
 54       }
 55     });
 56 };
 57 
 58 exports.logout = function(req, res) {
 59   console.info('Logout USER: ' + req.session.userId);
 60   req.session.destroy(function(error) {
 61     if (!error) {
 62       res.send({
 63         msg: 'Logged out'
 64       });
 65     }
 66   });
 67 };
 68 
 69 exports.profile = function(req, res, next) {
 70   req.db.User.findById(req.session.userId, 'firstName lastName displayName headli\
 71 ne photoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUr\
 72 l githubUrl', function(err, obj) {
 73     if (err) next(err);
 74     if (!obj) next(new Error('User is not found'));
 75     req.db.Post.find({
 76       author: {
 77         id: obj._id,
 78         name: obj.displayName
 79       }
 80     }, null, {
 81       sort: {
 82         'created': -1
 83       }
 84     }, function(err, list) {
 85       if (err) next(err);
 86       obj.posts.own = list || [];
 87       req.db.Post.find({
 88         likes: obj._id
 89       }, null, {
 90         sort: {
 91           'created': -1
 92         }
 93       }, function(err, list) {
 94         if (err) next(err);
 95         obj.posts.likes = list || [];
 96         req.db.Post.find({
 97           watches: obj._id
 98         }, null, {
 99           sort: {
100             'created': -1
101           }
102         }, function(err, list) {
103           if (err) next(err);
104           obj.posts.watches = list || [];
105           req.db.Post.find({
106             'comments.author.id': obj._id
107           }, null, {
108             sort: {
109               'created': -1
110             }
111           }, function(err, list) {
112             if (err) next(err);
113             obj.posts.comments = [];
114             list.forEach(function(value, key, list) {
115               obj.posts.comments.push(value.comments.filter(function(el, i, arr) {
116                 return (el.author.id.toString() == obj._id.toString());
117               }));
118             });
119             res.json(200, obj);
120           });
121         });
122       });
123     });
124   });
125 };
126 
127 exports.delProfile = function(req, res, next) {
128   console.log('del profile');
129   console.log(req.session.userId);
130   req.db.User.findByIdAndRemove(req.session.user._id, {}, function(err, obj) {
131     if (err) next(err);
132     req.session.destroy(function(error) {
133       if (err) {
134         next(err)
135       }
136     });
137     res.json(200, obj);
138   });
139 };

4.5.4 users.js

The full source code for hackhall/routes/users.js files:

  1 objectId = require('mongodb').ObjectID;
  2 
  3 exports.getUsers = function(req, res, next) {
  4   if (req.session.auth && req.session.userId) {
  5     req.db.User.find({}, 'firstName lastName displayName headline photoUrl admin \
  6 approved banned role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl', func\
  7 tion(err, list) {
  8       if (err) next(err);
  9       res.json(200, list);
 10     });
 11   } else {
 12     next('User is not recognized.')
 13   }
 14 }
 15 
 16 exports.getUser = function(req, res, next) {
 17   req.db.User.findById(req.params.id, 'firstName lastName displayName headline ph\
 18 otoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUrl git\
 19 hubUrl', function(err, obj) {
 20     if (err) next(err);
 21     if (!obj) next(new Error('User is not found'));
 22     req.db.Post.find({
 23       author: {
 24         id: obj._id,
 25         name: obj.displayName
 26       }
 27     }, null, {
 28       sort: {
 29         'created': -1
 30       }
 31     }, function(err, list) {
 32       if (err) next(err);
 33       obj.posts.own = list || [];
 34       req.db.Post.find({
 35         likes: obj._id
 36       }, null, {
 37         sort: {
 38           'created': -1
 39         }
 40       }, function(err, list) {
 41         if (err) next(err);
 42         obj.posts.likes = list || [];
 43         req.db.Post.find({
 44           watches: obj._id
 45         }, null, {
 46           sort: {
 47             'created': -1
 48           }
 49         }, function(err, list) {
 50           if (err) next(err);
 51           obj.posts.watches = list || [];
 52           req.db.Post.find({
 53             'comments.author.id': obj._id
 54           }, null, {
 55             sort: {
 56               'created': -1
 57             }
 58           }, function(err, list) {
 59             if (err) next(err);
 60             obj.posts.comments = [];
 61             list.forEach(function(value, key, list) {
 62               obj.posts.comments.push(value.comments.filter(function(el, i, arr) {
 63                 return (el.author.id.toString() == obj._id.toString());
 64               }));
 65             });
 66             res.json(200, obj);
 67           });
 68         });
 69       });
 70     });
 71   });
 72 };
 73 
 74 exports.add = function(req, res, next) {
 75   var user = new req.db.User(req.body);
 76   user.save(function(err) {
 77     if (err) next(err);
 78     res.json(user);
 79   });
 80 };
 81 
 82 exports.update = function(req, res, next) {
 83   var obj = req.body;
 84   obj.updated = new Date();
 85   delete obj._id;
 86   req.db.User.findByIdAndUpdate(req.params.id, {
 87     $set: obj
 88   }, {
 89     new: true
 90   }, function(err, obj) {
 91     if (err) next(err);
 92     res.json(200, obj);
 93   });
 94 };
 95 
 96 exports.del = function(req, res, next) {
 97   req.db.User.findByIdAndRemove(req.params.id, function(err, obj) {
 98     if (err) next(err);
 99     res.json(200, obj);
100   });
101 };
102 
103 exports.findOrAddUser = function(req, res, next) {
104   data = req.angelProfile;
105   req.db.User.findOne({
106     angelListId: data.id
107   }, function(err, obj) {
108     console.log('angelListLogin4');
109     if (err) next(err);
110     console.warn(obj);
111     if (!obj) {
112       req.db.User.create({
113         angelListId: data.id,
114         angelToken: token,
115         angelListProfile: data,
116         email: data.email,
117         firstName: data.name.split(' ')[0],
118         lastName: data.name.split(' ')[1],
119         displayName: data.name,
120         headline: data.bio,
121         photoUrl: data.image,
122         angelUrl: data.angellist_url,
123         twitterUrl: data.twitter_url,
124         facebookUrl: data.facebook_url,
125         linkedinUrl: data.linkedin_url,
126         githubUrl: data.github_url
127       }, function(err, obj) { //remember the scope of variables!
128         if (err) next(err);
129         console.log(obj);
130         req.session.auth = true;
131         req.session.userId = obj._id;
132         req.session.user = obj;
133         req.session.admin = false; //assing regular user role by default         \
134          
135         res.redirect('/#application');
136         // }
137       });
138     } else { //user is in the database
139       req.session.auth = true;
140       req.session.userId = obj._id;
141       req.session.user = obj;
142       req.session.admin = obj.admin; //false; //assing regular user role by defau\
143 lt
144       if (obj.approved) {
145         res.redirect('/#posts');
146       } else {
147         res.redirect('/#application');
148       }
149     }
150   })
151 }

4.5.5 applications.js

In the current version, submitting and approving an application won’t trigger an email notification. Therefore, users have to come back to the website to check their status.

Merely add a user object (with approved=false by default) to the database:

 1 exports.add = function(req, res, next) {
 2   req.db.User.create({
 3     firstName: req.body.firstName,
 4     lastName: req.body.lastName,
 5     displayName: req.body.displayName,
 6     headline: req.body.headline,
 7     photoUrl: req.body.photoUrl,
 8     password: req.body.password,
 9     email: req.body.email,
10     angelList: {
11       blah: 'blah'
12     },
13     angelUrl: req.body.angelUrl,
14     twitterUrl: req.body.twitterUrl,
15     facebookUrl: req.body.facebookUrl,
16     linkedinUrl: req.body.linkedinUrl,
17     githubUrl: req.body.githubUrl
18   }, function(err, obj) {
19     if (err) next(err);
20     if (!obj) next('Cannot create.')
21     res.json(200, obj);
22   })
23 };

Let the users update information in their applications:

 1 exports.update = function(req, res, next) {
 2   var data = {};
 3   Object.keys(req.body).forEach(function(k) {
 4     if (req.body[k]) {
 5       data[k] = req.body[k];
 6     }
 7   });
 8   delete data._id;
 9   req.db.User.findByIdAndUpdate(req.session.user._id, {
10     $set: data
11   }, function(err, obj) {
12     if (err) next(err);
13     if (!obj) next('Cannot save.')
14     res.json(200, obj);
15   });
16 };

Select a particular object with the get() function:

 1 exports.get = function(req, res, next) {
 2   req.db.User.findById(req.session.user._id,
 3     'firstName lastName photoUrl headline displayName'
 4       + 'angelUrl facebookUrl twitterUrl linkedinUrl'
 5       + 'githubUrl', {}, function(err, obj) {
 6       if (err) next(err);
 7       if (!obj) next('cannot find');
 8       res.json(200, obj);
 9     })
10 };

The full source code of hackhall/routes/applications.js files:

<<(hackhall/routes/applications.js)

4.5.6 posts.js

The last routes module that we bisect is hackhall/routes/posts.js. It takes care of adding, editing and removing posts, as well as commenting, watching and liking.

We use object ID for conversion from HEX strings to proper objects:

1 objectId = require('mongodb').ObjectID;

The coloring is nice for logging, but it’s of course optional. We accomplish it with escape sequences:

1 var red, blue, reset;
2 red   = '\u001b[31m';
3 blue  = '\u001b[34m';
4 reset = '\u001b[0m';
5 console.log(red + 'This is red' + reset + ' while ' + blue + ' this is blue' + re\
6 set);

The default values for the pagination of posts:

1 var LIMIT = 10;
2 var SKIP = 0;

The add() function handles creation of new posts:

 1 exports.add = function(req, res, next) {
 2   if (req.body) {
 3     req.db.Post.create({
 4       title: req.body.title,
 5       text: req.body.text || null,
 6       url: req.body.url || null,
 7       author: {
 8         id: req.session.user._id,
 9         name: req.session.user.displayName
10       }
11     }, function(err, docs) {
12       if (err) {
13         console.error(err);
14         next(err);
15       } else {
16         res.json(200, docs);
17       }
18 
19     });
20   } else {
21     next(new Error('No data'));
22   }
23 };

To retrieve the list of posts:

 1 exports.getPosts = function(req, res, next) {
 2   var limit = req.query.limit || LIMIT;
 3   var skip = req.query.skip || SKIP;
 4   req.db.Post.find({}, null, {
 5     limit: limit,
 6     skip: skip,
 7     sort: {
 8       '_id': -1
 9     }
10   }, function(err, obj) {
11     if (!obj) next('There are not posts.');
12     obj.forEach(function(item, i, list) {
13       if (req.session.user.admin) {
14         item.admin = true;
15       } else {
16         item.admin = false;
17       }
18       if (item.author.id == req.session.userId) {
19         item.own = true;
20       } else {
21         item.own = false;
22       }
23       if (item.likes
24         && item.likes.indexOf(req.session.userId) > -1) {
25         item.like = true;
26       } else {
27         item.like = false;
28       }
29       if (item.watches
30         && item.watches.indexOf(req.session.userId) > -1) {
31         item.watch = true;
32       } else {
33         item.watch = false;
34       }
35     });
36     var body = {};
37     body.limit = limit;
38     body.skip = skip;
39     body.posts = obj;
40     req.db.Post.count({}, function(err, total) {
41       if (err) next(err);
42       body.total = total;
43       res.json(200, body);
44     });
45   });
46 };

For the individual post page, we need the getPost() method:

 1 exports.getPost = function(req, res, next) {
 2   if (req.params.id) {
 3     req.db.Post.findById(req.params.id, {
 4       title: true,
 5       text: true,
 6       url: true,
 7       author: true,
 8       comments: true,
 9       watches: true,
10       likes: true
11     }, function(err, obj) {
12       if (err) next(err);
13       if (!obj) {
14         next('Nothing is found.');
15       } else {
16         res.json(200, obj);
17       }
18     });
19   } else {
20     next('No post id');
21   }
22 };

The del() function removes specific posts from the database. The findById() and remove() methods from Mongoose are used in this snippet. However, the same thing can be accomplished with just remove().

 1 exports.del = function(req, res, next) {
 2   req.db.Post.findById(req.params.id, function(err, obj) {
 3     if (err) next(err);
 4     if (req.session.admin || req.session.userId === obj.author.id) {
 5       obj.remove();
 6       res.json(200, obj);
 7     } else {
 8       next('User is not authorized to delete post.');
 9     }
10   })
11 };

To like the post, we update the post item by prepending the post.likes array with the ID of the user:

 1 function likePost(req, res, next) {
 2   req.db.Post.findByIdAndUpdate(req.body._id, {
 3     $push: {
 4       likes: req.session.userId
 5     }
 6   }, {}, function(err, obj) {
 7     if (err) {
 8       next(err);
 9     } else {
10       res.json(200, obj);
11     }
12   });
13 };

Likewise, when a user performs the watch action, the system adds a new ID to the post.watches array:

 1 function watchPost(req, res, next) {
 2   req.db.Post.findByIdAndUpdate(req.body._id, {
 3     $push: {
 4       watches: req.session.userId
 5     }
 6   }, {}, function(err, obj) {
 7     if (err) next(err);
 8     else {
 9       res.json(200, obj);
10     }
11   });
12 };

The updatePost() is what calls like or watch functions based on the action flag sent with the request. In addition, the updatePost() processes the changes to the post and comments:

 1 exports.updatePost = function(req, res, next) {
 2   var anyAction = false;
 3   if (req.body._id && req.params.id) {
 4     if (req.body && req.body.action == 'like') {
 5       anyAction = true;
 6       likePost(req, res);
 7     }
 8     if (req.body && req.body.action == 'watch') {
 9       anyAction = true;
10       watchPost(req, res);
11     }
12     if (req.body && req.body.action == 'comment'
13       && req.body.comment && req.params.id) {
14       anyAction = true;
15       req.db.Post.findByIdAndUpdate(req.params.id, {
16         $push: {
17           comments: {
18             author: {
19               id: req.session.userId,
20               name: req.session.user.displayName
21             },
22             text: req.body.comment
23           }
24         }
25       }, {
26         safe: true,
27         new: true
28       }, function(err, obj) {
29         if (err) throw err;
30         res.json(200, obj);
31       });
32     }
33     if (req.session.auth && req.session.userId && req.body
34       && req.body.action != 'comment' &&
35       req.body.action != 'watch' && req.body != 'like' &&
36       req.params.id && (req.body.author.id == req.session.user._id
37       || req.session.user.admin)) {
38       req.db.Post.findById(req.params.id, function(err, doc) {
39         if (err) next(err);
40         doc.title = req.body.title;
41         doc.text = req.body.text || null;
42         doc.url = req.body.url || null;
43         doc.save(function(e, d) {
44           if (e) next(e);
45           res.json(200, d);
46         });
47       })
48     } else {
49       if (!anyAction) next('Something went wrong.');
50     }
51 
52   } else {
53     next('No post ID.');
54   }
55 };

The full source code for the hackhall/routes/posts.js file:

  1 objectId = require('mongodb').ObjectID;
  2 var red, blue, reset;
  3 red = '\u001b[31m';
  4 blue = '\u001b[34m';
  5 reset = '\u001b[0m';
  6 console.log(red + 'This is red' + reset + ' while ' + blue + ' this is blue' + re\
  7 set);
  8 
  9 var LIMIT = 10;
 10 var SKIP = 0;
 11 
 12 exports.add = function(req, res, next) {
 13   if (req.body) {
 14     req.db.Post.create({
 15       title: req.body.title,
 16       text: req.body.text || null,
 17       url: req.body.url || null,
 18       author: {
 19         id: req.session.user._id,
 20         name: req.session.user.displayName
 21       }
 22     }, function(err, docs) {
 23       if (err) {
 24         console.error(err);
 25         next(err);
 26       } else {
 27         res.json(200, docs);
 28       }
 29 
 30     });
 31   } else {
 32     next(new Error('No data'));
 33   }
 34 };
 35 
 36 exports.getPosts = function(req, res, next) {
 37   var limit = req.query.limit || LIMIT;
 38   var skip = req.query.skip || SKIP;
 39   req.db.Post.find({}, null, {
 40     limit: limit,
 41     skip: skip,
 42     sort: {
 43       '_id': -1
 44     }
 45   }, function(err, obj) {
 46     if (!obj) next('There are not posts.');
 47     obj.forEach(function(item, i, list) {
 48       if (req.session.user.admin) {
 49         item.admin = true;
 50       } else {
 51         item.admin = false;
 52       }
 53       if (item.author.id == req.session.userId) {
 54         item.own = true;
 55       } else {
 56         item.own = false;
 57       }
 58       if (item.likes && item.likes.indexOf(req.session.userId) > -1) {
 59         item.like = true;
 60       } else {
 61         item.like = false;
 62       }
 63       if (item.watches && item.watches.indexOf(req.session.userId) > -1) {
 64         item.watch = true;
 65       } else {
 66         item.watch = false;
 67       }
 68     });
 69     var body = {};
 70     body.limit = limit;
 71     body.skip = skip;
 72     body.posts = obj;
 73     req.db.Post.count({}, function(err, total) {
 74       if (err) next(err);
 75       body.total = total;
 76       res.json(200, body);
 77     });
 78   });
 79 };
 80 
 81 
 82 exports.getPost = function(req, res, next) {
 83   if (req.params.id) {
 84     req.db.Post.findById(req.params.id, {
 85       title: true,
 86       text: true,
 87       url: true,
 88       author: true,
 89       comments: true,
 90       watches: true,
 91       likes: true
 92     }, function(err, obj) {
 93       if (err) next(err);
 94       if (!obj) {
 95         next('Nothing is found.');
 96       } else {
 97         res.json(200, obj);
 98       }
 99     });
100   } else {
101     next('No post id');
102   }
103 };
104 
105 exports.del = function(req, res, next) {
106   req.db.Post.findById(req.params.id, function(err, obj) {
107     if (err) next(err);
108     if (req.session.admin || req.session.userId === obj.author.id) {
109       obj.remove();
110       res.json(200, obj);
111     } else {
112       next('User is not authorized to delete post.');
113     }
114   })
115 };
116 
117 function likePost(req, res, next) {
118   req.db.Post.findByIdAndUpdate(req.body._id, {
119     $push: {
120       likes: req.session.userId
121     }
122   }, {}, function(err, obj) {
123     if (err) {
124       next(err);
125     } else {
126       res.json(200, obj);
127     }
128   });
129 };
130 
131 function watchPost(req, res, next) {
132   req.db.Post.findByIdAndUpdate(req.body._id, {
133     $push: {
134       watches: req.session.userId
135     }
136   }, {}, function(err, obj) {
137     if (err) next(err);
138     else {
139       res.json(200, obj);
140     }
141   });
142 };
143 
144 exports.updatePost = function(req, res, next) {
145   var anyAction = false;
146   if (req.body._id && req.params.id) {
147     if (req.body && req.body.action == 'like') {
148       anyAction = true;
149       likePost(req, res);
150     }
151     if (req.body && req.body.action == 'watch') {
152       anyAction = true;
153       watchPost(req, res);
154     }
155     if (req.body && req.body.action == 'comment' && req.body.comment && req.param\
156 s.id) {
157       anyAction = true;
158       req.db.Post.findByIdAndUpdate(req.params.id, {
159         $push: {
160           comments: {
161             author: {
162               id: req.session.userId,
163               name: req.session.user.displayName
164             },
165             text: req.body.comment
166           }
167         }
168       }, {
169         safe: true,
170         new: true
171       }, function(err, obj) {
172         if (err) throw err;
173         res.json(200, obj);
174       });
175     }
176     if (req.session.auth && req.session.userId && req.body && req.body.action != \
177 'comment' &&
178       req.body.action != 'watch' && req.body != 'like' &&
179       req.params.id && (req.body.author.id == req.session.user._id || req.session\
180 .user.admin)) {
181       req.db.Post.findById(req.params.id, function(err, doc) {
182         if (err) next(err);
183         doc.title = req.body.title;
184         doc.text = req.body.text || null;
185         doc.url = req.body.url || null;
186         doc.save(function(e, d) {
187           if (e) next(e);
188           res.json(200, d);
189         });
190       })
191     } else {
192       if (!anyAction) next('Something went wrong.');
193     }
194 
195   } else {
196     next('No post ID.');
197   }
198 };

4.6 Mogoose Models

Ideally, in a big application, we would break down each model into a separate file. Right now in the HackHall app, we have them all in hackhall/models/index.js.

As always, our dependencies look better at the top:

1 var mongoose = require('mongoose');
2 var Schema = mongoose.Schema;
3 var roles = 'user staff mentor investor founder'.split(' ');

The Post model represents post with its likes, comments and watches.

 1 exports.Post = new Schema ({
 2   title: {
 3     required: true,
 4     type: String,
 5     trim: true,
 6     // match: /^([[:alpha:][:space:][:punct:]]{1,100})$/
 7     match: /^([\w ,.!?]{1,100})$/
 8   },
 9   url: {
10     type: String,
11     trim: true,
12     max: 1000
13   },
14   text: {
15     type: String,
16     trim: true,
17     max: 2000
18   },
19   comments: [{
20     text: {
21       type: String,
22       trim: true,
23       max:2000
24     },
25     author: {
26       id: {
27         type: Schema.Types.ObjectId,
28         ref: 'User'
29       },
30       name: String
31     }
32   }],
33   watches: [{
34     type: Schema.Types.ObjectId,
35     ref: 'User'
36   }],
37   likes: [{
38     type: Schema.Types.ObjectId,
39     ref: 'User'
40   }],
41   author: {
42     id: {
43       type: Schema.Types.ObjectId,
44       ref: 'User',
45       required: true
46     },
47     name: {
48       type: String,
49       required: true
50     }
51   },
52   created: {
53     type: Date,
54     default: Date.now,
55     required: true
56   },
57   updated:  {
58     type: Date,
59     default: Date.now,
60     required: true
61   },
62   own: Boolean,
63   like: Boolean,
64   watch: Boolean,
65   admin: Boolean,
66   action: String
67 });

The User model can also serve as an application object (when approved=false):

 1 exports.User = new Schema({
 2   angelListId: String,
 3   angelListProfile: Schema.Types.Mixed,
 4   angelToken: String,
 5     firstName: {
 6     type: String,
 7     required: true,
 8     trim: true
 9   },
10   lastName: {
11     type: String,
12     required: true,
13     trim: true
14   },
15   displayName: {
16     type: String,
17     required: true,
18     trim: true
19   },
20     password: String,
21     email: {
22     type: String,
23     required: true,
24     trim: true
25   },
26   role: {
27     type:String,
28     enum: roles,
29     required: true,
30     default: roles[0]
31   },
32   approved: {
33     type: Boolean,
34     default: false
35   },
36   banned: {
37     type: Boolean,
38     default: false
39   },
40   admin: {
41     type: Boolean,
42     default: false
43   },
44   headline: String,
45   photoUrl: String,
46   angelList: Schema.Types.Mixed,
47   created: {
48     type: Date,
49     default: Date.now
50   },
51   updated:  {
52     type: Date, default: Date.now
53   },
54   angelUrl: String,
55   twitterUrl: String,
56   facebookUrl: String,
57   linkedinUrl: String,
58   githubUrl: String,
59   own: Boolean,
60   posts: {
61     own: [Schema.Types.Mixed],
62     likes: [Schema.Types.Mixed],
63     watches: [Schema.Types.Mixed],
64     comments: [Schema.Types.Mixed]
65   }
66 });

The full source code for hackhall/models/index.js:

  1 var mongoose = require('mongoose');
  2 var Schema = mongoose.Schema;
  3 var roles = 'user staff mentor investor founder'.split(' ');
  4 
  5 exports.Post = new Schema ({
  6   title: {
  7     required: true,
  8     type: String,
  9     trim: true,
 10     // match: /^([[:alpha:][:space:][:punct:]]{1,100})$/
 11     match: /^([\w ,.!?]{1,100})$/
 12   },
 13   url: {
 14     type: String,
 15     trim: true,
 16     max: 1000
 17   },
 18   text: {
 19     type: String,
 20     trim: true,
 21     max: 2000
 22   },
 23   comments: [{
 24     text: {
 25       type: String,
 26       trim: true,
 27       max:2000
 28     },
 29     author: {
 30       id: { 
 31         type: Schema.Types.ObjectId, 
 32         ref: 'User' 
 33       },
 34       name: String
 35     }
 36   }],
 37   watches: [{ 
 38     type: Schema.Types.ObjectId, 
 39     ref: 'User' 
 40   }],
 41   likes: [{
 42     type: Schema.Types.ObjectId, 
 43     ref: 'User'
 44   }],
 45   author: {
 46     id: { 
 47       type: Schema.Types.ObjectId, 
 48       ref: 'User',
 49       required: true
 50     },
 51     name: {
 52       type: String,
 53       required: true
 54     }
 55   },
 56   created: { 
 57     type: Date, 
 58     default: Date.now,
 59     required: true
 60   },
 61   updated:  { 
 62     type: Date, 
 63     default: Date.now, 
 64     required: true
 65   },  
 66   own: Boolean,
 67   like: Boolean,
 68   watch: Boolean,
 69   admin: Boolean,
 70   action: String  
 71 });
 72 
 73 exports.User = new Schema({
 74   angelListId: String,
 75   angelListProfile: Schema.Types.Mixed,
 76   angelToken: String, 
 77     firstName: {
 78     type: String,
 79     required: true,
 80     trim: true
 81   },
 82   lastName: {
 83     type: String,
 84     required: true,
 85     trim: true
 86   },
 87   displayName: {
 88     type: String,
 89     required: true,
 90     trim: true
 91   },
 92     password: String,
 93     email: {
 94     type: String,
 95     required: true,
 96     trim: true
 97   },
 98   role: {
 99     type:String,
100     enum: roles,
101     required: true,
102     default: roles[0]
103   },
104   approved: {
105     type: Boolean,
106     default: false
107   },
108   banned: {
109     type: Boolean,
110     default: false
111   },  
112   admin: {
113     type: Boolean,
114     default: false
115   },  
116   headline: String,
117   photoUrl: String,
118   angelList: Schema.Types.Mixed,
119   created: { 
120     type: Date, 
121     default: Date.now 
122   },
123   updated:  { 
124     type: Date, default: Date.now 
125   },
126   angelUrl: String,
127   twitterUrl: String,
128   facebookUrl: String,
129   linkedinUrl: String,
130   githubUrl: String,
131   own: Boolean,
132   posts: {
133     own: [Schema.Types.Mixed],
134     likes: [Schema.Types.Mixed],
135     watches: [Schema.Types.Mixed],
136     comments: [Schema.Types.Mixed]
137   }
138 });

4.7 Mocha Tests

One of the benefits of using REST API server architecture is that each route and the application as a whole become very testable. The assurance of the passed tests is a wonderful supplement during development — the so-called test-driven development approach.

To run tests, we utilize Makefile:

 1 REPORTER = list
 2 MOCHA_OPTS = --ui tdd --ignore-leaks
 3 
 4 test: 
 5 	clear
 6 	echo Starting test *********************************************************
 7 	./node_modules/mocha/bin/mocha \
 8 	--reporter $(REPORTER) \
 9 	$(MOCHA_OPTS) \
10 	tests/*.js
11 	echo Ending test
12 
13 test-w: 
14 	./node_modules/mocha/bin/mocha \
15 	--reporter $(REPORTER) \
16 	--growl \
17 	--watch \
18 	$(MOCHA_OPTS) \
19 	tests/*.js 
20 
21 users: 
22 	mocha tests/users.js --ui tdd --reporter list --ignore-leaks
23 	
24 posts: 
25 	clear
26 	echo Starting test *********************************************************
27 	./node_modules/mocha/bin/mocha \
28 	--reporter $(REPORTER) \
29 	$(MOCHA_OPTS) \
30 	tests/posts.js 
31 	echo Ending test
32 
33 application: 
34 	mocha tests/application.js --ui tdd --reporter list --ignore-leaks
35 	
36 .PHONY: test test-w posts application

Therefore, we can start tests with the $ make command.

All 20 tests should pass:

The results of running Mocha tests.

The results of running Mocha tests.

The HackHall tests live in the tests folder and consist of:

  • hackhall/tests/application.js: functional tests for unapproved users’ information
  • hackhall/tests/posts.js: functional tests for posts
  • hackhall/tests/users.js: functional tests for users

Tests use a library called superagent(GitHub).

The full content for hackhall/tests/application.js:

  1 var app = require ('../server').app,
  2   assert = require('assert'),
  3   request = require('superagent');
  4 
  5 app.listen(app.get('port'), function(){
  6   console.log('Express server listening on port ' + app.get('port'));
  7 });
  8 
  9 var user1 = request.agent();
 10 var port = 'http://localhost:'+app.get('port');
 11 var userId;
 12 
 13 suite('APPLICATION API', function (){
 14   suiteSetup(function(done){
 15     done();
 16   });
 17   test('log in as admin', function(done){
 18     user1.post(port+'/api/login').send({email:'1@1.com',password:'1'}).end(functi\
 19 on(res){
 20         assert.equal(res.status,200);
 21       done();
 22     });       
 23   });
 24   test('get profile for admin',function(done){
 25     user1.get(port+'/api/profile').end(function(res){
 26         assert.equal(res.status,200);
 27       done();
 28     });
 29   }); 
 30   test('submit applicaton for user 3@3.com', function(done){
 31     user1.post(port+'/api/application').send({
 32         firstName: 'Dummy',
 33       lastName: 'Application',
 34       displayName: 'Dummy Application',
 35         password: '3',
 36         email: '3@3.com',
 37       headline: 'Dummy Appliation',
 38       photoUrl: '/img/user.png',
 39       angelList: {blah:'blah'},
 40       angelUrl: 'http://angel.co.com/someuser',
 41       twitterUrl: 'http://twitter.com/someuser',
 42       facebookUrl: 'http://facebook.com/someuser',
 43       linkedinUrl: 'http://linkedin.com/someuser',
 44       githubUrl: 'http://github.com/someuser'     
 45     }).end(function(res){
 46       assert.equal(res.status,200);
 47       userId = res.body._id;        
 48       done();
 49     });
 50     
 51   });
 52   test('logout admin',function(done){
 53     user1.post(port+'/api/logout').end(function(res){
 54         assert.equal(res.status,200);
 55       done();
 56     });
 57   });
 58   test('get profile again after logging out',function(done){
 59     user1.get(port+'/api/profile').end(function(res){
 60         assert.equal(res.status,500);
 61       done();
 62     });
 63   }); 
 64   test('log in as user3 - unapproved', function(done){
 65     user1.post(port+'/api/login').send({email:'3@3.com',password:'3'}).end(functi\
 66 on(res){
 67         assert.equal(res.status,200);
 68       done();
 69     });       
 70   }); 
 71   test('get user application', function(done){
 72     user1.get(port+'/api/application/').end(function(res){
 73       // console.log(res.body)
 74       assert.equal(res.status, 200);  
 75       done();
 76     });
 77   }); 
 78   test('update user application', function(done){
 79     user1.put(port+'/api/application/').send({      
 80       firstName: 'boo'}).end(function(res){
 81       // console.log(res.body)
 82       assert.equal(res.status, 200);  
 83       done();
 84     });
 85   }); 
 86   test('get user application', function(done){
 87     user1.get(port+'/api/application/').end(function(res){
 88       // console.log(res.body)
 89       assert.equal(res.status, 200);  
 90       done();
 91     });
 92   }); 
 93   test('check for posts - fail (unapproved?)', function(done){
 94     user1.get(port+'/api/posts/').end(function(res){
 95       // console.log(res.body)
 96       assert.equal(res.status, 500);
 97   
 98       done();
 99     });
100   });
101   test('logout user',function(done){
102     user1.post(port+'/api/logout').end(function(res){
103         assert.equal(res.status,200);
104       done();
105     });
106   }); 
107   test('log in as admin', function(done){
108     user1.post(port+'/api/login').send({email:'1@1.com',password:'1'}).end(functi\
109 on(res){
110         assert.equal(res.status,200);
111       done();
112     });       
113   }); 
114   test('delete user3', function(done){
115     user1.del(port+'/api/users/'+userId).end(function(res){
116       assert.equal(res.status, 200);
117       done();
118     });   
119   }); 
120   test('logout admin',function(done){
121     user1.post(port+'/api/logout').end(function(res){
122         assert.equal(res.status,200);
123       done();
124     });
125   }); 
126   test('log in as user - should fail', function(done){
127     user1.post(port+'/api/login').send({email:'3@3.com',password:'3'}).end(functi\
128 on(res){
129       // console.log(res.body)
130         assert.equal(res.status,500);
131       
132       done();
133     });       
134   }); 
135   test('check for posts - must fail', function(done){
136     user1.get(port+'/api/posts/').end(function(res){
137       // console.log(res.body)
138       assert.equal(res.status, 500);
139   
140       done();
141     });
142   }); 
143   suiteTeardown(function(done){
144     done();
145   });
146 
147 });

The full content for hackhall/tests/posts.js:

 1 var app = require('../server').app,
 2   assert = require('assert'),
 3   request = require('superagent');
 4 
 5 app.listen(app.get('port'), function() {
 6   console.log('Express server listening on port ' + app.get('port'));
 7 });
 8 
 9 var user1 = request.agent();
10 var port = 'http://localhost:' + app.get('port');
11 var postId;
12 
13 suite('POSTS API', function() {
14   suiteSetup(function(done) {
15     done();
16   });
17   test('log in', function(done) {
18     user1.post(port + '/api/login').send({
19       email: '1@1.com',
20       password: '1'
21     }).end(function(res) {
22       assert.equal(res.status, 200);
23       done();
24     });
25   });
26   test('add post', function(done) {
27     user1.post(port + '/api/posts').send({
28       title: 'Yo Test Title',
29       text: 'Yo Test text',
30       url: ''
31     }).end(function(res) {
32       assert.equal(res.status, 200);
33       postId = res.body._id;
34       done();
35     });
36 
37   });
38   test('get profile', function(done) {
39     user1.get(port + '/api/profile').end(function(res) {
40       assert.equal(res.status, 200);
41       done();
42     });
43   });
44   test('post get', function(done) {
45     // console.log('000'+postId);
46     user1.get(port + '/api/posts/' + postId).end(function(res) {
47       assert.equal(res.status, 200);
48       assert.equal(res.body._id, postId);
49       done();
50     });
51   });
52   test('delete post', function(done) {
53     user1.del(port + '/api/posts/' + postId).end(function(res) {
54       assert.equal(res.status, 200);
55       done();
56     });
57   });
58   test('check for deleted post', function(done) {
59     user1.get(port + '/api/posts/' + postId).end(function(res) {
60       // console.log(res.body)
61       assert.equal(res.status, 500);
62 
63       done();
64     });
65   });
66   suiteTeardown(function(done) {
67     done();
68   });
69 
70 });

The full content for hackhall/tests/users.js:

  1 var app = require('../server').app,
  2   assert = require('assert'),
  3   request = require('superagent');
  4 // http = require('support/http');
  5 
  6 var user1 = request.agent();
  7 var port = 'http://localhost:' + app.get('port');
  8 
  9 
 10 app.listen(app.get('port'), function() {
 11   console.log('Express server listening on port ' + app.get('port'));
 12 });
 13 
 14 suite('Test root', function() {
 15   setup(function(done) {
 16     console.log('setup');
 17 
 18     done();
 19   });
 20 
 21   test('check /', function(done) {
 22     request.get('http://localhost:3000').end(function(res) {
 23       assert.equal(res.status, 200);
 24       done();
 25     });
 26   });
 27   test('check /api/profile', function(done) {
 28     request.get('http://localhost:' + app.get('port') + '/api/profile').end(funct\
 29 ion(res) {
 30       assert.equal(res.status, 500);
 31       done();
 32     });
 33   });
 34   test('check /api/users', function(done) {
 35     user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(\
 36 res) {
 37       assert.equal(res.status, 500);
 38       // console.log(res.text.length);
 39       done();
 40     });
 41     // done();
 42   });
 43   test('check /api/posts', function(done) {
 44     user1.get('http://localhost:' + app.get('port') + '/api/posts').end(function(\
 45 res) {
 46       assert.equal(res.status, 500);
 47       // console.log(res.text.length);
 48       done();
 49     });
 50     // done();
 51   });
 52   teardown(function(done) {
 53     console.log('teardown');
 54     done();
 55   });
 56 
 57 });
 58 
 59 suite('Test log in', function() {
 60   setup(function(done) {
 61     console.log('setup');
 62 
 63     done();
 64   });
 65   test('login', function(done) {
 66     user1.post('http://localhost:3000/api/login').send({
 67       email: '1@1.com',
 68       password: '1'
 69     }).end(function(res) {
 70       assert.equal(res.status, 200);
 71       done();
 72     });
 73 
 74   });
 75   test('check /api/profile', function(done) {
 76     user1.get('http://localhost:' + app.get('port') + '/api/profile').end(functio\
 77 n(res) {
 78       assert.equal(res.status, 200);
 79       // console.log(res.text.length);
 80       done();
 81     });
 82     // done();
 83   });
 84   test('check /api/users', function(done) {
 85     user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(\
 86 res) {
 87       assert.equal(res.status, 200);
 88       // console.log(res.text);
 89       done();
 90     });
 91     // done();
 92   });
 93   test('check /api/posts', function(done) {
 94     user1.get('http://localhost:' + app.get('port') + '/api/posts').end(function(\
 95 res) {
 96       assert.equal(res.status, 200);
 97       // console.log(res.text.length);
 98       done();
 99     });
100     // done();
101   });
102 
103   teardown(function(done) {
104     console.log('teardown');
105     done();
106   });
107 
108 });
109 suite('User control', function() {
110   var user2 = {
111     firstName: 'Bob',
112     lastName: 'Dilan',
113     displayName: 'Bob Dilan',
114     email: '2@2.com'
115   };
116   suiteSetup(function(done) {
117     user1.post('http://localhost:3000/api/login').send({
118       email: '1@1.com',
119       password: '1'
120     }).end(function(res) {
121       assert.equal(res.status, 200);
122       // done();
123     });
124     user1.get('http://localhost:' + app.get('port') + '/api/profile').end(functio\
125 n(res) {
126       assert.equal(res.status, 200);
127       // console.log(res.text.length);
128       // done();
129     });
130 
131     done();
132   })
133 
134   test('new user POST /api/users', function(done) {
135     user1.post(port + '/api/users')
136       .send(user2)
137       .end(function(res) {
138         assert.equal(res.status, 200);
139         // console.log(res.text.length);
140         user2 = res.body;
141         // console.log(user2)
142         done();
143       })
144   });
145   test('get user list and check for new user GET /api/users', function(done) {
146     user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(\
147 res) {
148       assert.equal(res.status, 200);
149       // console.log(res.body)
150       var user3 = res.body.filter(function(el, i, list) {
151         return (el._id == user2._id);
152       });
153       assert(user3.length === 1);
154       // assert(res.body.indexOf(user2)>-1);
155       // console.log(res.body.length)
156       done();
157     })
158   });
159   test('Approve User: PUT /api/users/' + user2._id, function(done) {
160     assert(user2._id != '');
161     user1.put(port + '/api/users/' + user2._id)
162       .send({
163         approved: true
164       })
165       .end(function(res) {
166         assert.equal(res.status, 200);
167         // console.log(res.text.length);
168         assert(res.body.approved);
169         user1.get(port + '/api/users/' + user2._id).end(function(res) {
170           assert(res.status, 200);
171           assert(res.body.approved);
172           done();
173         })
174 
175       })
176   });
177   test('Banned User: PUT /api/users/' + user2._id, function(done) {
178     assert(user2._id != '');
179     user1.put(port + '/api/users/' + user2._id)
180       .send({
181         banned: true
182       })
183       .end(function(res) {
184         assert.equal(res.status, 200);
185         // console.log(res.text.length);
186         assert(res.body.banned);
187         user1.get(port + '/api/users/' + user2._id).end(function(res) {
188           assert(res.status, 200);
189           assert(res.body.banned);
190           done();
191         })
192 
193       })
194   });
195   test('Promote User: PUT /api/users/' + user2._id, function(done) {
196     assert(user2._id != '');
197     user1.put(port + '/api/users/' + user2._id)
198       .send({
199         admin: true
200       })
201       .end(function(res) {
202         assert.equal(res.status, 200);
203         // console.log(res.text.length);
204         assert(res.body.admin);
205         user1.get(port + '/api/users/' + user2._id).end(function(res) {
206           assert(res.status, 200);
207           assert(res.body.admin);
208           done();
209         })
210 
211       })
212   });
213   test('Delete User: DELETE /api/users/:id', function(done) {
214     assert(user2._id != '');
215     user1.del(port + '/api/users/' + user2._id)
216       .end(function(res) {
217         assert.equal(res.status, 200);
218         // console.log('id:' + user2._id)
219         user1.get(port + '/api/users').end(function(res) {
220           assert.equal(res.status, 200);
221           var user3 = res.body.filter(function(el, i, list) {
222             return (el._id === user2._id);
223           });
224           // console.log('***');
225           // console.warn(user3);
226           assert(user3.length === 0);
227           done();
228         });
229       });
230 
231 
232   });
233 });
234 // app.close();   
235 // console.log(app)

Warning

Please don’t store plain passwords/keys in the database. Any serious production app should at least salt the passwords before storing them.

4.8 Conclusion

HackHall is still in development, but there are important real production application components, such as REST API architecture, OAuth, Mongoose and its models, MVC structure of Express.js apps, access to environment variables, etc.