4 HackHall
|
ExampleThe 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.
Enter username and password to get in (the ones from the seed.js file).

The HackHall login page with seed data.
After successful authentication, users are redirected to the Posts page:

The HackHall Posts page.
where they can create a post (e.g., a question):

The HackHall Posts page.
Save the post:

The HackHall Posts page with a saved post.
Like posts:

The HackHall Posts page with a liked post.
Visit other users’ profiles:

The HackHall People page.
If they have admin rights, users can approve applicants:

The HackHall People page with admin rights.
and manage their account on the 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.
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 betweenserver.jsand 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 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)
|
WarningPlease 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.