Express.js 和 Mongoose 示例:構建 HackHall
注意: 本文是 Express.js 指南的一部分:Express.js 綜合書籍。
HackHall 項目的前端應用使用 Backbone.js 和 Underscore 編寫,後端 REST API 服務器使用 Express.js、MongoDB 通過 Mongoose 編寫。
示例 :HackHall 源代碼在公共 GitHub 存儲庫中。
使用 AngelList 或預先填寫的電子郵件 ([email protected]) 和密碼 (1) 可以在 hackhall.com 訪問現場演示。
什麼是 HackHall
HackHall (ex-Accelerator.IO) 是一個開源的僅限邀請的社交網絡和協作工具,適用於黑客、潮人、企業家和海盜(開個玩笑)。 HackHall 類似於 Reddit,再加上 Hacker News,以及帶有策展功能的 Facebook 群組。
HackHall 項目處於早期階段,大致處於測試階段。我們計劃在未來擴展代碼庫,讓更多的人分享編程的技能、智慧和熱情。
在本章中,我們將介紹 1.0 版本,它具有:
- 帶有 oauth 模塊和 AngelList API 的 OAuth 1.0
- 電子郵件和密碼驗證
- Mongoose 模型和架構
- Express.js 結構與模塊中的路由
- JSON REST API
- Express.js 錯誤處理
- 前端客戶端 Backbone.js 應用(有關 Backbone.js 的更多信息,請在線下載/閱讀我們的 JS 快速原型製作教程)
- 使用 Foreman 的
.env
的環境變量 - TDD 與 Mocha
- 基本 Makefile 設置
運行 HackHall
要獲取源代碼,您可以導航到 hackhall
文件夾或從 GitHub 克隆:
$ git clone [email protected]:azat-co/hackhall
$ git checkout 1.0
$ npm install
如果您計劃測試 AngelList(可選),HackHall 使用 Heroku 和 Foreman 設置 AngelList API 密鑰,將它們存儲在環境變量中,因此我們需要添加 .evn
像這樣的文件(以下是假值):
ANGELLIST_CLIENT_ID=254C0335-5F9A-4607-87C0
ANGELLIST_CLIENT_SECRET=99F5C1AC-C5F7-44E6-81A1-8DF4FC42B8D9
有人創建並註冊他/她的 AngelList 應用程序後,可以在 angel.co/api 上獲得密鑰。
如果您還沒有 MongoDB,請下載並安裝它。數據庫和第三方庫超出了本書的範圍。但是,您可以在網上和使用 JS 的快速原型中找到足夠的材料。
要啟動 MongoDB 服務器,請打開一個新的終端窗口並運行:
$ mongod
回到項目文件夾運行:
$ foreman start
在 MongoDB 使用默認端口 27017 在 localhost 上運行後,可以為數據庫 hackhall
播種 通過運行 seed.js
使用默認管理員用戶 mongo腳本:
[旁注]
閱讀博客文章很好,但觀看視頻課程更好,因為它們更具吸引力。
許多開發人員抱怨 Node.js 上缺乏負擔得起的高質量視頻材料。觀看 YouTube 視頻會讓人分心,花 500 美元購買 Node 視頻課程很瘋狂!
去看看 Node University,它有關於 Node 的免費視頻課程:node.university。
[旁注結束]
$ mongo localhost:27017/hackhall seed.js
隨意修改seed.js
隨心所欲(注意它會刪除所有以前的數據!):
db.dropDatabase();
var seedUser ={
firstName:'Azat',
lastName:"Mardanov",
displayName:"Azat Mardanov",
password:'1',
email:'[email protected]',
role:'admin',
approved: true,
admin: true
};
db.users.save(seedUser);
如果您在 http://localhost:5000 打開瀏覽器,您應該會看到登錄屏幕。
輸入用戶名和密碼進入(seed.js
文件)。
認證成功後,用戶被重定向到帖子頁面:
他們可以在哪裡創建帖子(例如,一個問題):
保存帖子:
喜歡帖子:
訪問其他用戶的個人資料:
如果他們有管理員權限,用戶可以批准申請人:
並在個人資料頁面上管理他們的帳戶:
結構
以下是每個文件夾和文件的內容:
/api
:應用共享路線/models
:貓鼬模型/public
:主幹應用,前端 JavaScript、CSS、HTML 等靜態文件/routes
:REST API 路由/tests
:摩卡測試.gitignore
:git 應該忽略的文件列表Makefile
:製作文件來運行測試Procfile
:Heroku 部署所需的 Cedar 堆棧文件package.json
:NPM 依賴和 HackHall 元數據readme.md
:描述server.js
:主 HackHall 服務器文件
Express.js 應用程序
讓我們直接跳到 server.js
文件並了解它是如何實現的。首先,我們聲明依賴:
var express = require('express'),
routes = require('./routes'),
http = require('http'),
util = require('util'),
oauth = require('oauth'),
querystring = require('querystring');
然後,我們初始化應用並配置中間件。 process.env.PORT
由 Heroku 填充,如果本地設置回退到 3000。
var app = express();
app.configure(function(){
app.set('port', process.env.PORT || 3000 );
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
身份驗證需要傳遞給 cookieParser 和會話中間件的值。顯然,會話秘密應該是私有的:
app.use(express.cookieParser('asd;lfkajs;ldfkj'));
app.use(express.session({
secret: '<h1>WHEEYEEE</h1>',
key: 'sid',
cookie: {
secret: true,
expires: false
}
}));
這就是我們為前端客戶端 Backbone.js 應用程序和 CSS 等其他靜態文件提供服務的方式:
app.use(express.static(__dirname + '/public'));
app.use(app.router);
});
clientErrorHandler
將錯誤處理分解為三個函數 專用於來自 Backbone.js 應用程序的 AJAX/XHR 請求(響應 JSON):
app.configure(function() {
app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);
});
function logErrors(err, req, res, next) {
console.error('logErrors', err.toString());
next(err);
}
function clientErrorHandler(err, req, res, next) {
console.error('clientErrors ', err.toString());
res.send(500, { error: err.toString()});
if (req.xhr) {
console.error(err);
res.send(500, { error: err.toString()});
} else {
next(err);
}
}
function errorHandler(err, req, res, next) {
console.error('lastErrors ', err.toString());
res.send(500, {error: err.toString()});
}
與我們確定process.env.PORT
的方式相同 並回退到本地設置值 3000,我們對 MongoDB 連接字符串做類似的事情:
var dbUrl = process.env.MONGOHQ_URL
|| 'mongodb://@127.0.0.1:27017/hackhall';
var mongoose = require('mongoose');
var connection = mongoose.createConnection(dbUrl);
connection.on('error', console.error.bind(console,
'connection error:'));
有時登錄連接打開事件是個好主意:
connection.once('open', function () {
console.info('connected to database')
});
Mongoose 模型位於 models
文件夾:
var models = require('./models');
這個中間件將提供對我們路由方法中的兩個集合的訪問:
function db (req, res, next) {
req.db = {
User: connection.model('User', models.User, 'users'),
Post: connection.model('Post', models.Post, 'posts')
};
return next();
}
只是導入的身份驗證函數的新名稱:
checkUser = routes.main.checkUser;
checkAdmin = routes.main.checkAdmin;
checkApplicant = routes.main.checkApplicant;
AngelList OAuth 路由:
app.get('/auth/angellist', routes.auth.angelList);
app.get('/auth/angellist/callback',
routes.auth.angelListCallback,
routes.auth.angelListLogin,
db,
routes.users.findOrAddUser);
主要應用路徑包括api/profile
如果用戶已登錄,則返回用戶會話:
//MAIN
app.get('/api/profile', checkUser, db, routes.main.profile);
app.del('/api/profile', checkUser, db, routes.main.delProfile);
app.post('/api/login', db, routes.main.login);
app.post('/api/logout', routes.main.logout);
創建用戶和帖子的 POST 請求:
//POSTS
app.get('/api/posts', checkUser, db, routes.posts.getPosts);
app.post('/api/posts', checkUser, db, routes.posts.add);
app.get('/api/posts/:id', checkUser, db, routes.posts.getPost);
app.put('/api/posts/:id', checkUser, db, routes.posts.updatePost);
app.del('/api/posts/:id', checkUser, db, routes.posts.del);
//USERS
app.get('/api/users', checkUser, db, routes.users.getUsers);
app.get('/api/users/:id', checkUser, db,routes.users.getUser);
app.post('/api/users', checkAdmin, db, routes.users.add);
app.put('/api/users/:id', checkAdmin, db, routes.users.update);
app.del('/api/users/:id', checkAdmin, db, routes.users.del);
這些路線適用於尚未獲得批准的新成員:
//APPLICATION
app.post('/api/application',
checkAdmin,
db,
routes.application.add);
app.put('/api/application',
checkApplicant,
db,
routes.application.update);
app.get('/api/application',
checkApplicant,
db,
routes.application.get);
抓住所有其他路線:
app.get('*', function(req, res){
res.send(404);
});
require.main === module
判斷這個文件是作為獨立模塊執行還是作為導入模塊執行是一個聰明的技巧:
http.createServer(app);
if (require.main === module) {
app.listen(app.get('port'), function(){
console.info('Express server listening on port '
+ app.get('port'));
});
}
else {
console.info('Running app as a module')
exports.app = app;
}
hackhall/server.js
的完整源代碼 :
var express = require('express'),
routes = require('./routes'),
http = require('http'),
util = require('util'),
oauth = require('oauth'),
querystring = require('querystring');
var app = express();
app.configure(function(){
app.set('port', process.env.PORT || 3000 );
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser('asd;lfkajs;ldfkj'));
app.use(express.session({
secret: '<h1>WHEEYEEE</h1>',
key: 'sid',
cookie: {
secret: true,
expires: false
}
}));
// app.use(express.csrf());
// app.use(function(req, res, next) {
// res.locals.csrf = req.session._cstf;
// return next();
// });
app.use(express.static(__dirname + '/public'));
app.use(app.router);
});
app.configure(function() {
app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);
});
function logErrors(err, req, res, next) {
console.error('logErrors', err.toString());
next(err);
}
function clientErrorHandler(err, req, res, next) {
console.error('clientErrors ', err.toString());
res.send(500, { error: err.toString()});
if (req.xhr) {
console.error(err);
res.send(500, { error: err.toString()});
} else {
next(err);
}
}
function errorHandler(err, req, res, next) {
console.error('lastErrors ', err.toString());
res.send(500, {error: err.toString()});
}
var dbUrl = process.env.MONGOHQ_URL || 'mongodb://@127.0.0.1:27017/hackhall';
var mongoose = require('mongoose');
var connection = mongoose.createConnection(dbUrl);
connection.on('error', console.error.bind(console, 'connection error:'));
connection.once('open', function () {
console.info('connected to database')
});
var models = require('./models');
function db (req, res, next) {
req.db = {
User: connection.model('User', models.User, 'users'),
Post: connection.model('Post', models.Post, 'posts')
};
return next();
}
checkUser = routes.main.checkUser;
checkAdmin = routes.main.checkAdmin;
checkApplicant = routes.main.checkApplicant;
app.get('/auth/angellist', routes.auth.angelList);
app.get('/auth/angellist/callback',
routes.auth.angelListCallback,
routes.auth.angelListLogin,
db,
routes.users.findOrAddUser);
//MAIN
app.get('/api/profile', checkUser, db, routes.main.profile);
app.del('/api/profile', checkUser, db, routes.main.delProfile);
app.post('/api/login', db, routes.main.login);
app.post('/api/logout', routes.main.logout);
//POSTS
app.get('/api/posts', checkUser, db, routes.posts.getPosts);
app.post('/api/posts', checkUser, db, routes.posts.add);
app.get('/api/posts/:id', checkUser, db, routes.posts.getPost);
app.put('/api/posts/:id', checkUser, db, routes.posts.updatePost);
app.del('/api/posts/:id', checkUser, db, routes.posts.del);
//USERS
app.get('/api/users', checkUser, db, routes.users.getUsers);
app.get('/api/users/:id', checkUser, db,routes.users.getUser);
app.post('/api/users', checkAdmin, db, routes.users.add);
app.put('/api/users/:id', checkAdmin, db, routes.users.update);
app.del('/api/users/:id', checkAdmin, db, routes.users.del);
//APPLICATION
app.post('/api/application', checkAdmin, db, routes.application.add);
app.put('/api/application', checkApplicant, db, routes.application.update);
app.get('/api/application', checkApplicant, db, routes.application.get);
app.get('*', function(req, res){
res.send(404);
});
http.createServer(app);
if (require.main === module) {
app.listen(app.get('port'), function(){
console.info('Express server listening on port ' + app.get('port'));
});
}
else {
console.info('Running app as a module')
exports.app = app;
}
路線
HackHall 路由位於 hackhall/routes
文件夾並被摸索到幾個模塊中:
hackhall/routes/index.js
:server.js
之間的橋樑 和文件夾中的其他路由hackhall/routes/auth.js
:使用 AngelList API 處理 OAuth 的路由hackhall/routes/main.js
:登錄、註銷等路由hackhall/routes/users.js
:與用戶 REST API 相關的路由hackhall/routes/application.js
:提交申請成為用戶hackhall/routes/posts.js
:與帖子 REST API 相關的路由
index.js
讓我們進入 hackhall/routes/index.js
我們包括其他模塊的地方:
exports.posts = require('./posts');
exports.main = require('./main');
exports.users = require('./users');
exports.application = require('./application');
exports.auth = require('./auth');
auth.js
在這個模塊中,我們將處理 OAuth dance 使用 AngelList API。為此,我們必須依賴 https
圖書館:
var https = require('https');
AngelList API 客戶端 ID 和客戶端密碼在 angel.co/api 網站獲取並存儲在環境變量中:
var angelListClientId = process.env.ANGELLIST_CLIENT_ID;
var angelListClientSecret = process.env.ANGELLIST_CLIENT_SECRET;
該方法會將用戶重定向到angel.co網站進行認證:
exports.angelList = function(req, res) {
res.redirect('https://angel.co/api/oauth/authorize?client_id=' + angelListClientId + '&scope=email&response_type=code');
}
用戶允許我們的應用訪問他們的信息後,AngelList 會將他們發送回此路由,以便我們發出新的(HTTPS)請求以檢索令牌:
exports.angelListCallback = function(req, res, next) {
var token;
var buf = '';
var data;
// console.log('/api/oauth/token?client_id='
//+ angelListClientId
//+ '&client_secret='
//+ angelListClientSecret
//+ '&code='
//+ req.query.code
//+ '&grant_type=authorization_code');
var angelReq = https.request({
host: 'angel.co',
path: '/api/oauth/token?client_id='
+ angelListClientId
+ '&client_secret='
+ angelListClientSecret
+ '&code='
+ req.query.code
+ '&grant_type=authorization_code',
port: 443,
method: 'POST',
headers: {
'content-length': 0
}
},
function(angelRes) {
angelRes.on('data', function(buffer) {
buf += buffer;
});
angelRes.on('end', function() {
try {
data = JSON.parse(buf.toString('utf-8'));
} catch (e) {
if (e) return res.send(e);
}
if (!data || !data.access_token) return res.send(500);
token = data.access_token;
req.session.angelListAccessToken = token;
if (token) next();
else res.send(500);
});
});
angelReq.end();
angelReq.on('error', function(e) {
console.error(e);
next(e);
});
}
直接用之前中間件的token調用AngleList API獲取用戶信息:
exports.angelListLogin = function(req, res, next) {
token = req.session.angelListAccessToken;
httpsRequest = https.request({
host: 'api.angel.co',
path: '/1/me?access_token=' + token,
port: 443,
method: 'GET'
},
function(httpsResponse) {
httpsResponse.on('data', function(buffer) {
data = JSON.parse(buffer.toString('utf-8'));
if (data) {
req.angelProfile = data;
next();
}
});
}
);
httpsRequest.end();
httpsRequest.on('error', function(e) {
console.error(e);
});
};
hackhall/routes/auth.js
的完整源代碼 文件:
var https = require('https');
var angelListClientId = process.env.ANGELLIST_CLIENT_ID;
var angelListClientSecret = process.env.ANGELLIST_CLIENT_SECRET;
exports.angelList = function(req, res) {
res.redirect('https://angel.co/api/oauth/authorize?client_id=' + angelListClientId + '&scope=email&response_type=code');
}
exports.angelListCallback = function(req, res, next) {
var token;
var buf = '';
var data;
// console.log('/api/oauth/token?client_id=' + angelListClientId + '&client_secret=' + angelListClientSecret + '&code=' + req.query.code + '&grant_type=authorization_code');
var angelReq = https.request({
host: 'angel.co',
path: '/api/oauth/token?client_id=' + angelListClientId + '&client_secret=' + angelListClientSecret + '&code=' + req.query.code + '&grant_type=authorization_code',
port: 443,
method: 'POST',
headers: {
'content-length': 0
}
},
function(angelRes) {
angelRes.on('data', function(buffer) {
buf += buffer;
});
angelRes.on('end', function() {
try {
data = JSON.parse(buf.toString('utf-8'));
} catch (e) {
if (e) return res.send(e);
}
if (!data || !data.access_token) return res.send(500);
token = data.access_token;
req.session.angelListAccessToken = token;
if (token) next();
else res.send(500);
});
});
angelReq.end();
angelReq.on('error', function(e) {
console.error(e);
next(e);
});
}
exports.angelListLogin = function(req, res, next) {
token = req.session.angelListAccessToken;
httpsRequest = https.request({
host: 'api.angel.co',
path: '/1/me?access_token=' + token,
port: 443,
method: 'GET'
},
function(httpsResponse) {
httpsResponse.on('data', function(buffer) {
data = JSON.parse(buffer.toString('utf-8'));
if (data) {
req.angelProfile = data;
next();
}
});
}
);
httpsRequest.end();
httpsRequest.on('error', function(e) {
console.error(e);
});
};
main.js
hackhall/routes/main.js
文件可能也很有趣。
checkAdmin()
函數執行管理員權限的身份驗證。如果會話對像沒有攜帶正確的標誌,我們調用 Express.js next()
帶有錯誤對象的函數:
exports.checkAdmin = function(request, response, next) {
if (request.session
&& request.session.auth
&& request.session.userId
&& request.session.admin) {
console.info('Access ADMIN: ' + request.session.userId);
return next();
} else {
next('User is not an administrator.');
}
};
同樣,我們可以只檢查用戶而不檢查管理員權限:
exports.checkUser = function(req, res, next) {
if (req.session && req.session.auth && req.session.userId
&& (req.session.user.approved || req.session.admin)) {
console.info('Access USER: ' + req.session.userId);
return next();
} else {
next('User is not logged in.');
}
};
應用程序只是一個未經批准的用戶對象,我們也可以檢查一下:
exports.checkApplicant = function(req, res, next) {
if (req.session && req.session.auth && req.session.userId
&& (!req.session.user.approved || req.session.admin)) {
console.info('Access USER: ' + req.session.userId);
return next();
} else {
next('User is not logged in.');
}
};
在登錄功能中,我們在數據庫中搜索電子郵件和密碼匹配項。成功後,我們將用戶對象存儲在會話中並繼續;否則請求失敗:
exports.login = function(req, res, next) {
req.db.User.findOne({
email: req.body.email,
password: req.body.password
},
null, {
safe: true
},
function(err, user) {
if (err) return next(err);
if (user) {
req.session.auth = true;
req.session.userId = user._id.toHexString();
req.session.user = user;
if (user.admin) {
req.session.admin = true;
}
console.info('Login USER: ' + req.session.userId);
res.json(200, {
msg: 'Authorized'
});
} else {
next(new Error('User is not found.'));
}
});
};
註銷過程會刪除所有會話信息:
exports.logout = function(req, res) {
console.info('Logout USER: ' + req.session.userId);
req.session.destroy(function(error) {
if (!error) {
res.send({
msg: 'Logged out'
});
}
});
};
此路由用於 Profile 頁面以及 Backbone.js 用於用戶身份驗證:
exports.profile = function(req, res, next) {
req.db.User.findById(req.session.userId, 'firstName lastName'
+ 'displayName headline photoUrl admin'
+ 'approved banned role angelUrl twitterUrl'
+ 'facebookUrl linkedinUrl githubUrl', function(err, obj) {
if (err) next(err);
if (!obj) next(new Error('User is not found'));
req.db.Post.find({
author: {
id: obj._id,
name: obj.displayName
}
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.own = list || [];
req.db.Post.find({
likes: obj._id
}, null, {
sort: {
'created': -1
}
此邏輯查找用戶發表的帖子和評論:
}, function(err, list) {
if (err) next(err);
obj.posts.likes = list || [];
req.db.Post.find({
watches: obj._id
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.watches = list || [];
req.db.Post.find({
'comments.author.id': obj._id
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.comments = [];
list.forEach(function(value, key, list) {
obj.posts.comments.push(
value.comments.filter(
function(el, i, arr) {
return (el.author.id.toString() == obj._id.toString());
}
)
);
});
res.json(200, obj);
});
});
});
});
});
};
允許用戶刪除他們的個人資料很重要:
exports.delProfile = function(req, res, next) {
console.log('del profile');
console.log(req.session.userId);
req.db.User.findByIdAndRemove(req.session.user._id, {},
function(err, obj) {
if (err) next(err);
req.session.destroy(function(error) {
if (err) {
next(err)
}
});
res.json(200, obj);
}
);
};
hackhall/routes/main.js
的完整源代碼 文件:
exports.checkAdmin = function(request, response, next) {
if (request.session && request.session.auth && request.session.userId && request.session.admin) {
console.info('Access ADMIN: ' + request.session.userId);
return next();
} else {
next('User is not an administrator.');
}
};
exports.checkUser = function(req, res, next) {
if (req.session && req.session.auth && req.session.userId && (req.session.user.approved || req.session.admin)) {
console.info('Access USER: ' + req.session.userId);
return next();
} else {
next('User is not logged in.');
}
};
exports.checkApplicant = function(req, res, next) {
if (req.session && req.session.auth && req.session.userId && (!req.session.user.approved || req.session.admin)) {
console.info('Access USER: ' + req.session.userId);
return next();
} else {
next('User is not logged in.');
}
};
exports.login = function(req, res, next) {
req.db.User.findOne({
email: req.body.email,
password: req.body.password
},
null, {
safe: true
},
function(err, user) {
if (err) return next(err);
if (user) {
req.session.auth = true;
req.session.userId = user._id.toHexString();
req.session.user = user;
if (user.admin) {
req.session.admin = true;
}
console.info('Login USER: ' + req.session.userId);
res.json(200, {
msg: 'Authorized'
});
} else {
next(new Error('User is not found.'));
}
});
};
exports.logout = function(req, res) {
console.info('Logout USER: ' + req.session.userId);
req.session.destroy(function(error) {
if (!error) {
res.send({
msg: 'Logged out'
});
}
});
};
exports.profile = function(req, res, next) {
req.db.User.findById(req.session.userId, 'firstName lastName displayName headline photoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl', function(err, obj) {
if (err) next(err);
if (!obj) next(new Error('User is not found'));
req.db.Post.find({
author: {
id: obj._id,
name: obj.displayName
}
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.own = list || [];
req.db.Post.find({
likes: obj._id
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.likes = list || [];
req.db.Post.find({
watches: obj._id
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.watches = list || [];
req.db.Post.find({
'comments.author.id': obj._id
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.comments = [];
list.forEach(function(value, key, list) {
obj.posts.comments.push(value.comments.filter(function(el, i, arr) {
return (el.author.id.toString() == obj._id.toString());
}));
});
res.json(200, obj);
});
});
});
});
});
};
exports.delProfile = function(req, res, next) {
console.log('del profile');
console.log(req.session.userId);
req.db.User.findByIdAndRemove(req.session.user._id, {}, function(err, obj) {
if (err) next(err);
req.session.destroy(function(error) {
if (err) {
next(err)
}
});
res.json(200, obj);
});
};
users.js
hackhall/routes/users.js
的完整源代碼 文件:
objectId = require('mongodb').ObjectID;
exports.getUsers = function(req, res, next) {
if (req.session.auth && req.session.userId) {
req.db.User.find({}, 'firstName lastName displayName headline photoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl', function(err, list) {
if (err) next(err);
res.json(200, list);
});
} else {
next('User is not recognized.')
}
}
exports.getUser = function(req, res, next) {
req.db.User.findById(req.params.id, 'firstName lastName displayName headline photoUrl admin approved banned role angelUrl twitterUrl facebookUrl linkedinUrl githubUrl', function(err, obj) {
if (err) next(err);
if (!obj) next(new Error('User is not found'));
req.db.Post.find({
author: {
id: obj._id,
name: obj.displayName
}
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.own = list || [];
req.db.Post.find({
likes: obj._id
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.likes = list || [];
req.db.Post.find({
watches: obj._id
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.watches = list || [];
req.db.Post.find({
'comments.author.id': obj._id
}, null, {
sort: {
'created': -1
}
}, function(err, list) {
if (err) next(err);
obj.posts.comments = [];
list.forEach(function(value, key, list) {
obj.posts.comments.push(value.comments.filter(function(el, i, arr) {
return (el.author.id.toString() == obj._id.toString());
}));
});
res.json(200, obj);
});
});
});
});
});
};
exports.add = function(req, res, next) {
var user = new req.db.User(req.body);
user.save(function(err) {
if (err) next(err);
res.json(user);
});
};
exports.update = function(req, res, next) {
var obj = req.body;
obj.updated = new Date();
delete obj._id;
req.db.User.findByIdAndUpdate(req.params.id, {
$set: obj
}, {
new: true
}, function(err, obj) {
if (err) next(err);
res.json(200, obj);
});
};
exports.del = function(req, res, next) {
req.db.User.findByIdAndRemove(req.params.id, function(err, obj) {
if (err) next(err);
res.json(200, obj);
});
};
exports.findOrAddUser = function(req, res, next) {
data = req.angelProfile;
req.db.User.findOne({
angelListId: data.id
}, function(err, obj) {
console.log('angelListLogin4');
if (err) next(err);
console.warn(obj);
if (!obj) {
req.db.User.create({
angelListId: data.id,
angelToken: token,
angelListProfile: data,
email: data.email,
firstName: data.name.split(' ')[0],
lastName: data.name.split(' ')[1],
displayName: data.name,
headline: data.bio,
photoUrl: data.image,
angelUrl: data.angellist_url,
twitterUrl: data.twitter_url,
facebookUrl: data.facebook_url,
linkedinUrl: data.linkedin_url,
githubUrl: data.github_url
}, function(err, obj) { //remember the scope of variables!
if (err) next(err);
console.log(obj);
req.session.auth = true;
req.session.userId = obj._id;
req.session.user = obj;
req.session.admin = false; //assing regular user role by default
res.redirect('/#application');
// }
});
} else { //user is in the database
req.session.auth = true;
req.session.userId = obj._id;
req.session.user = obj;
req.session.admin = obj.admin; //false; //assing regular user role by default
if (obj.approved) {
res.redirect('/#posts');
} else {
res.redirect('/#application');
}
}
})
}
applications.js
在當前版本中,提交和批准申請不會觸發電子郵件通知。因此,用戶必須返回網站查看他們的狀態。
只需在數據庫中添加一個用戶對象(默認為approved=false):
exports.add = function(req, res, next) {
req.db.User.create({
firstName: req.body.firstName,
lastName: req.body.lastName,
displayName: req.body.displayName,
headline: req.body.headline,
photoUrl: req.body.photoUrl,
password: req.body.password,
email: req.body.email,
angelList: {
blah: 'blah'
},
angelUrl: req.body.angelUrl,
twitterUrl: req.body.twitterUrl,
facebookUrl: req.body.facebookUrl,
linkedinUrl: req.body.linkedinUrl,
githubUrl: req.body.githubUrl
}, function(err, obj) {
if (err) next(err);
if (!obj) next('Cannot create.')
res.json(200, obj);
})
};
讓用戶在他們的應用程序中更新信息:
exports.update = function(req, res, next) {
var data = {};
Object.keys(req.body).forEach(function(k) {
if (req.body[k]) {
data[k] = req.body[k];
}
});
delete data._id;
req.db.User.findByIdAndUpdate(req.session.user._id, {
$set: data
}, function(err, obj) {
if (err) next(err);
if (!obj) next('Cannot save.')
res.json(200, obj);
});
};
使用 get()
選擇特定對象 功能:
exports.get = function(req, res, next) {
req.db.User.findById(req.session.user._id,
'firstName lastName photoUrl headline displayName'
+ 'angelUrl facebookUrl twitterUrl linkedinUrl'
+ 'githubUrl', {}, function(err, obj) {
if (err) next(err);
if (!obj) next('cannot find');
res.json(200, obj);
})
};
hackhall/routes/applications.js
的完整源代碼 文件:
***Error:** File "applications.js" does not exist at this path*
posts.js
我們分割的最後一個路由模塊是 hackhall/routes/posts.js
.它負責添加、編輯和刪除帖子,以及評論、觀看和喜歡。
我們使用對象 ID 從 HEX 字符串轉換為正確的對象:
objectId = require('mongodb').ObjectID;
著色很適合記錄,但當然是可選的。我們通過轉義序列來完成它:
var red, blue, reset;
red = '\u001b[31m';
blue = '\u001b[34m';
reset = '\u001b[0m';
console.log(red + 'This is red' + reset + ' while ' + blue + ' this is blue' + reset);
帖子分頁的默認值:
var LIMIT = 10;
var SKIP = 0;
add()
函數處理新帖子的創建:
exports.add = function(req, res, next) {
if (req.body) {
req.db.Post.create({
title: req.body.title,
text: req.body.text || null,
url: req.body.url || null,
author: {
id: req.session.user._id,
name: req.session.user.displayName
}
}, function(err, docs) {
if (err) {
console.error(err);
next(err);
} else {
res.json(200, docs);
}
});
} else {
next(new Error('No data'));
}
};
檢索帖子列表:
exports.getPosts = function(req, res, next) {
var limit = req.query.limit || LIMIT;
var skip = req.query.skip || SKIP;
req.db.Post.find({}, null, {
limit: limit,
skip: skip,
sort: {
'_id': -1
}
}, function(err, obj) {
if (!obj) next('There are not posts.');
obj.forEach(function(item, i, list) {
if (req.session.user.admin) {
item.admin = true;
} else {
item.admin = false;
}
if (item.author.id == req.session.userId) {
item.own = true;
} else {
item.own = false;
}
if (item.likes
&& item.likes.indexOf(req.session.userId) > -1) {
item.like = true;
} else {
item.like = false;
}
if (item.watches
&& item.watches.indexOf(req.session.userId) > -1) {
item.watch = true;
} else {
item.watch = false;
}
});
var body = {};
body.limit = limit;
body.skip = skip;
body.posts = obj;
req.db.Post.count({}, function(err, total) {
if (err) next(err);
body.total = total;
res.json(200, body);
});
});
};
對於單個帖子頁面,我們需要 getPost()
方法:
exports.getPost = function(req, res, next) {
if (req.params.id) {
req.db.Post.findById(req.params.id, {
title: true,
text: true,
url: true,
author: true,
comments: true,
watches: true,
likes: true
}, function(err, obj) {
if (err) next(err);
if (!obj) {
next('Nothing is found.');
} else {
res.json(200, obj);
}
});
} else {
next('No post id');
}
};
del()
函數從數據庫中刪除特定的帖子。 findById()
和 remove()
此代碼段中使用了來自 Mongoose 的方法。然而,同樣的事情可以用 remove()
來完成 .
exports.del = function(req, res, next) {
req.db.Post.findById(req.params.id, function(err, obj) {
if (err) next(err);
if (req.session.admin || req.session.userId === obj.author.id) {
obj.remove();
res.json(200, obj);
} else {
next('User is not authorized to delete post.');
}
})
};
要喜歡帖子,我們會通過添加 post.likes
來更新帖子項目 用戶ID數組:
function likePost(req, res, next) {
req.db.Post.findByIdAndUpdate(req.body._id, {
$push: {
likes: req.session.userId
}
}, {}, function(err, obj) {
if (err) {
next(err);
} else {
res.json(200, obj);
}
});
};
同樣,當用戶執行 watch 操作時,系統會在 post.watches
中添加一個新 ID 數組:
function watchPost(req, res, next) {
req.db.Post.findByIdAndUpdate(req.body._id, {
$push: {
watches: req.session.userId
}
}, {}, function(err, obj) {
if (err) next(err);
else {
res.json(200, obj);
}
});
};
updatePost()
是根據隨請求發送的操作標誌調用 like 或 watch 函數。此外,updatePost()
處理帖子和評論的更改:
exports.updatePost = function(req, res, next) {
var anyAction = false;
if (req.body._id && req.params.id) {
if (req.body && req.body.action == 'like') {
anyAction = true;
likePost(req, res);
}
if (req.body && req.body.action == 'watch') {
anyAction = true;
watchPost(req, res);
}
if (req.body && req.body.action == 'comment'
&& req.body.comment && req.params.id) {
anyAction = true;
req.db.Post.findByIdAndUpdate(req.params.id, {
$push: {
comments: {
author: {
id: req.session.userId,
name: req.session.user.displayName
},
text: req.body.comment
}
}
}, {
safe: true,
new: true
}, function(err, obj) {
if (err) throw err;
res.json(200, obj);
});
}
if (req.session.auth && req.session.userId && req.body
&& req.body.action != 'comment' &&
req.body.action != 'watch' && req.body != 'like' &&
req.params.id && (req.body.author.id == req.session.user._id
|| req.session.user.admin)) {
req.db.Post.findById(req.params.id, function(err, doc) {
if (err) next(err);
doc.title = req.body.title;
doc.text = req.body.text || null;
doc.url = req.body.url || null;
doc.save(function(e, d) {
if (e) next(e);
res.json(200, d);
});
})
} else {
if (!anyAction) next('Something went wrong.');
}
} else {
next('No post ID.');
}
};
hackhall/routes/posts.js
的完整源代碼 文件:
objectId = require('mongodb').ObjectID;
var red, blue, reset;
red = '\u001b[31m';
blue = '\u001b[34m';
reset = '\u001b[0m';
console.log(red + 'This is red' + reset + ' while ' + blue + ' this is blue' + reset);
var LIMIT = 10;
var SKIP = 0;
exports.add = function(req, res, next) {
if (req.body) {
req.db.Post.create({
title: req.body.title,
text: req.body.text || null,
url: req.body.url || null,
author: {
id: req.session.user._id,
name: req.session.user.displayName
}
}, function(err, docs) {
if (err) {
console.error(err);
next(err);
} else {
res.json(200, docs);
}
});
} else {
next(new Error('No data'));
}
};
exports.getPosts = function(req, res, next) {
var limit = req.query.limit || LIMIT;
var skip = req.query.skip || SKIP;
req.db.Post.find({}, null, {
limit: limit,
skip: skip,
sort: {
'_id': -1
}
}, function(err, obj) {
if (!obj) next('There are not posts.');
obj.forEach(function(item, i, list) {
if (req.session.user.admin) {
item.admin = true;
} else {
item.admin = false;
}
if (item.author.id == req.session.userId) {
item.own = true;
} else {
item.own = false;
}
if (item.likes && item.likes.indexOf(req.session.userId) > -1) {
item.like = true;
} else {
item.like = false;
}
if (item.watches && item.watches.indexOf(req.session.userId) > -1) {
item.watch = true;
} else {
item.watch = false;
}
});
var body = {};
body.limit = limit;
body.skip = skip;
body.posts = obj;
req.db.Post.count({}, function(err, total) {
if (err) next(err);
body.total = total;
res.json(200, body);
});
});
};
exports.getPost = function(req, res, next) {
if (req.params.id) {
req.db.Post.findById(req.params.id, {
title: true,
text: true,
url: true,
author: true,
comments: true,
watches: true,
likes: true
}, function(err, obj) {
if (err) next(err);
if (!obj) {
next('Nothing is found.');
} else {
res.json(200, obj);
}
});
} else {
next('No post id');
}
};
exports.del = function(req, res, next) {
req.db.Post.findById(req.params.id, function(err, obj) {
if (err) next(err);
if (req.session.admin || req.session.userId === obj.author.id) {
obj.remove();
res.json(200, obj);
} else {
next('User is not authorized to delete post.');
}
})
};
function likePost(req, res, next) {
req.db.Post.findByIdAndUpdate(req.body._id, {
$push: {
likes: req.session.userId
}
}, {}, function(err, obj) {
if (err) {
next(err);
} else {
res.json(200, obj);
}
});
};
function watchPost(req, res, next) {
req.db.Post.findByIdAndUpdate(req.body._id, {
$push: {
watches: req.session.userId
}
}, {}, function(err, obj) {
if (err) next(err);
else {
res.json(200, obj);
}
});
};
exports.updatePost = function(req, res, next) {
var anyAction = false;
if (req.body._id && req.params.id) {
if (req.body && req.body.action == 'like') {
anyAction = true;
likePost(req, res);
}
if (req.body && req.body.action == 'watch') {
anyAction = true;
watchPost(req, res);
}
if (req.body && req.body.action == 'comment' && req.body.comment && req.params.id) {
anyAction = true;
req.db.Post.findByIdAndUpdate(req.params.id, {
$push: {
comments: {
author: {
id: req.session.userId,
name: req.session.user.displayName
},
text: req.body.comment
}
}
}, {
safe: true,
new: true
}, function(err, obj) {
if (err) throw err;
res.json(200, obj);
});
}
if (req.session.auth && req.session.userId && req.body && req.body.action != 'comment' &&
req.body.action != 'watch' && req.body != 'like' &&
req.params.id && (req.body.author.id == req.session.user._id || req.session.user.admin)) {
req.db.Post.findById(req.params.id, function(err, doc) {
if (err) next(err);
doc.title = req.body.title;
doc.text = req.body.text || null;
doc.url = req.body.url || null;
doc.save(function(e, d) {
if (e) next(e);
res.json(200, d);
});
})
} else {
if (!anyAction) next('Something went wrong.');
}
} else {
next('No post ID.');
}
};
Mogoose 模型
理想情況下,在大型應用程序中,我們會將每個模型分解為單獨的文件。現在在 HackHall 應用程序中,我們將它們全部放在 hackhall/models/index.js
中 .
與往常一樣,我們的依賴項在頂部看起來更好:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var roles = 'user staff mentor investor founder'.split(' ');
Post 模型表示帶有點贊、評論和觀看次數的帖子。
exports.Post = new Schema ({
title: {
required: true,
type: String,
trim: true,
// match: /^([[:alpha:][:space:][:punct:]]{1,100})$/
match: /^([\w ,.!?]{1,100})$/
},
url: {
type: String,
trim: true,
max: 1000
},
text: {
type: String,
trim: true,
max: 2000
},
comments: [{
text: {
type: String,
trim: true,
max:2000
},
author: {
id: {
type: Schema.Types.ObjectId,
ref: 'User'
},
name: String
}
}],
watches: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
likes: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
author: {
id: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
name: {
type: String,
required: true
}
},
created: {
type: Date,
default: Date.now,
required: true
},
updated: {
type: Date,
default: Date.now,
required: true
},
own: Boolean,
like: Boolean,
watch: Boolean,
admin: Boolean,
action: String
});
User 模型也可以作為應用程序對象(當 approved=false
):
exports.User = new Schema({
angelListId: String,
angelListProfile: Schema.Types.Mixed,
angelToken: String,
firstName: {
type: String,
required: true,
trim: true
},
lastName: {
type: String,
required: true,
trim: true
},
displayName: {
type: String,
required: true,
trim: true
},
password: String,
email: {
type: String,
required: true,
trim: true
},
role: {
type:String,
enum: roles,
required: true,
default: roles[0]
},
approved: {
type: Boolean,
default: false
},
banned: {
type: Boolean,
default: false
},
admin: {
type: Boolean,
default: false
},
headline: String,
photoUrl: String,
angelList: Schema.Types.Mixed,
created: {
type: Date,
default: Date.now
},
updated: {
type: Date, default: Date.now
},
angelUrl: String,
twitterUrl: String,
facebookUrl: String,
linkedinUrl: String,
githubUrl: String,
own: Boolean,
posts: {
own: [Schema.Types.Mixed],
likes: [Schema.Types.Mixed],
watches: [Schema.Types.Mixed],
comments: [Schema.Types.Mixed]
}
});
hackhall/models/index.js
的完整源代碼 :
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var roles = 'user staff mentor investor founder'.split(' ');
exports.Post = new Schema ({
title: {
required: true,
type: String,
trim: true,
// match: /^([[:alpha:][:space:][:punct:]]{1,100})$/
match: /^([\w ,.!?]{1,100})$/
},
url: {
type: String,
trim: true,
max: 1000
},
text: {
type: String,
trim: true,
max: 2000
},
comments: [{
text: {
type: String,
trim: true,
max:2000
},
author: {
id: {
type: Schema.Types.ObjectId,
ref: 'User'
},
name: String
}
}],
watches: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
likes: [{
type: Schema.Types.ObjectId,
ref: 'User'
}],
author: {
id: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true
},
name: {
type: String,
required: true
}
},
created: {
type: Date,
default: Date.now,
required: true
},
updated: {
type: Date,
default: Date.now,
required: true
},
own: Boolean,
like: Boolean,
watch: Boolean,
admin: Boolean,
action: String
});
exports.User = new Schema({
angelListId: String,
angelListProfile: Schema.Types.Mixed,
angelToken: String,
firstName: {
type: String,
required: true,
trim: true
},
lastName: {
type: String,
required: true,
trim: true
},
displayName: {
type: String,
required: true,
trim: true
},
password: String,
email: {
type: String,
required: true,
trim: true
},
role: {
type:String,
enum: roles,
required: true,
default: roles[0]
},
approved: {
type: Boolean,
default: false
},
banned: {
type: Boolean,
default: false
},
admin: {
type: Boolean,
default: false
},
headline: String,
photoUrl: String,
angelList: Schema.Types.Mixed,
created: {
type: Date,
default: Date.now
},
updated: {
type: Date, default: Date.now
},
angelUrl: String,
twitterUrl: String,
facebookUrl: String,
linkedinUrl: String,
githubUrl: String,
own: Boolean,
posts: {
own: [Schema.Types.Mixed],
likes: [Schema.Types.Mixed],
watches: [Schema.Types.Mixed],
comments: [Schema.Types.Mixed]
}
});
摩卡測試
使用 REST API 服務器架構的好處之一是每個路由和整個應用程序都變得非常可測試。測試通過的保證是開發過程中的一個很好的補充——所謂的測試驅動開發方法。
為了運行測試,我們使用 Makefile:
REPORTER = list
MOCHA_OPTS = --ui tdd --ignore-leaks
test:
clear
echo Starting test *********************************************************
./node_modules/mocha/bin/mocha \
--reporter $(REPORTER) \
$(MOCHA_OPTS) \
tests/*.js
echo Ending test
test-w:
./node_modules/mocha/bin/mocha \
--reporter $(REPORTER) \
--growl \
--watch \
$(MOCHA_OPTS) \
tests/*.js
users:
mocha tests/users.js --ui tdd --reporter list --ignore-leaks
posts:
clear
echo Starting test *********************************************************
./node_modules/mocha/bin/mocha \
--reporter $(REPORTER) \
$(MOCHA_OPTS) \
tests/posts.js
echo Ending test
application:
mocha tests/application.js --ui tdd --reporter list --ignore-leaks
.PHONY: test test-w posts application
因此,我們可以從 $ make
開始測試 命令。
所有 20 項測試都應通過:
HackHall 測試在 tests
中進行 文件夾,包括:
hackhall/tests/application.js
:未經批准的用戶信息的功能測試hackhall/tests/posts.js
:帖子的功能測試hackhall/tests/users.js
:用戶功能測試
測試使用名為 superagent(GitHub) 的庫。
hackhall/tests/application.js
的完整內容 :
var app = require ('../server').app,
assert = require('assert'),
request = require('superagent');
app.listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
var user1 = request.agent();
var port = 'http://localhost:'+app.get('port');
var userId;
suite('APPLICATION API', function (){
suiteSetup(function(done){
done();
});
test('log in as admin', function(done){
user1.post(port+'/api/login').send({email:'[email protected]',password:'1'}).end(function(res){
assert.equal(res.status,200);
done();
});
});
test('get profile for admin',function(done){
user1.get(port+'/api/profile').end(function(res){
assert.equal(res.status,200);
done();
});
});
test('submit applicaton for user [email protected]', function(done){
user1.post(port+'/api/application').send({
firstName: 'Dummy',
lastName: 'Application',
displayName: 'Dummy Application',
password: '3',
email: '[email protected]',
headline: 'Dummy Appliation',
photoUrl: '/img/user.png',
angelList: {blah:'blah'},
angelUrl: 'http://angel.co.com/someuser',
twitterUrl: 'http://twitter.com/someuser',
facebookUrl: 'http://facebook.com/someuser',
linkedinUrl: 'http://linkedin.com/someuser',
githubUrl: 'http://github.com/someuser'
}).end(function(res){
assert.equal(res.status,200);
userId = res.body._id;
done();
});
});
test('logout admin',function(done){
user1.post(port+'/api/logout').end(function(res){
assert.equal(res.status,200);
done();
});
});
test('get profile again after logging out',function(done){
user1.get(port+'/api/profile').end(function(res){
assert.equal(res.status,500);
done();
});
});
test('log in as user3 - unapproved', function(done){
user1.post(port+'/api/login').send({email:'[email protected]',password:'3'}).end(function(res){
assert.equal(res.status,200);
done();
});
});
test('get user application', function(done){
user1.get(port+'/api/application/').end(function(res){
// console.log(res.body)
assert.equal(res.status, 200);
done();
});
});
test('update user application', function(done){
user1.put(port+'/api/application/').send({
firstName: 'boo'}).end(function(res){
// console.log(res.body)
assert.equal(res.status, 200);
done();
});
});
test('get user application', function(done){
user1.get(port+'/api/application/').end(function(res){
// console.log(res.body)
assert.equal(res.status, 200);
done();
});
});
test('check for posts - fail (unapproved?)', function(done){
user1.get(port+'/api/posts/').end(function(res){
// console.log(res.body)
assert.equal(res.status, 500);
done();
});
});
test('logout user',function(done){
user1.post(port+'/api/logout').end(function(res){
assert.equal(res.status,200);
done();
});
});
test('log in as admin', function(done){
user1.post(port+'/api/login').send({email:'[email protected]',password:'1'}).end(function(res){
assert.equal(res.status,200);
done();
});
});
test('delete user3', function(done){
user1.del(port+'/api/users/'+userId).end(function(res){
assert.equal(res.status, 200);
done();
});
});
test('logout admin',function(done){
user1.post(port+'/api/logout').end(function(res){
assert.equal(res.status,200);
done();
});
});
test('log in as user - should fail', function(done){
user1.post(port+'/api/login').send({email:'[email protected]',password:'3'}).end(function(res){
// console.log(res.body)
assert.equal(res.status,500);
done();
});
});
test('check for posts - must fail', function(done){
user1.get(port+'/api/posts/').end(function(res){
// console.log(res.body)
assert.equal(res.status, 500);
done();
});
});
suiteTeardown(function(done){
done();
});
});
hackhall/tests/posts.js
的完整內容 :
var app = require('../server').app,
assert = require('assert'),
request = require('superagent');
app.listen(app.get('port'), function() {
console.log('Express server listening on port ' + app.get('port'));
});
var user1 = request.agent();
var port = 'http://localhost:' + app.get('port');
var postId;
suite('POSTS API', function() {
suiteSetup(function(done) {
done();
});
test('log in', function(done) {
user1.post(port + '/api/login').send({
email: '[email protected]',
password: '1'
}).end(function(res) {
assert.equal(res.status, 200);
done();
});
});
test('add post', function(done) {
user1.post(port + '/api/posts').send({
title: 'Yo Test Title',
text: 'Yo Test text',
url: ''
}).end(function(res) {
assert.equal(res.status, 200);
postId = res.body._id;
done();
});
});
test('get profile', function(done) {
user1.get(port + '/api/profile').end(function(res) {
assert.equal(res.status, 200);
done();
});
});
test('post get', function(done) {
// console.log('000'+postId);
user1.get(port + '/api/posts/' + postId).end(function(res) {
assert.equal(res.status, 200);
assert.equal(res.body._id, postId);
done();
});
});
test('delete post', function(done) {
user1.del(port + '/api/posts/' + postId).end(function(res) {
assert.equal(res.status, 200);
done();
});
});
test('check for deleted post', function(done) {
user1.get(port + '/api/posts/' + postId).end(function(res) {
// console.log(res.body)
assert.equal(res.status, 500);
done();
});
});
suiteTeardown(function(done) {
done();
});
});
hackhall/tests/users.js
的完整內容 :
var app = require('../server').app,
assert = require('assert'),
request = require('superagent');
// http = require('support/http');
var user1 = request.agent();
var port = 'http://localhost:' + app.get('port');
app.listen(app.get('port'), function() {
console.log('Express server listening on port ' + app.get('port'));
});
suite('Test root', function() {
setup(function(done) {
console.log('setup');
done();
});
test('check /', function(done) {
request.get('http://localhost:3000').end(function(res) {
assert.equal(res.status, 200);
done();
});
});
test('check /api/profile', function(done) {
request.get('http://localhost:' + app.get('port') + '/api/profile').end(function(res) {
assert.equal(res.status, 500);
done();
});
});
test('check /api/users', function(done) {
user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(res) {
assert.equal(res.status, 500);
// console.log(res.text.length);
done();
});
// done();
});
test('check /api/posts', function(done) {
user1.get('http://localhost:' + app.get('port') + '/api/posts').end(function(res) {
assert.equal(res.status, 500);
// console.log(res.text.length);
done();
});
// done();
});
teardown(function(done) {
console.log('teardown');
done();
});
});
suite('Test log in', function() {
setup(function(done) {
console.log('setup');
done();
});
test('login', function(done) {
user1.post('http://localhost:3000/api/login').send({
email: '[email protected]',
password: '1'
}).end(function(res) {
assert.equal(res.status, 200);
done();
});
});
test('check /api/profile', function(done) {
user1.get('http://localhost:' + app.get('port') + '/api/profile').end(function(res) {
assert.equal(res.status, 200);
// console.log(res.text.length);
done();
});
// done();
});
test('check /api/users', function(done) {
user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(res) {
assert.equal(res.status, 200);
// console.log(res.text);
done();
});
// done();
});
test('check /api/posts', function(done) {
user1.get('http://localhost:' + app.get('port') + '/api/posts').end(function(res) {
assert.equal(res.status, 200);
// console.log(res.text.length);
done();
});
// done();
});
teardown(function(done) {
console.log('teardown');
done();
});
});
suite('User control', function() {
var user2 = {
firstName: 'Bob',
lastName: 'Dilan',
displayName: 'Bob Dilan',
email: '[email protected]'
};
suiteSetup(function(done) {
user1.post('http://localhost:3000/api/login').send({
email: '[email protected]',
password: '1'
}).end(function(res) {
assert.equal(res.status, 200);
// done();
});
user1.get('http://localhost:' + app.get('port') + '/api/profile').end(function(res) {
assert.equal(res.status, 200);
// console.log(res.text.length);
// done();
});
done();
})
test('new user POST /api/users', function(done) {
user1.post(port + '/api/users')
.send(user2)
.end(function(res) {
assert.equal(res.status, 200);
// console.log(res.text.length);
user2 = res.body;
// console.log(user2)
done();
})
});
test('get user list and check for new user GET /api/users', function(done) {
user1.get('http://localhost:' + app.get('port') + '/api/users').end(function(res) {
assert.equal(res.status, 200);
// console.log(res.body)
var user3 = res.body.filter(function(el, i, list) {
return (el._id == user2._id);
});
assert(user3.length === 1);
// assert(res.body.indexOf(user2)>-1);
// console.log(res.body.length)
done();
})
});
test('Approve User: PUT /api/users/' + user2._id, function(done) {
assert(user2._id != '');
user1.put(port + '/api/users/' + user2._id)
.send({
approved: true
})
.end(function(res) {
assert.equal(res.status, 200);
// console.log(res.text.length);
assert(res.body.approved);
user1.get(port + '/api/users/' + user2._id).end(function(res) {
assert(res.status, 200);
assert(res.body.approved);
done();
})
})
});
test('Banned User: PUT /api/users/' + user2._id, function(done) {
assert(user2._id != '');
user1.put(port + '/api/users/' + user2._id)
.send({
banned: true
})
.end(function(res) {
assert.equal(res.status, 200);
// console.log(res.text.length);
assert(res.body.banned);
user1.get(port + '/api/users/' + user2._id).end(function(res) {
assert(res.status, 200);
assert(res.body.banned);
done();
})
})
});
test('Promote User: PUT /api/users/' + user2._id, function(done) {
assert(user2._id != '');
user1.put(port + '/api/users/' + user2._id)
.send({
admin: true
})
.end(function(res) {
assert.equal(res.status, 200);
// console.log(res.text.length);
assert(res.body.admin);
user1.get(port + '/api/users/' + user2._id).end(function(res) {
assert(res.status, 200);
assert(res.body.admin);
done();
})
})
});
test('Delete User: DELETE /api/users/:id', function(done) {
assert(user2._id != '');
user1.del(port + '/api/users/' + user2._id)
.end(function(res) {
assert.equal(res.status, 200);
// console.log('id:' + user2._id)
user1.get(port + '/api/users').end(function(res) {
assert.equal(res.status, 200);
var user3 = res.body.filter(function(el, i, list) {
return (el._id === user2._id);
});
// console.log('***');
// console.warn(user3);
assert(user3.length === 0);
done();
});
});
});
});
// app.close();
// console.log(app)
警告: 請不要在數據庫中存儲普通密碼/密鑰。任何嚴肅的生產應用程序至少應該在存儲密碼之前加鹽。
結論
HackHall 仍在開發中,但有重要的實際生產應用程序組件,例如 REST API 架構、OAuth、Mongoose 及其模型、Express.js 應用程序的 MVC 結構、環境變量的訪問等。