diff --git a/.gitignore b/.gitignore
index ab05ebd6ba..f482055c79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,9 @@ assets.json
# Ignore Windows desktop setting file
desktop.ini
+# Ignore Redis snapshot
+dump.rdb
+
*.log
.idea
@@ -24,3 +27,5 @@ public/upload/*
*.sublime-project
*.sublime-workspace
*.swp
+
+package-lock.json
diff --git a/.mention-bot b/.mention-bot
new file mode 100644
index 0000000000..86a447db50
--- /dev/null
+++ b/.mention-bot
@@ -0,0 +1,3 @@
+{
+ "userBlacklist": ["huacnlee"]
+}
diff --git a/.snyk b/.snyk
new file mode 100644
index 0000000000..2fb1107f87
--- /dev/null
+++ b/.snyk
@@ -0,0 +1,8 @@
+# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
+version: v1.12.0
+ignore: {}
+# patches apply the minimum changes required to fix a vulnerability
+patch:
+ 'npm:tunnel-agent:20170305':
+ - jpush-sdk > request > tunnel-agent:
+ patched: '2018-07-01T04:07:14.342Z'
diff --git a/.travis.yml b/.travis.yml
index 60738ca13f..78ce352cc9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,11 +1,27 @@
+sudo: false
+
language: node_js
+
+env:
+ - CXX=g++-4.8
+
node_js:
- - '0.10'
- - '0.12'
- - 'iojs'
+ - stable
+
+addons:
+ apt:
+ sources:
+ - ubuntu-toolchain-r-test
+ packages:
+ - g++-4.8
+
services:
- mongodb
- redis
+before_install:
+ - $CXX --version
+
script: make test-cov
-after_success: cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
+
+after_success: npm i codecov && codecov
diff --git a/History.md b/History.md
index 85a5e449da..918c8f4787 100644
--- a/History.md
+++ b/History.md
@@ -1,5 +1,14 @@
+2.1.0 / 2015-09-15
+==================
+
+* 使用 oneapm 代替 newrelic
+
+2.0.1 / 2015-08-07
+==================
+
+* 去掉【收藏功能】
-0.3.6 / 2013-11-22
+0.3.6 / 2013-11-22
==================
* fix #237 if topic not exists, do not modified it.
@@ -63,7 +72,7 @@
* limit the with of message links to prevent line breaks
* 修复文字过长没有换行的问题
-0.3.5 / 2013-05-30
+0.3.5 / 2013-05-30
==================
* Update logo based on the new official one (@finian)
@@ -81,7 +90,7 @@
* 增加邮件提示内容
* read file sync package.json
-0.3.4 / 2013-05-27
+0.3.4 / 2013-05-27
==================
* user markd instead showdown, use ace (@fengmk2)
@@ -102,7 +111,7 @@
* Add 0.10 for travis
* fixed #107 update user links
-0.3.3 / 2013-03-11
+0.3.3 / 2013-03-11
==================
* Merge pull request #126 from cnodejs/updateSignFlow
@@ -170,7 +179,7 @@
* Merge pull request #85 from dead-horse/master
* 过滤url允许绝对路径
-0.3.2 / 2012-03-04
+0.3.2 / 2012-03-04
==================
* ensure IncomingForm.UPLOAD_DIR
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000..29b5e9c758
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+(The MIT License)
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Makefile b/Makefile
index 347a106ac8..8a408ea27d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
TESTS = $(shell find test -type f -name "*.test.js")
-TEST_TIMEOUT = 5000
+TEST_TIMEOUT = 10000
MOCHA_REPORTER = spec
# NPM_REGISTRY = "--registry=http://registry.npm.taobao.org"
NPM_REGISTRY = ""
@@ -26,6 +26,14 @@ test: install pretest
--timeout $(TEST_TIMEOUT) \
$(TESTS)
+testfile:
+ @NODE_ENV=test ./node_modules/mocha/bin/mocha \
+ --reporter $(MOCHA_REPORTER) \
+ -r should \
+ -r test/env \
+ --timeout $(TEST_TIMEOUT) \
+ $(FILE)
+
test-cov cov: install pretest
@NODE_ENV=test node \
node_modules/.bin/istanbul cover --preserve-comments \
@@ -37,16 +45,17 @@ test-cov cov: install pretest
--timeout $(TEST_TIMEOUT) \
$(TESTS)
+
build:
- @./node_modules/loader/bin/build views .
+ @./node_modules/loader-builder/bin/builder views .
run:
@node app.js
start: install build
- @NODE_ENV=production nohup ./node_modules/.bin/pm2 start app.js -i 0 --name "cnode" --max-memory-restart 400M >> cnode.log 2>&1 &
+ @NODE_ENV=production ./node_modules/.bin/pm2 start app.js -i 0 --name "cnode" --max-memory-restart 400M
restart: install build
- @NODE_ENV=production nohup ./node_modules/.bin/pm2 restart "cnode" >> cnode.log 2>&1 &
+ @NODE_ENV=production ./node_modules/.bin/pm2 restart "cnode"
-.PHONY: install test cov test-cov build run start restart
+.PHONY: install test testfile cov test-cov build run start restart
diff --git a/README.md b/README.md
index bc406dad10..c4b08f5de4 100644
--- a/README.md
+++ b/README.md
@@ -2,17 +2,17 @@ Nodeclub
=
[![build status][travis-image]][travis-url]
-[![Coverage Status][coverage-image]][coverage-url]
+[![codecov.io][codecov-image]][codecov-url]
[![David deps][david-image]][david-url]
[![node version][node-image]][node-url]
-[travis-image]: https://img.shields.io/travis/cnodejs/nodeclub.svg?style=flat-square
+[travis-image]: https://img.shields.io/travis/cnodejs/nodeclub/master.svg?style=flat-square
[travis-url]: https://travis-ci.org/cnodejs/nodeclub
-[coverage-image]: https://img.shields.io/coveralls/cnodejs/nodeclub.svg?style=flat-square
-[coverage-url]: https://coveralls.io/r/cnodejs/nodeclub?branch=master
+[codecov-image]: https://img.shields.io/codecov/c/github/cnodejs/nodeclub/master.svg?style=flat-square
+[codecov-url]: https://codecov.io/github/cnodejs/nodeclub?branch=master
[david-image]: https://img.shields.io/david/cnodejs/nodeclub.svg?style=flat-square
[david-url]: https://david-dm.org/cnodejs/nodeclub
-[node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square
+[node-image]: https://img.shields.io/badge/node.js-%3E=_4.2-green.svg?style=flat-square
[node-url]: http://nodejs.org/download/
## 介绍
@@ -24,16 +24,16 @@ Nodeclub 是使用 **Node.js** 和 **MongoDB** 开发的社区系统,界面优
*不保证 Windows 系统的兼容性*
-线上跑的是 Node.js v1.5,MongoDB 是 v2.6,Redis 是 v2.8.9。
+线上跑的是 [Node.js](https://nodejs.org) v8.12.0,[MongoDB](https://www.mongodb.org) 是 v4.0.3,[Redis](http://redis.io) 是 v4.0.9。
```
-1. 安装 `node.js[必须]` `mongodb[必须]` `redis[必须]`
-2. 启动 mongodb 和 redis
+1. 安装 `Node.js[必须]` `MongoDB[必须]` `Redis[必须]`
+2. 启动 MongoDB 和 Redis
3. `$ make install` 安装 Nodeclub 的依赖包
4. `cp config.default.js config.js` 请根据需要修改配置文件
5. `$ make test` 确保各项服务都正常
6. `$ node app.js`
-7. visit `localhost:3000`
+7. visit `http://localhost:3000`
8. done!
```
diff --git a/api/v1/message.js b/api/v1/message.js
index 02b8609555..b5b9ede9d9 100644
--- a/api/v1/message.js
+++ b/api/v1/message.js
@@ -1,14 +1,18 @@
var eventproxy = require('eventproxy');
var Message = require('../../proxy').Message;
+var at = require('../../common/at');
+var renderHelper = require('../../common/render_helper');
var _ = require('lodash');
var index = function (req, res, next) {
var user_id = req.user._id;
+ var mdrender = req.query.mdrender === 'false' ? false : true;
var ep = new eventproxy();
ep.fail(next);
ep.all('has_read_messages', 'hasnot_read_messages', function (has_read_messages, hasnot_read_messages) {
res.send({
+ success: true,
data: {
has_read_messages: has_read_messages,
hasnot_read_messages: hasnot_read_messages
@@ -28,7 +32,10 @@ var index = function (req, res, next) {
doc.author = _.pick(doc.author, ['loginname', 'avatar_url']);
doc.topic = _.pick(doc.topic, ['id', 'author', 'title', 'last_reply_at']);
doc.reply = _.pick(doc.reply, ['id', 'content', 'ups', 'create_at']);
- doc = _.pick(doc, ['id', 'type', 'has_read', 'author', 'topic', 'reply']);
+ if (mdrender) {
+ doc.reply.content = renderHelper.markdown(at.linkUsers(doc.reply.content));
+ }
+ doc = _.pick(doc, ['id', 'type', 'has_read', 'author', 'topic', 'reply', 'create_at']);
return doc;
});
@@ -66,13 +73,33 @@ var markAll = function (req, res, next) {
});
res.send({
success: true,
- marked_msgs: unread,
+ marked_msgs: unread
});
});
};
exports.markAll = markAll;
+
+var markOne = function (req, res, next) {
+ var msg_id = req.params.msg_id;
+ var ep = new eventproxy();
+ ep.fail(next);
+ Message.updateOneMessageToRead(msg_id, ep.done('marked_result', function (result) {
+ return result;
+ }));
+
+ ep.all('marked_result', function (result) {
+ res.send({
+ success: true,
+ marked_msg_id: msg_id
+ });
+ });
+};
+
+exports.markOne = markOne;
+
+
var count = function (req, res, next) {
var userId = req.user.id;
@@ -80,7 +107,7 @@ var count = function (req, res, next) {
ep.fail(next);
Message.getMessagesCount(userId, ep.done(function (count) {
- res.send({data: count});
+ res.send({success: true, data: count});
}));
};
diff --git a/api/v1/middleware.js b/api/v1/middleware.js
index 64951f3a3a..c97a5f798b 100644
--- a/api/v1/middleware.js
+++ b/api/v1/middleware.js
@@ -2,17 +2,22 @@ var UserModel = require('../../models').User;
var eventproxy = require('eventproxy');
var validator = require('validator');
+// 非登录用户直接屏蔽
var auth = function (req, res, next) {
var ep = new eventproxy();
ep.fail(next);
- var accessToken = req.body.accesstoken || req.query.accesstoken;
+ var accessToken = String(req.body.accesstoken || req.query.accesstoken || '');
accessToken = validator.trim(accessToken);
UserModel.findOne({accessToken: accessToken}, ep.done(function (user) {
if (!user) {
+ res.status(401);
+ return res.send({success: false, error_msg: '错误的accessToken'});
+ }
+ if (user.is_block) {
res.status(403);
- return res.send({error_msg: 'wrong accessToken'});
+ return res.send({success: false, error_msg: '您的账户被禁用'});
}
req.user = user;
next();
@@ -21,3 +26,27 @@ var auth = function (req, res, next) {
};
exports.auth = auth;
+
+// 非登录用户也可通过
+var tryAuth = function (req, res, next) {
+ var ep = new eventproxy();
+ ep.fail(next);
+
+ var accessToken = String(req.body.accesstoken || req.query.accesstoken || '');
+ accessToken = validator.trim(accessToken);
+
+ UserModel.findOne({accessToken: accessToken}, ep.done(function (user) {
+ if (!user) {
+ return next()
+ }
+ if (user.is_block) {
+ res.status(403);
+ return res.send({success: false, error_msg: '您的账户被禁用'});
+ }
+ req.user = user;
+ next();
+ }));
+
+};
+
+exports.tryAuth = tryAuth;
diff --git a/api/v1/reply.js b/api/v1/reply.js
index f3108c6007..82d105c580 100644
--- a/api/v1/reply.js
+++ b/api/v1/reply.js
@@ -9,7 +9,7 @@ var config = require('../../config');
var create = function (req, res, next) {
var topic_id = req.params.topic_id;
- var content = req.body.content;
+ var content = req.body.content || '';
var reply_id = req.body.reply_id;
var ep = new eventproxy();
@@ -17,20 +17,23 @@ var create = function (req, res, next) {
var str = validator.trim(content);
if (str === '') {
- res.status(422);
- res.send({error_msg: '回复内容不能为空!'});
- return;
+ res.status(400);
+ return res.send({success: false, error_msg: '回复内容不能为空'});
}
+ if (!validator.isMongoId(topic_id)) {
+ res.status(400);
+ return res.send({success: false, error_msg: '不是有效的话题id'});
+ }
+
Topic.getTopic(topic_id, ep.done(function (topic) {
if (!topic) {
res.status(404);
- res.send({error_msg: 'topic `' + topic_id + '` not found'});
- return;
+ return res.send({success: false, error_msg: '话题不存在'});
}
if (topic.lock) {
res.status(403);
- return res.send({error_msg: 'topic is locked'});
+ return res.send({success: false, error_msg: '该话题已被锁定'});
}
ep.emit('topic', topic);
}));
@@ -67,7 +70,7 @@ var create = function (req, res, next) {
ep.all('reply_saved', 'message_saved', 'score_saved', function (reply) {
res.send({
success: true,
- reply_id: reply._id,
+ reply_id: reply._id
});
});
};
@@ -78,19 +81,22 @@ var ups = function (req, res, next) {
var replyId = req.params.reply_id;
var userId = req.user.id;
+ if (!validator.isMongoId(replyId)) {
+ res.status(400);
+ return res.send({success: false, error_msg: '不是有效的评论id'});
+ }
+
Reply.getReplyById(replyId, function (err, reply) {
if (err) {
return next(err);
}
if (!reply) {
res.status(404);
- return res.send({error_msg: 'reply `' + replyId + '` not found'});
+ return res.send({success: false, error_msg: '评论不存在'});
}
if (reply.author_id.equals(userId) && !config.debug) {
- // 不能帮自己点赞
- res.send({
- error_msg: '呵呵,不能帮自己点赞。',
- });
+ res.status(403);
+ return res.send({success: false, error_msg: '不能帮自己点赞'});
} else {
var action;
reply.ups = reply.ups || [];
diff --git a/api/v1/tools.js b/api/v1/tools.js
index 368bdb4bc8..6c82cd230e 100644
--- a/api/v1/tools.js
+++ b/api/v1/tools.js
@@ -8,7 +8,7 @@ var accesstoken = function (req, res, next) {
success: true,
loginname: req.user.loginname,
avatar_url: req.user.avatar_url,
- id: req.user.id,
+ id: req.user.id
});
};
exports.accesstoken = accesstoken;
diff --git a/api/v1/topic.js b/api/v1/topic.js
index 8ef50f24fb..2165909f52 100644
--- a/api/v1/topic.js
+++ b/api/v1/topic.js
@@ -19,7 +19,9 @@ var index = function (req, res, next) {
var mdrender = req.query.mdrender === 'false' ? false : true;
var query = {};
- if (tab && tab !== 'all') {
+ if (!tab || tab === 'all') {
+ query.tab = {$nin: ['job', 'dev']}
+ } else {
if (tab === 'good') {
query.good = true;
} else {
@@ -51,7 +53,7 @@ var index = function (req, res, next) {
'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']);
});
- res.send({data: topics});
+ res.send({success: true, data: topics});
});
});
};
@@ -59,16 +61,27 @@ var index = function (req, res, next) {
exports.index = index;
var show = function (req, res, next) {
- var topicId = req.params.id;
+ var topicId = String(req.params.id);
+
var mdrender = req.query.mdrender === 'false' ? false : true;
var ep = new eventproxy();
+ if (!validator.isMongoId(topicId)) {
+ res.status(400);
+ return res.send({success: false, error_msg: '不是有效的话题id'});
+ }
+
ep.fail(next);
TopicProxy.getFullTopic(topicId, ep.done(function (msg, topic, author, replies) {
if (!topic) {
- return res.send({error_msg: 'topic_id `' + topicId + '` is not exists.'});
+ res.status(404);
+ return res.send({success: false, error_msg: '话题不存在'});
}
+
+ topic.visit_count += 1;
+ topic.save();
+
topic = _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at',
'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']);
@@ -82,21 +95,42 @@ var show = function (req, res, next) {
reply.content = renderHelper.markdown(at.linkUsers(reply.content));
}
reply.author = _.pick(reply.author, ['loginname', 'avatar_url']);
- reply = _.pick(reply, ['id', 'author', 'content', 'ups', 'create_at']);
+ reply = _.pick(reply, ['id', 'author', 'content', 'ups', 'create_at', 'reply_id']);
+ reply.reply_id = reply.reply_id || null;
+
+ if (reply.ups && req.user && reply.ups.indexOf(req.user._id) != -1) {
+ reply.is_uped = true;
+ } else {
+ reply.is_uped = false;
+ }
+
return reply;
});
- res.send({data: topic});
+
+ ep.emit('full_topic', topic)
}));
+
+
+ if (!req.user) {
+ ep.emitLater('is_collect', null)
+ } else {
+ TopicCollect.getTopicCollect(req.user._id, topicId, ep.done('is_collect'))
+ }
+
+ ep.all('full_topic', 'is_collect', function (full_topic, is_collect) {
+ full_topic.is_collect = !!is_collect;
+
+ res.send({success: true, data: full_topic});
+ })
+
};
exports.show = show;
var create = function (req, res, next) {
- var title = validator.trim(req.body.title);
- title = validator.escape(title);
- var tab = validator.trim(req.body.tab);
- tab = validator.escape(tab);
- var content = validator.trim(req.body.content);
+ var title = validator.trim(req.body.title || '');
+ var tab = validator.trim(req.body.tab || '');
+ var content = validator.trim(req.body.content || '');
// 得到所有的 tab, e.g. ['ask', 'share', ..]
var allTabs = config.tabs.map(function (tPair) {
@@ -106,21 +140,19 @@ var create = function (req, res, next) {
// 验证
var editError;
if (title === '') {
- editError = '标题不能是空的。';
+ editError = '标题不能为空';
} else if (title.length < 5 || title.length > 100) {
- editError = '标题字数太多或太少。';
- } else if (!tab || allTabs.indexOf(tab) === -1) {
- editError = '必须选择一个版块。';
+ editError = '标题字数太多或太少';
+ } else if (!tab || !_.includes(allTabs, tab)) {
+ editError = '必须选择一个版块';
} else if (content === '') {
editError = '内容不可为空';
}
// END 验证
if (editError) {
- res.status(422);
- return res.send({
- error_msg: editError,
- });
+ res.status(400);
+ return res.send({success: false, error_msg: editError});
}
TopicProxy.newAndSave(title, content, tab, req.user.id, function (err, topic) {
@@ -134,7 +166,7 @@ var create = function (req, res, next) {
proxy.all('score_saved', function () {
res.send({
success: true,
- topic_id: topic.id,
+ topic_id: topic.id
});
});
UserProxy.getUserById(req.user.id, proxy.done(function (user) {
@@ -152,73 +184,61 @@ var create = function (req, res, next) {
exports.create = create;
-exports.collect = function (req, res, next) {
- var topic_id = req.body.topic_id;
- TopicProxy.getTopic(topic_id, function (err, topic) {
- if (err) {
- return next(err);
- }
+exports.update = function (req, res, next) {
+ var topic_id = _.trim(req.body.topic_id);
+ var title = _.trim(req.body.title);
+ var tab = _.trim(req.body.tab);
+ var content = _.trim(req.body.content);
+
+ // 得到所有的 tab, e.g. ['ask', 'share', ..]
+ var allTabs = config.tabs.map(function (tPair) {
+ return tPair[0];
+ });
+
+ TopicProxy.getTopicById(topic_id, function (err, topic, tags) {
if (!topic) {
- return res.json({error_msg: '主题不存在'});
+ res.status(400);
+ return res.send({success: false, error_msg: '此话题不存在或已被删除。'});
}
- TopicCollect.getTopicCollect(req.user.id, topic._id, function (err, doc) {
- if (err) {
- return next(err);
+ if (topic.author_id.equals(req.user._id) || req.user.is_admin) {
+ // 验证
+ var editError;
+ if (title === '') {
+ editError = '标题不能是空的。';
+ } else if (title.length < 5 || title.length > 100) {
+ editError = '标题字数太多或太少。';
+ } else if (!tab || !_.includes(allTabs, tab)) {
+ editError = '必须选择一个版块。';
}
- if (doc) {
- res.json({success: true});
- return;
+ // END 验证
+
+ if (editError) {
+ return res.send({success: false, error_msg: editError});
}
- TopicCollect.newAndSave(req.user.id, topic._id, function (err) {
- if (err) {
- return next(err);
- }
- res.json({success: true});
- });
- UserProxy.getUserById(req.user.id, function (err, user) {
+ //保存话题
+ topic.title = title;
+ topic.content = content;
+ topic.tab = tab;
+ topic.update_at = new Date();
+
+ topic.save(function (err) {
if (err) {
return next(err);
}
- user.collect_topic_count += 1;
- user.save();
- });
+ //发送at消息
+ at.sendMessageToMentionUsers(content, topic._id, req.user._id);
- req.user.collect_topic_count += 1;
- topic.collect_count += 1;
- topic.save();
- });
- });
-};
-
-exports.de_collect = function (req, res, next) {
- var topic_id = req.body.topic_id;
- TopicProxy.getTopic(topic_id, function (err, topic) {
- if (err) {
- return next(err);
- }
- if (!topic) {
- return res.json({error_msg: '主题不存在'});
+ res.send({
+ success: true,
+ topic_id: topic.id
+ });
+ });
+ } else {
+ res.status(403)
+ return res.send({success: false, error_msg: '对不起,你不能编辑此话题。'});
}
- TopicCollect.remove(req.user.id, topic._id, function (err) {
- if (err) {
- return next(err);
- }
- res.json({success: true});
- });
-
- UserProxy.getUserById(req.user.id, function (err, user) {
- if (err) {
- return next(err);
- }
- user.collect_topic_count -= 1;
- user.save();
- });
-
- topic.collect_count -= 1;
- topic.save();
-
- req.user.collect_topic_count -= 1;
});
};
+
diff --git a/api/v1/topic_collect.js b/api/v1/topic_collect.js
new file mode 100644
index 0000000000..5a6d01360c
--- /dev/null
+++ b/api/v1/topic_collect.js
@@ -0,0 +1,141 @@
+var eventproxy = require('eventproxy');
+var TopicProxy = require('../../proxy').Topic;
+var TopicCollectProxy = require('../../proxy').TopicCollect;
+var UserProxy = require('../../proxy').User;
+var _ = require('lodash');
+var validator = require('validator');
+
+function list(req, res, next) {
+ var loginname = req.params.loginname;
+ var ep = new eventproxy();
+
+ ep.fail(next);
+
+ UserProxy.getUserByLoginName(loginname, ep.done(function (user) {
+ if (!user) {
+ res.status(404);
+ return res.send({success: false, error_msg: '用户不存在'});
+ }
+
+ // api 返回 100 条就好了
+ TopicCollectProxy.getTopicCollectsByUserId(user._id, {limit: 100}, ep.done('collected_topics'));
+
+ ep.all('collected_topics', function (collected_topics) {
+
+ var ids = collected_topics.map(function (doc) {
+ return String(doc.topic_id)
+ });
+ var query = { _id: { '$in': ids } };
+ TopicProxy.getTopicsByQuery(query, {}, ep.done('topics', function (topics) {
+ topics = _.sortBy(topics, function (topic) {
+ return ids.indexOf(String(topic._id))
+ });
+ return topics
+ }));
+
+ });
+
+ ep.all('topics', function (topics) {
+ topics = topics.map(function (topic) {
+ topic.author = _.pick(topic.author, ['loginname', 'avatar_url']);
+ return _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at',
+ 'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']);
+ });
+ res.send({success: true, data: topics})
+
+ })
+ }))
+}
+
+exports.list = list;
+
+function collect(req, res, next) {
+ var topic_id = req.body.topic_id;
+
+ if (!validator.isMongoId(topic_id)) {
+ res.status(400);
+ return res.send({success: false, error_msg: '不是有效的话题id'});
+ }
+
+ TopicProxy.getTopic(topic_id, function (err, topic) {
+ if (err) {
+ return next(err);
+ }
+ if (!topic) {
+ res.status(404);
+ return res.json({success: false, error_msg: '话题不存在'});
+ }
+
+ TopicCollectProxy.getTopicCollect(req.user.id, topic._id, function (err, doc) {
+ if (err) {
+ return next(err);
+ }
+ if (doc) {
+ res.json({success: false});
+ return;
+ }
+
+ TopicCollectProxy.newAndSave(req.user.id, topic._id, function (err) {
+ if (err) {
+ return next(err);
+ }
+ res.json({success: true});
+ });
+ UserProxy.getUserById(req.user.id, function (err, user) {
+ if (err) {
+ return next(err);
+ }
+ user.collect_topic_count += 1;
+ user.save();
+ });
+
+ topic.collect_count += 1;
+ topic.save();
+ });
+ });
+}
+
+exports.collect = collect;
+
+function de_collect(req, res, next) {
+ var topic_id = req.body.topic_id;
+
+ if (!validator.isMongoId(topic_id)) {
+ res.status(400);
+ return res.send({success: false, error_msg: '不是有效的话题id'});
+ }
+
+ TopicProxy.getTopic(topic_id, function (err, topic) {
+ if (err) {
+ return next(err);
+ }
+ if (!topic) {
+ res.status(404);
+ return res.json({success: false, error_msg: '话题不存在'});
+ }
+ TopicCollectProxy.remove(req.user.id, topic._id, function (err, removeResult) {
+ if (err) {
+ return next(err);
+ }
+ if (removeResult.n == 0) {
+ return res.json({success: false})
+ }
+
+ UserProxy.getUserById(req.user.id, function (err, user) {
+ if (err) {
+ return next(err);
+ }
+ user.collect_topic_count -= 1;
+ user.save();
+ });
+
+ topic.collect_count -= 1;
+ topic.save();
+
+ res.json({success: true});
+ });
+
+ });
+}
+
+exports.de_collect = de_collect;
diff --git a/api/v1/user.js b/api/v1/user.js
index 8d4a7385a6..7696bc27f3 100644
--- a/api/v1/user.js
+++ b/api/v1/user.js
@@ -8,44 +8,37 @@ var TopicCollect = require('../../proxy').TopicCollect;
var show = function (req, res, next) {
var loginname = req.params.loginname;
var ep = new eventproxy();
-
+
ep.fail(next);
UserProxy.getUserByLoginName(loginname, ep.done(function (user) {
if (!user) {
- return res.send({error_msg: 'user `' + loginname + '` is not exists'});
+ res.status(404);
+ return res.send({success: false, error_msg: '用户不存在'});
}
var query = {author_id: user._id};
- var opt = {limit: 5, sort: '-create_at'};
+ var opt = {limit: 15, sort: '-create_at'};
TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_topics'));
ReplyProxy.getRepliesByAuthorId(user._id, {limit: 20, sort: '-create_at'},
ep.done(function (replies) {
- var topic_ids = [];
- for (var i = 0; i < replies.length; i++) {
- if (topic_ids.indexOf(replies[i].topic_id.toString()) < 0) {
- topic_ids.push(replies[i].topic_id.toString());
- }
- }
- var query = {_id: {'$in': topic_ids}};
- var opt = {limit: 5, sort: '-create_at'};
- TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_replies'));
- }));
+ var topic_ids = replies.map(function (reply) {
+ return reply.topic_id.toString()
+ });
+ topic_ids = _.uniq(topic_ids).slice(0, 5); // 只显示最近5条
- TopicCollect.getTopicCollectsByUserId(user._id,
- ep.done(function (collections) {
- var topic_ids = [];
- for (var i = 0; i < collections.length; i++) {
- if (topic_ids.indexOf(collections[i].topic_id.toString()) < 0) {
- topic_ids.push(collections[i].topic_id.toString());
- }
- }
var query = {_id: {'$in': topic_ids}};
- var opt = {sort: '-create_at'};
- TopicProxy.getTopicsByQuery(query, opt, ep.done('collect_topics'));
+ var opt = {};
+ TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_replies', function (recent_replies) {
+ recent_replies = _.sortBy(recent_replies, function (topic) {
+ return topic_ids.indexOf(topic._id.toString())
+ });
+ return recent_replies;
+ }));
}));
- ep.all('recent_topics', 'recent_replies', 'collect_topics',
- function (recent_topics, recent_replies, collect_topics) {
+
+ ep.all('recent_topics', 'recent_replies',
+ function (recent_topics, recent_replies) {
user = _.pick(user, ['loginname', 'avatar_url', 'githubUsername',
'create_at', 'score']);
@@ -60,13 +53,8 @@ var show = function (req, res, next) {
topic = _.pick(topic, ['id', 'author', 'title', 'last_reply_at']);
return topic;
});
- user.collect_topics = collect_topics.map(function (topic) {
- topic.author = _.pick(topic.author, ['loginname', 'avatar_url']);
- topic = _.pick(topic, ['id', 'author', 'title', 'last_reply_at']);
- return topic;
- });
- res.send({data: user});
+ res.send({success: true, data: user});
});
}));
};
diff --git a/api_router_v1.js b/api_router_v1.js
index 961fff5bc3..7d876cdf9b 100644
--- a/api_router_v1.js
+++ b/api_router_v1.js
@@ -1,5 +1,6 @@
var express = require('express');
var topicController = require('./api/v1/topic');
+var topicCollectController = require('./api/v1/topic_collect');
var userController = require('./api/v1/user');
var toolsController = require('./api/v1/tools');
var replyController = require('./api/v1/reply');
@@ -13,24 +14,32 @@ var router = express.Router();
// 主题
router.get('/topics', topicController.index);
-router.get('/topic/:id', topicController.show);
-router.post('/topics', middleware.auth, limit.peruserperday('create_topic', config.create_post_per_day), topicController.create);
-router.post('/topic/collect', middleware.auth, topicController.collect); // 关注某话题
-router.post('/topic/de_collect', middleware.auth, topicController.de_collect); // 取消关注某话题
+router.get('/topic/:id', middleware.tryAuth, topicController.show);
+router.post('/topics', middleware.auth, limit.peruserperday('create_topic', config.create_post_per_day, {showJson: true}), topicController.create);
+router.post('/topics/update', middleware.auth, topicController.update);
+
+
+// 主题收藏
+router.post('/topic_collect/collect', middleware.auth, topicCollectController.collect); // 关注某话题
+router.post('/topic_collect/de_collect', middleware.auth, topicCollectController.de_collect); // 取消关注某话题
+router.get('/topic_collect/:loginname', topicCollectController.list);
// 用户
router.get('/user/:loginname', userController.show);
+
+
// accessToken 测试
router.post('/accesstoken', middleware.auth, toolsController.accesstoken);
// 评论
-router.post('/topic/:topic_id/replies', middleware.auth, limit.peruserperday('create_reply', config.create_reply_per_day), replyController.create);
+router.post('/topic/:topic_id/replies', middleware.auth, limit.peruserperday('create_reply', config.create_reply_per_day, {showJson: true}), replyController.create);
router.post('/reply/:reply_id/ups', middleware.auth, replyController.ups);
// 通知
router.get('/messages', middleware.auth, messageController.index);
router.get('/message/count', middleware.auth, messageController.count);
router.post('/message/mark_all', middleware.auth, messageController.markAll);
+router.post('/message/mark_one/:msg_id', middleware.auth, messageController.markOne);
module.exports = router;
diff --git a/app.js b/app.js
index 9a095798d4..0c20ec938e 100644
--- a/app.js
+++ b/app.js
@@ -8,53 +8,56 @@
var config = require('./config');
-if (!config.debug) {
- require('newrelic');
+if (!config.debug && config.oneapm_key) {
+ require('oneapm');
}
require('colors');
-var path = require('path');
-var Loader = require('loader');
-var express = require('express');
-var session = require('express-session');
-var passport = require('passport');
+var path = require('path');
+var Loader = require('loader');
+var LoaderConnect = require('loader-connect')
+var express = require('express');
+var session = require('express-session');
+var passport = require('passport');
require('./middlewares/mongoose_log'); // 打印 mongodb 查询日志
require('./models');
-var GitHubStrategy = require('passport-github').Strategy;
+var GitHubStrategy = require('passport-github').Strategy;
var githubStrategyMiddleware = require('./middlewares/github_strategy');
-var webRouter = require('./web_router');
-var apiRouterV1 = require('./api_router_v1');
-var auth = require('./middlewares/auth');
-var errorPageMiddleware = require("./middlewares/error_page");
-var proxyMiddleware = require('./middlewares/proxy');
-var RedisStore = require('connect-redis')(session);
-var _ = require('lodash');
-var csurf = require('csurf');
-var compress = require('compression');
-var bodyParser = require('body-parser');
-var busboy = require('connect-busboy');
-var errorhandler = require('errorhandler');
-var cors = require('cors');
-var requestLog = require('./middlewares/request_log');
-var renderMiddleware = require('./middlewares/render');
-var logger = require("./common/logger");
+var webRouter = require('./web_router');
+var apiRouterV1 = require('./api_router_v1');
+var auth = require('./middlewares/auth');
+var errorPageMiddleware = require('./middlewares/error_page');
+var proxyMiddleware = require('./middlewares/proxy');
+var RedisStore = require('connect-redis')(session);
+var _ = require('lodash');
+var csurf = require('csurf');
+var compress = require('compression');
+var bodyParser = require('body-parser');
+var busboy = require('connect-busboy');
+var errorhandler = require('errorhandler');
+var cors = require('cors');
+var requestLog = require('./middlewares/request_log');
+var renderMiddleware = require('./middlewares/render');
+var logger = require('./common/logger');
+var helmet = require('helmet');
+var bytes = require('bytes')
// 静态文件目录
var staticDir = path.join(__dirname, 'public');
// assets
-var assets = {};
+var assets = {};
if (config.mini_assets) {
try {
assets = require('./assets.json');
} catch (e) {
- console.log('You must execute `make build` before start app when mini_assets is true.');
+ logger.error('You must execute `make build` before start app when mini_assets is true.');
throw e;
}
}
-var urlinfo = require('url').parse(config.host);
+var urlinfo = require('url').parse(config.host);
config.hostname = urlinfo.hostname || config.host;
var app = express();
@@ -75,15 +78,17 @@ if (config.debug) {
}
// 静态资源
-app.use(Loader.less(__dirname));
+if (config.debug) {
+ app.use(LoaderConnect.less(__dirname)); // 测试环境用,编译 .less on the fly
+}
app.use('/public', express.static(staticDir));
app.use('/agent', proxyMiddleware.proxy);
-// 每日访问限制
-
+// 通用的中间件
app.use(require('response-time')());
-app.use(bodyParser.json());
-app.use(bodyParser.urlencoded({ extended: true }));
+app.use(helmet.frameguard('sameorigin'));
+app.use(bodyParser.json({limit: '1mb'}));
+app.use(bodyParser.urlencoded({ extended: true, limit: '1mb' }));
app.use(require('method-override')());
app.use(require('cookie-parser')(config.session_secret));
app.use(compress());
@@ -92,20 +97,32 @@ app.use(session({
store: new RedisStore({
port: config.redis_port,
host: config.redis_host,
+ db: config.redis_db,
+ pass: config.redis_password,
}),
- resave: true,
- saveUninitialized: true,
+ resave: false,
+ saveUninitialized: false,
}));
+// oauth 中间件
app.use(passport.initialize());
+// github oauth
+passport.serializeUser(function (user, done) {
+ done(null, user);
+});
+passport.deserializeUser(function (user, done) {
+ done(null, user);
+});
+passport.use(new GitHubStrategy(config.GITHUB_OAUTH, githubStrategyMiddleware));
+
// custom middleware
app.use(auth.authUser);
app.use(auth.blockUser());
if (!config.debug) {
app.use(function (req, res, next) {
- if (req.path.indexOf('/api') === -1) {
+ if (req.path === '/api' || req.path.indexOf('/api') === -1) {
csurf()(req, res, next);
return;
}
@@ -133,18 +150,9 @@ app.use(function (req, res, next) {
next();
});
-// github oauth
-passport.serializeUser(function (user, done) {
- done(null, user);
-});
-passport.deserializeUser(function (user, done) {
- done(null, user);
-});
-passport.use(new GitHubStrategy(config.GITHUB_OAUTH, githubStrategyMiddleware));
-
app.use(busboy({
limits: {
- fileSize: 10 * 1024 * 1024 // 10MB
+ fileSize: bytes(config.file_limit)
}
}));
@@ -157,17 +165,18 @@ if (config.debug) {
app.use(errorhandler());
} else {
app.use(function (err, req, res, next) {
- console.error('server 500 error:', err);
+ logger.error(err);
return res.status(500).send('500 status');
});
}
-app.listen(config.port, function () {
- logger.log("NodeClub listening on port %d", config.port);
- logger.log("God bless love....");
- logger.log("You can debug your app with http://" + config.hostname + ':' + config.port);
- logger.log("");
-});
-
+if (!module.parent) {
+ app.listen(config.port, function () {
+ logger.info('NodeClub listening on port', config.port);
+ logger.info('God bless love....');
+ logger.info('You can debug your app with http://' + config.hostname + ':' + config.port);
+ logger.info('');
+ });
+}
module.exports = app;
diff --git a/bin/fix_at_problem.js b/bin/fix_at_problem.js
index a1beb0ad17..8374a4f654 100644
--- a/bin/fix_at_problem.js
+++ b/bin/fix_at_problem.js
@@ -1,4 +1,5 @@
// 一次性脚本
+// 修复之前重复编辑帖子会导致重复 @someone 的渲染问题
var TopicModel = require('../models').Topic;
TopicModel.find({content: /\[{2,}@/}).exec(function (err, topics) {
diff --git a/bin/fix_topic_collect_count.js b/bin/fix_topic_collect_count.js
new file mode 100644
index 0000000000..577dfb35a1
--- /dev/null
+++ b/bin/fix_topic_collect_count.js
@@ -0,0 +1,61 @@
+var TopicCollect = require('../models').TopicCollect;
+var UserModel = require('../models').User;
+var TopicModel = require('../models').Topic
+
+// 修复用户的topic_collect计数
+TopicCollect.aggregate(
+ [{
+ "$group" :
+ {
+ _id : {user_id: "$user_id"},
+ count : { $sum : 1}
+ }
+ }], function (err, result) {
+ result.forEach(function (row) {
+ var userId = row._id.user_id;
+ var count = row.count;
+
+ UserModel.findOne({
+ _id: userId
+ }, function (err, user) {
+
+ if (!user) {
+ return;
+ }
+
+ user.collect_topic_count = count;
+ user.save(function () {
+ console.log(user.loginname, count)
+ });
+ })
+ })
+ })
+
+ // 修复帖子的topic_collect计数
+ TopicCollect.aggregate(
+ [{
+ "$group" :
+ {
+ _id : {topic_id: "$topic_id"},
+ count : { $sum : 1}
+ }
+ }], function (err, result) {
+ result.forEach(function (row) {
+ var topic_id = row._id.topic_id;
+ var count = row.count;
+
+ TopicModel.findOne({
+ _id: topic_id
+ }, function (err, topic) {
+
+ if (!topic) {
+ return;
+ }
+
+ topic.collect_topic_count = count;
+ topic.save(function () {
+ console.log(topic.id, count)
+ });
+ })
+ })
+ })
diff --git a/bin/get_user_topics.js b/bin/get_user_topics.js
new file mode 100644
index 0000000000..9a1b3d43ea
--- /dev/null
+++ b/bin/get_user_topics.js
@@ -0,0 +1,14 @@
+var UserModel = require('../models').User;
+var TopicModel = require('../models').Topic
+
+// usage:
+// node get_user_topics.js alsotang
+UserModel.findOne({
+ loginname: process.argv[2]
+}, function (err, user) {
+ TopicModel.find({
+ author_id: user._id
+ }, function (err, topics) {
+ console.log(topics)
+ })
+})
\ No newline at end of file
diff --git a/common/at.js b/common/at.js
index 3df4442050..b6ddaf16dc 100644
--- a/common/at.js
+++ b/common/at.js
@@ -20,6 +20,10 @@ var _ = require('lodash');
* @return {Array} 用户名数组
*/
var fetchUsers = function (text) {
+ if (!text) {
+ return [];
+ }
+
var ignoreRegexs = [
/```.+?```/g, // 去除单行的 ```
/^```[\s\S]+?^```/gm, // ``` 里面的是 pre 标签内容
@@ -27,6 +31,7 @@ var fetchUsers = function (text) {
/^ .*/gm, // 4个空格也是 pre 标签,在这里 . 不会匹配换行
/\b\S*?@[^\s]*?\..+?\b/g, // somebody@gmail.com 会被去除
/\[@.+?\]\(\/.+?\)/g, // 已经被 link 的 username
+ /\/@/g, // 一般是url中path的一部分
];
ignoreRegexs.forEach(function (ignore_regex) {
@@ -98,7 +103,7 @@ exports.linkUsers = function (text, callback) {
var users = fetchUsers(text);
for (var i = 0, l = users.length; i < l; i++) {
var name = users[i];
- text = text.replace(new RegExp('@' + name + '\\b', 'g'), '[@' + name + '](/user/' + name + ')');
+ text = text.replace(new RegExp('@' + name + '\\b(?!\\])', 'g'), '[@' + name + '](/user/' + name + ')');
}
if (!callback) {
return text;
diff --git a/common/logger.js b/common/logger.js
index f457c99c82..ba7fbe71ee 100644
--- a/common/logger.js
+++ b/common/logger.js
@@ -1,62 +1,18 @@
-var fs = require('fs');
var config = require('../config');
+var pathLib = require('path')
-if (!fs.existsSync("./log")) {
- fs.mkdirSync("./log");
-}
+var env = process.env.NODE_ENV || "development"
-exports.log = function () {
- writeLog('', 'info', arguments);
-};
-exports.info = function () {
- writeLog(' ', 'info', arguments);
-};
-
-exports.debug = function () {
- writeLog(" ", 'debug', arguments);
-};
-
-exports.warn = function () {
- writeLog(" ", 'warn', arguments);
-};
-
-exports.error = function () {
- writeLog(" ", 'error', arguments);
-};
-
-var env = process.env.NODE_ENV || "development";
-var consolePrint = config.debug && env !== 'test';
-var writeLog = function (prefix, logType, args) {
- var filePrint = logType !== 'debug';
-
- if (!filePrint && !consolePrint) {
- return;
- }
-
- var infos = Array.prototype.slice.call(args);
- var logStr = infos.join(" ");
-
- switch (logType) {
- case "debug":
- logStr = logStr.gray;
- break;
- case 'warn':
- logStr = logStr.yellow;
- break;
- case 'error':
- logStr = logStr.red;
- break;
- }
-
- var line = prefix + logStr;
-
- if (filePrint) {
- fs.appendFile('./log/' + env + '.log', line + "\n");
- }
- if (consolePrint) {
- console.log(line);
- }
-};
+var log4js = require('log4js');
+log4js.configure({
+ appenders: [
+ { type: 'console' },
+ { type: 'file', filename: pathLib.join(config.log_dir, 'cheese.log'), category: 'cheese' }
+ ]
+});
+var logger = log4js.getLogger('cheese');
+logger.setLevel(config.debug && env !== 'test' ? 'DEBUG' : 'ERROR')
+module.exports = logger;
diff --git a/common/mail.js b/common/mail.js
index a13b91ed91..a6d3b73b0a 100644
--- a/common/mail.js
+++ b/common/mail.js
@@ -1,8 +1,11 @@
var mailer = require('nodemailer');
+var smtpTransport = require('nodemailer-smtp-transport');
var config = require('../config');
var util = require('util');
-var transport = mailer.createTransport('SMTP', config.mail_opts);
+var logger = require('./logger');
+var transporter = mailer.createTransport(smtpTransport(config.mail_opts));
var SITE_ROOT_URL = 'http://' + config.host;
+var async = require('async')
/**
* Send an email
@@ -12,13 +15,23 @@ var sendMail = function (data) {
if (config.debug) {
return;
}
- // 遍历邮件数组,发送每一封邮件,如果有发送失败的,就再压入数组,同时触发mailEvent事件
- transport.sendMail(data, function (err) {
+
+ // 重试5次
+ async.retry({times: 5}, function (done) {
+ transporter.sendMail(data, function (err) {
+ if (err) {
+ // 写为日志
+ logger.error('send mail error', err, data);
+ return done(err);
+ }
+ return done()
+ });
+ }, function (err) {
if (err) {
- // 写为日志
- console.log(err);
+ return logger.error('send mail finally error', err, data);
}
- });
+ logger.info('send mail success', data)
+ })
};
exports.sendMail = sendMail;
diff --git a/common/message.js b/common/message.js
index bba03e586e..ccc82212fc 100644
--- a/common/message.js
+++ b/common/message.js
@@ -2,7 +2,6 @@ var models = require('../models');
var eventproxy = require('eventproxy');
var Message = models.Message;
var User = require('../proxy').User;
-var push = require('../common/push');
var messageProxy = require('../proxy/message');
var _ = require('lodash');
@@ -20,7 +19,6 @@ exports.sendReplyMessage = function (master_id, author_id, topic_id, reply_id, c
message.save(ep.done('message_saved'));
ep.all('message_saved', function (msg) {
- push.send(message.type, author_id, master_id, topic_id);
callback(null, msg);
});
};
@@ -39,7 +37,6 @@ exports.sendAtMessage = function (master_id, author_id, topic_id, reply_id, call
message.save(ep.done('message_saved'));
ep.all('message_saved', function (msg) {
- push.send(message.type, author_id, master_id, topic_id);
callback(null, msg);
});
};
diff --git a/common/push.js b/common/push.js
deleted file mode 100644
index 80c2f064b9..0000000000
--- a/common/push.js
+++ /dev/null
@@ -1,59 +0,0 @@
-var User = require('../proxy/user');
-var Message = require('../proxy/message');
-var JPush = require("jpush-sdk");
-var eventproxy = require('eventproxy');
-var config = require('../config');
-var client = null;
-
-if (config.jpush && config.jpush.masterSecret !== 'YourSecretKeyyyyyyyyyyyyy') {
- client = JPush.buildClient(config.jpush);
-}
-
-/**
- * 通过极光推送发生消息通知
- * @param {String} type 消息类型
- * @param {String} author_id 消息作者ID
- * @param {String} master_id 被通知者ID
- * @param {String} topic_id 相关主题ID
- */
-exports.send = function (type, author_id, master_id, topic_id) {
- if (client !== null) {
- var ep = new eventproxy();
- User.getUserById(author_id, ep.done('author'));
- Message.getMessagesCount(master_id, ep.done('count'));
- ep.all('author', 'count', function (author, count) {
- var msg = author.loginname + ' ';
- var extras = {
- topicId: topic_id
- };
- switch (type) {
- case 'at':
- msg += '@了你';
- break;
- case 'reply':
- msg += '回复了你的主题';
- break;
- default:
- break;
- }
- client.push()
- .setPlatform(JPush.ALL)
- .setAudience(JPush.alias(master_id.toString()))
- .setNotification(msg,
- JPush.ios(msg, null, count, null, extras),
- JPush.android(msg, null, null, extras)
- )
- .setOptions(null, null, null, !config.debug)
- .send(function (err, res) {
- if (config.debug) {
- if (err) {
- console.log(err.message);
- } else {
- console.log('Sendno: ' + res.sendno);
- console.log('Msg_id: ' + res.msg_id);
- }
- }
- });
- })
- }
-};
diff --git a/common/redis.js b/common/redis.js
index d956501872..01939b070c 100644
--- a/common/redis.js
+++ b/common/redis.js
@@ -1,10 +1,19 @@
var config = require('../config');
var Redis = require('ioredis');
+var logger = require('./logger')
var client = new Redis({
port: config.redis_port,
host: config.redis_host,
db: config.redis_db,
+ password: config.redis_password,
});
+client.on('error', function (err) {
+ if (err) {
+ logger.error('connect to redis error, check your redis config', err);
+ process.exit(1);
+ }
+})
+
exports = module.exports = client;
diff --git a/common/render_helper.js b/common/render_helper.js
index f180372396..73368d04ca 100644
--- a/common/render_helper.js
+++ b/common/render_helper.js
@@ -14,14 +14,14 @@ var MarkdownIt = require('markdown-it');
var _ = require('lodash');
var config = require('../config');
var validator = require('validator');
-var multiline = require('multiline');
var jsxss = require('xss');
+var multiline = require('multiline')
// Set default options
var md = new MarkdownIt();
md.set({
- html: true, // Enable HTML tags in source
+ html: false, // Enable HTML tags in source
xhtmlOut: false, // Use '/' to close single tags (
)
breaks: false, // Convert '\n' in paragraphs into
linkify: true, // Autoconvert URL-like text to links
@@ -30,7 +30,7 @@ md.set({
md.renderer.rules.fence = function (tokens, idx) {
var token = tokens[idx];
- var language = token.params && ('language-' + token.params) || '';
+ var language = token.info && ('language-' + token.info) || '';
language = validator.escape(language);
return '
'
@@ -40,18 +40,12 @@ md.renderer.rules.fence = function (tokens, idx) {
md.renderer.rules.code_block = function (tokens, idx /*, options*/) {
var token = tokens[idx];
- var language = token.params && ('language-' + token.params) || '';
- language = validator.escape(language);
- return ''
+ return ''
+ '' + validator.escape(token.content) + ''
+ '';
};
-md.renderer.rules.code_inline = function (tokens, idx /*, options*/) {
- return '' + validator.escape(tokens[idx].content) + '';
-};
-
var myxss = new jsxss.FilterXSS({
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
// 让 prettyprint 可以工作
@@ -65,8 +59,6 @@ exports.markdown = function (text) {
return '' + myxss.process(md.render(text || '')) + '';
};
-exports.multiline = multiline;
-
exports.escapeSignature = function (signature) {
return signature.split('\n').map(function (p) {
return _.escape(p);
@@ -90,7 +82,11 @@ exports.tabName = function (tab) {
};
exports.proxy = function (url) {
- return '"/agent?&url=' + encodeURIComponent(url) + '"';
+ return url;
+ // 当 google 和 github 封锁严重时,则需要通过服务器代理访问它们的静态资源
+ // return '/agent?url=' + encodeURIComponent(url);
};
+// 为了在 view 中使用
exports._ = _;
+exports.multiline = multiline;
diff --git a/common/tools.js b/common/tools.js
index d44da6022f..d047c393b9 100644
--- a/common/tools.js
+++ b/common/tools.js
@@ -1,4 +1,4 @@
-var bcrypt = require('bcrypt');
+var bcrypt = require('bcryptjs');
var moment = require('moment');
moment.locale('zh-cn'); // 使用中文
diff --git a/config.default.js b/config.default.js
index 85e7cca29e..3cbd990593 100644
--- a/config.default.js
+++ b/config.default.js
@@ -41,6 +41,7 @@ var config = {
redis_host: '127.0.0.1',
redis_port: 6379,
redis_db: 0,
+ redis_password: '',
session_secret: 'node_club_secret', // 务必修改
auth_cookie_name: 'node_club',
@@ -61,6 +62,8 @@ var config = {
max_rss_items: 50
},
+ log_dir: path.join(__dirname, 'logs'),
+
// 邮箱配置
mail_opts: {
host: 'smtp.126.com',
@@ -68,14 +71,15 @@ var config = {
auth: {
user: 'club@126.com',
pass: 'club'
- }
+ },
+ ignoreTLS: true,
},
//weibo app key
weibo_key: 10000000,
weibo_id: 'your_weibo_id',
- // admin 可删除话题,编辑标签,设某人为达人
+ // admin 可删除话题,编辑标签。把 user_login_name 换成你的登录名
admins: { user_login_name: true },
// github 登陆的配置
@@ -87,8 +91,8 @@ var config = {
// 是否允许直接注册(否则只能走 github 的方式)
allow_sign_up: true,
- // newrelic 是个用来监控网站性能的服务
- newrelic_key: 'yourkey',
+ // oneapm 是个用来监控网站性能的服务
+ oneapm_key: '',
// 下面两个配置都是文件上传的配置
@@ -97,7 +101,10 @@ var config = {
accessKey: 'your access key',
secretKey: 'your secret key',
bucket: 'your bucket name',
- domain: 'http://{bucket}.qiniudn.com/'
+ origin: 'http://your qiniu domain',
+ // 如果vps在国外,请使用 http://up.qiniug.com/ ,这是七牛的国际节点
+ // 如果在国内,此项请留空
+ uploadURL: 'http://xxxxxxxx/',
},
// 文件上传配置
@@ -107,6 +114,8 @@ var config = {
url: '/public/upload/'
},
+ file_limit: '1MB',
+
// 版块
tabs: [
['share', '分享'],
@@ -123,6 +132,7 @@ var config = {
create_post_per_day: 1000, // 每个用户一天可以发的主题数
create_reply_per_day: 1000, // 每个用户一天可以发的评论数
+ create_user_per_ip: 1000, // 每个 ip 每天可以注册账号的次数
visit_per_day: 1000, // 每个 ip 每天能访问的次数
};
diff --git a/controllers/github.js b/controllers/github.js
index 7df3489aef..9fdbaed68b 100644
--- a/controllers/github.js
+++ b/controllers/github.js
@@ -8,6 +8,11 @@ var validator = require('validator');
exports.callback = function (req, res, next) {
var profile = req.user;
+ var email = profile.emails && profile.emails[0] && profile.emails[0].value;
+ if (!email) {
+ return res.status(500)
+ .render('sign/no_github_email');
+ }
User.findOne({githubId: profile.id}, function (err, user) {
if (err) {
return next(err);
@@ -19,12 +24,18 @@ exports.callback = function (req, res, next) {
user.githubAccessToken = profile.accessToken;
// user.loginname = profile.username;
user.avatar = profile._json.avatar_url;
- if (profile.emails[0].value) {
- user.email = profile.emails[0].value;
- }
+ user.email = email || user.email;
+
user.save(function (err) {
if (err) {
+ // 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看
+ if (err.message.indexOf('duplicate key error') !== -1) {
+ if (err.message.indexOf('loginname') !== -1) {
+ return res.status(500)
+ .send('您 GitHub 账号的用户名与之前在 CNodejs 注册的用户名重复了');
+ }
+ }
return next(err);
}
authMiddleWare.gen_session(user, res);
@@ -44,9 +55,10 @@ exports.new = function (req, res, next) {
exports.create = function (req, res, next) {
var profile = req.session.profile;
+
var isnew = req.body.isnew;
- var loginname = validator.trim(req.body.name).toLowerCase();
- var password = validator.trim(req.body.pass);
+ var loginname = validator.trim(req.body.name || '').toLowerCase();
+ var password = validator.trim(req.body.pass || '');
var ep = new eventproxy();
ep.fail(next);
@@ -54,11 +66,17 @@ exports.create = function (req, res, next) {
return res.redirect('/signin');
}
delete req.session.profile;
+
+ var email = profile.emails && profile.emails[0] && profile.emails[0].value;
+ if (!email) {
+ return res.status(500)
+ .render('sign/no_github_email');
+ }
if (isnew) { // 注册新账号
var user = new User({
loginname: profile.username,
pass: profile.accessToken,
- email: profile.emails[0].value,
+ email: email,
avatar: profile._json.avatar_url,
githubId: profile.id,
githubUsername: profile.username,
@@ -70,11 +88,7 @@ exports.create = function (req, res, next) {
if (err) {
// 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看
if (err.message.indexOf('duplicate key error') !== -1) {
- if (err.message.indexOf('users.$email') !== -1) {
- return res.status(500)
- .render('sign/no_github_email');
- }
- if (err.message.indexOf('users.$loginname') !== -1) {
+ if (err.message.indexOf('loginname') !== -1) {
return res.status(500)
.send('您 GitHub 账号的用户名与之前在 CNodejs 注册的用户名重复了');
}
diff --git a/controllers/reply.js b/controllers/reply.js
index f7c965d20c..d41446f654 100644
--- a/controllers/reply.js
+++ b/controllers/reply.js
@@ -16,7 +16,7 @@ exports.add = function (req, res, next) {
var topic_id = req.params.topic_id;
var reply_id = req.body.reply_id;
- var str = validator.trim(content);
+ var str = validator.trim(String(content));
if (str === '') {
return res.renderError('回复内容不能为空!', 422);
}
@@ -30,7 +30,7 @@ exports.add = function (req, res, next) {
// just 404 page
return next();
}
-
+
if (topic.lock) {
return res.status(403).send('此主题已锁定。');
}
@@ -92,11 +92,9 @@ exports.delete = function (req, res, next) {
reply.save();
res.json({status: 'success'});
- if (!reply.reply_id) {
- reply.author.score -= 5;
- reply.author.reply_count -= 1;
- reply.author.save();
- }
+ reply.author.score -= 5;
+ reply.author.reply_count -= 1;
+ reply.author.save();
} else {
res.json({status: 'failed'});
return;
@@ -141,6 +139,7 @@ exports.update = function (req, res, next) {
if (content.trim().length > 0) {
reply.content = content;
+ reply.update_at = new Date();
reply.save(function (err) {
if (err) {
return next(err);
diff --git a/controllers/rss.js b/controllers/rss.js
index ab105582ab..5ef330bf05 100644
--- a/controllers/rss.js
+++ b/controllers/rss.js
@@ -19,8 +19,11 @@ exports.index = function (req, res, next) {
if (!config.debug && rss) {
res.send(rss);
} else {
- var opt = { limit: config.rss.max_rss_items, sort: '-create_at'};
- Topic.getTopicsByQuery({}, opt, function (err, topics) {
+ var opt = {
+ limit: config.rss.max_rss_items,
+ sort: '-create_at',
+ };
+ Topic.getTopicsByQuery({tab: {$nin: ['dev']}}, opt, function (err, topics) {
if (err) {
return next(err);
}
@@ -47,10 +50,14 @@ exports.index = function (req, res, next) {
});
var rssContent = convert('rss', rss_obj);
-
+ rssContent = utf8ForXml(rssContent)
cache.set('rss', rssContent, 60 * 5); // 五分钟
res.send(rssContent);
});
}
}));
};
+
+function utf8ForXml(inputStr) {
+ return inputStr.replace(/[^\x09\x0A\x0D\x20-\xFF\x85\xA0-\uD7FF\uE000-\uFDCF\uFDE0-\uFFFD]/gm, '');
+}
diff --git a/controllers/search.js b/controllers/search.js
index d41c378208..d1e0ec1992 100644
--- a/controllers/search.js
+++ b/controllers/search.js
@@ -1,5 +1,5 @@
exports.index = function (req, res, next) {
var q = req.query.q;
q = encodeURIComponent(q);
- res.redirect('https://www.google.com.hk/#hl=zh-CN&q=site:cnodejs.org+' + q);
+ res.redirect('https://www.google.com.hk/search?q=site:cnodejs.org+' + q);
};
diff --git a/controllers/sign.js b/controllers/sign.js
index 4aef3dab89..9fc076c3ec 100644
--- a/controllers/sign.js
+++ b/controllers/sign.js
@@ -239,8 +239,8 @@ exports.updateSearchPass = function (req, res, next) {
* @param {Function} next
*/
exports.resetPass = function (req, res, next) {
- var key = validator.trim(req.query.key);
- var name = validator.trim(req.query.name);
+ var key = validator.trim(req.query.key || '');
+ var name = validator.trim(req.query.name || '');
User.getUserByNameAndKey(name, key, function (err, user) {
if (!user) {
diff --git a/controllers/site.js b/controllers/site.js
index 3588f54415..4b7b32138d 100644
--- a/controllers/site.js
+++ b/controllers/site.js
@@ -17,6 +17,7 @@ var cache = require('../common/cache');
var xmlbuilder = require('xmlbuilder');
var renderHelper = require('../common/render_helper');
var _ = require('lodash');
+var moment = require('moment');
exports.index = function (req, res, next) {
var page = parseInt(req.query.page, 10) || 1;
@@ -28,13 +29,18 @@ exports.index = function (req, res, next) {
// 取主题
var query = {};
- if (tab && tab !== 'all') {
+ if (!tab || tab === 'all') {
+ query.tab = {$nin: ['job', 'dev']}
+ } else {
if (tab === 'good') {
query.good = true;
} else {
query.tab = tab;
}
}
+ if (!query.good) {
+ query.create_at = {$gte: moment().subtract(1, 'years').toDate()}
+ }
var limit = config.list_topic_count;
var options = { skip: (page - 1) * limit, limit: limit, sort: '-top -last_reply_at'};
@@ -49,10 +55,7 @@ exports.index = function (req, res, next) {
proxy.emit('tops', tops);
} else {
User.getUsersByQuery(
- {'$or': [
- {is_block: {'$exists': false}},
- {is_block: false}
- ]},
+ {is_block: false},
{ limit: 10, sort: '-score'},
proxy.done('tops', function (tops) {
cache.set('tops', tops, 60 * 1);
@@ -69,7 +72,7 @@ exports.index = function (req, res, next) {
proxy.emit('no_reply_topics', no_reply_topics);
} else {
Topic.getTopicsByQuery(
- { reply_count: 0, tab: {$ne: 'job'}},
+ { reply_count: 0, tab: {$nin: ['job', 'dev']}},
{ limit: 5, sort: '-create_at'},
proxy.done('no_reply_topics', function (no_reply_topics) {
cache.set('no_reply_topics', no_reply_topics, 60 * 1);
@@ -80,13 +83,14 @@ exports.index = function (req, res, next) {
// END 取0回复的主题
// 取分页数据
- cache.get('pages', proxy.done(function (pages) {
+ var pagesCacheKey = JSON.stringify(query) + 'pages';
+ cache.get(pagesCacheKey, proxy.done(function (pages) {
if (pages) {
proxy.emit('pages', pages);
} else {
Topic.getCountByQuery(query, proxy.done(function (all_topics_count) {
var pages = Math.ceil(all_topics_count / limit);
- cache.set(JSON.stringify(query) + 'pages', pages, 60 * 1);
+ cache.set(pagesCacheKey, pages, 60 * 1);
proxy.emit('pages', pages);
}));
}
@@ -145,9 +149,5 @@ exports.sitemap = function (req, res, next) {
};
exports.appDownload = function (req, res, next) {
- if (/Android/i.test(req.headers['user-agent'])) {
- res.redirect('http://fir.im/ks4u');
- } else {
- res.redirect('https://itunes.apple.com/cn/app/id954734793');
- }
+ res.redirect('https://github.com/soliury/noder-react-native/blob/master/README.md')
};
diff --git a/controllers/static.js b/controllers/static.js
index 660617bdf9..b1b3f6fe67 100644
--- a/controllers/static.js
+++ b/controllers/static.js
@@ -13,7 +13,9 @@ exports.faq = function (req, res, next) {
};
exports.getstart = function (req, res) {
- res.render('static/getstart');
+ res.render('static/getstart', {
+ pageTitle: 'Node.js 新手入门'
+ });
};
@@ -22,7 +24,7 @@ exports.robots = function (req, res, next) {
res.send(multiline(function () {;
/*
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
-#
+#
# To ban all spiders from the entire site uncomment the next two lines:
# User-Agent: *
# Disallow: /
diff --git a/controllers/topic.js b/controllers/topic.js
index dccac93618..a53081b26e 100644
--- a/controllers/topic.js
+++ b/controllers/topic.js
@@ -18,6 +18,7 @@ var store = require('../common/store');
var config = require('../config');
var _ = require('lodash');
var cache = require('../common/cache');
+var logger = require('../common/logger')
/**
* Topic page
@@ -35,16 +36,20 @@ exports.index = function (req, res, next) {
}
var topic_id = req.params.tid;
+ var currentUser = req.session.user;
+
if (topic_id.length !== 24) {
return res.render404('此话题不存在或已被删除。');
}
- var events = ['topic', 'other_topics', 'no_reply_topics'];
- var ep = EventProxy.create(events, function (topic, other_topics, no_reply_topics) {
+ var events = ['topic', 'other_topics', 'no_reply_topics', 'is_collect'];
+ var ep = EventProxy.create(events,
+ function (topic, other_topics, no_reply_topics, is_collect) {
res.render('topic/index', {
topic: topic,
author_other_topics: other_topics,
no_reply_topics: no_reply_topics,
- is_uped: isUped
+ is_uped: isUped,
+ is_collect: is_collect,
});
});
@@ -52,7 +57,7 @@ exports.index = function (req, res, next) {
Topic.getFullTopic(topic_id, ep.done(function (message, topic, author, replies) {
if (message) {
- ep.unbind();
+ logger.error('getFullTopic error topic_id: ' + topic_id)
return res.renderError(message);
}
@@ -69,17 +74,14 @@ exports.index = function (req, res, next) {
});
allUpCount = _.sortBy(allUpCount, Number).reverse();
- return allUpCount[2] || 0;
+ var threshold = allUpCount[2] || 0;
+ if (threshold < 3) {
+ threshold = 3;
+ }
+ return threshold;
})();
- if (!req.session.user) {
- ep.emit('topic', topic);
- } else {
- TopicCollect.getTopicCollect(req.session.user._id, topic._id, ep.done(function (doc) {
- topic.in_collection = doc;
- ep.emit('topic', topic);
- }));
- }
+ ep.emit('topic', topic);
// get other_topics
var options = { limit: 5, sort: '-last_reply_at'};
@@ -92,7 +94,7 @@ exports.index = function (req, res, next) {
ep.emit('no_reply_topics', no_reply_topics);
} else {
Topic.getTopicsByQuery(
- { reply_count: 0, tab: {$ne: 'job'}},
+ { reply_count: 0, tab: {$nin: ['job', 'dev']}},
{ limit: 5, sort: '-create_at'},
ep.done('no_reply_topics', function (no_reply_topics) {
cache.set('no_reply_topics', no_reply_topics, 60 * 1);
@@ -101,6 +103,12 @@ exports.index = function (req, res, next) {
}
}));
}));
+
+ if (!currentUser) {
+ ep.emit('is_collect', null);
+ } else {
+ TopicCollect.getTopicCollect(currentUser._id, topic_id, ep.done('is_collect'))
+ }
};
exports.create = function (req, res, next) {
@@ -112,9 +120,7 @@ exports.create = function (req, res, next) {
exports.put = function (req, res, next) {
var title = validator.trim(req.body.title);
- title = validator.escape(title);
var tab = validator.trim(req.body.tab);
- tab = validator.escape(tab);
var content = validator.trim(req.body.t_content);
// 得到所有的 tab, e.g. ['ask', 'share', ..]
@@ -207,9 +213,7 @@ exports.update = function (req, res, next) {
if (topic.author_id.equals(req.session.user._id) || req.session.user.is_admin) {
title = validator.trim(title);
- title = validator.escape(title);
tab = validator.trim(tab);
- tab = validator.escape(tab);
content = validator.trim(content);
// 验证
@@ -262,7 +266,7 @@ exports.delete = function (req, res, next) {
var topic_id = req.params.tid;
- Topic.getTopic(topic_id, function (err, topic) {
+ Topic.getFullTopic(topic_id, function (err, err_msg, topic, author, replies) {
if (err) {
return res.send({ success: false, message: err.message });
}
@@ -274,6 +278,10 @@ exports.delete = function (req, res, next) {
res.status(422);
return res.send({ success: false, message: '此话题不存在或已被删除。' });
}
+ author.score -= 5;
+ author.topic_count -= 1;
+ author.save();
+
topic.deleted = true;
topic.save(function (err) {
if (err) {
@@ -362,6 +370,7 @@ exports.lock = function (req, res, next) {
// 收藏主题
exports.collect = function (req, res, next) {
var topic_id = req.body.topic_id;
+
Topic.getTopic(topic_id, function (err, topic) {
if (err) {
return next(err);
@@ -375,7 +384,7 @@ exports.collect = function (req, res, next) {
return next(err);
}
if (doc) {
- res.json({status: 'success'});
+ res.json({status: 'failed'});
return;
}
@@ -409,39 +418,56 @@ exports.de_collect = function (req, res, next) {
if (!topic) {
res.json({status: 'failed'});
}
- TopicCollect.remove(req.session.user._id, topic._id, function (err) {
+ TopicCollect.remove(req.session.user._id, topic._id, function (err, removeResult) {
if (err) {
return next(err);
}
- res.json({status: 'success'});
- });
-
- User.getUserById(req.session.user._id, function (err, user) {
- if (err) {
- return next(err);
+ if (removeResult.n == 0) {
+ return res.json({status: 'failed'})
}
- user.collect_topic_count -= 1;
- user.save();
- });
- topic.collect_count -= 1;
- topic.save();
+ User.getUserById(req.session.user._id, function (err, user) {
+ if (err) {
+ return next(err);
+ }
+ user.collect_topic_count -= 1;
+ req.session.user = user;
+ user.save();
+ });
+
+ topic.collect_count -= 1;
+ topic.save();
- req.session.user.collect_topic_count -= 1;
+ res.json({status: 'success'});
+ });
});
};
exports.upload = function (req, res, next) {
+ var isFileLimit = false;
req.busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
+ file.on('limit', function () {
+ isFileLimit = true;
+
+ res.json({
+ success: false,
+ msg: 'File size too large. Max is ' + config.file_limit
+ })
+ });
+
store.upload(file, {filename: filename}, function (err, result) {
if (err) {
return next(err);
}
+ if (isFileLimit) {
+ return;
+ }
res.json({
success: true,
url: result.url,
});
});
+
});
req.pipe(req.busboy);
diff --git a/controllers/user.js b/controllers/user.js
index f6ff52791d..b897a67373 100644
--- a/controllers/user.js
+++ b/controllers/user.js
@@ -10,8 +10,8 @@ var tools = require('../common/tools');
var config = require('../config');
var EventProxy = require('eventproxy');
var validator = require('validator');
-var utility = require('utility');
var _ = require('lodash');
+var uuid = require('node-uuid')
exports.index = function (req, res, next) {
var user_name = req.params.name;
@@ -55,15 +55,20 @@ exports.index = function (req, res, next) {
Reply.getRepliesByAuthorId(user._id, {limit: 20, sort: '-create_at'},
proxy.done(function (replies) {
- var topic_ids = [];
- for (var i = 0; i < replies.length; i++) {
- if (topic_ids.indexOf(replies[i].topic_id.toString()) < 0) {
- topic_ids.push(replies[i].topic_id.toString());
- }
- }
+
+ var topic_ids = replies.map(function (reply) {
+ return reply.topic_id.toString()
+ })
+ topic_ids = _.uniq(topic_ids).slice(0, 5); // 只显示最近5条
+
var query = {_id: {'$in': topic_ids}};
- var opt = {limit: 5, sort: '-create_at'};
- Topic.getTopicsByQuery(query, opt, proxy.done('recent_replies'));
+ var opt = {};
+ Topic.getTopicsByQuery(query, opt, proxy.done('recent_replies', function (recent_replies) {
+ recent_replies = _.sortBy(recent_replies, function (topic) {
+ return topic_ids.indexOf(topic._id.toString())
+ })
+ return recent_replies;
+ }));
}));
});
};
@@ -118,13 +123,9 @@ exports.setting = function (req, res, next) {
var action = req.body.action;
if (action === 'change_setting') {
var url = validator.trim(req.body.url);
- url = validator.escape(url);
var location = validator.trim(req.body.location);
- location = validator.escape(location);
var weibo = validator.trim(req.body.weibo);
- weibo = validator.escape(weibo);
var signature = validator.trim(req.body.signature);
- signature = validator.escape(signature);
User.getUserById(req.session.user._id, ep.done(function (user) {
user.url = url;
@@ -189,15 +190,15 @@ exports.toggleStar = function (req, res, next) {
exports.listCollectedTopics = function (req, res, next) {
var name = req.params.name;
+ var page = Number(req.query.page) || 1;
+ var limit = config.list_topic_count;
+
User.getUserByLoginName(name, function (err, user) {
if (err || !user) {
return next(err);
}
-
- var page = Number(req.query.page) || 1;
- var limit = config.list_topic_count;
-
- var render = function (topics, pages) {
+ var pages = Math.ceil(user.collect_topic_count/limit);
+ var render = function (topics) {
res.render('user/collect_topics', {
topics: topics,
current_page: page,
@@ -206,24 +207,25 @@ exports.listCollectedTopics = function (req, res, next) {
});
};
- var proxy = EventProxy.create('topics', 'pages', render);
+ var proxy = EventProxy.create('topics', render);
proxy.fail(next);
- TopicCollect.getTopicCollectsByUserId(user._id, proxy.done(function (docs) {
- var ids = [];
- for (var i = 0; i < docs.length; i++) {
- ids.push(docs[i].topic_id);
- }
+ var opt = {
+ skip: (page - 1) * limit,
+ limit: limit,
+ };
+
+ TopicCollect.getTopicCollectsByUserId(user._id, opt, proxy.done(function (docs) {
+ var ids = docs.map(function (doc) {
+ return String(doc.topic_id)
+ })
var query = { _id: { '$in': ids } };
- var opt = {
- skip: (page - 1) * limit,
- limit: limit,
- sort: '-create_at'
- };
- Topic.getTopicsByQuery(query, opt, proxy.done('topics'));
- Topic.getCountByQuery(query, proxy.done(function (all_topics_count) {
- var pages = Math.ceil(all_topics_count / limit);
- proxy.emit('pages', pages);
+
+ Topic.getTopicsByQuery(query, {}, proxy.done('topics', function (topics) {
+ topics = _.sortBy(topics, function (topic) {
+ return ids.indexOf(String(topic._id))
+ })
+ return topics
}));
}));
});
@@ -231,10 +233,7 @@ exports.listCollectedTopics = function (req, res, next) {
exports.top100 = function (req, res, next) {
var opt = {limit: 100, sort: '-score'};
- User.getUsersByQuery({'$or': [
- {is_block: {'$exists': false}},
- {is_block: false},
- ]}, opt, function (err, tops) {
+ User.getUsersByQuery({is_block: false}, opt, function (err, tops) {
if (err) {
return next(err);
}
@@ -308,11 +307,17 @@ exports.listReplies = function (req, res, next) {
Reply.getRepliesByAuthorId(user._id, opt, proxy.done(function (replies) {
// 获取所有有评论的主题
var topic_ids = replies.map(function (reply) {
- return reply.topic_id;
+ return reply.topic_id.toString();
});
topic_ids = _.uniq(topic_ids);
+
var query = {'_id': {'$in': topic_ids}};
- Topic.getTopicsByQuery(query, {}, proxy.done('topics'));
+ Topic.getTopicsByQuery(query, {}, proxy.done('topics', function (topics) {
+ topics = _.sortBy(topics, function (topic) {
+ return topic_ids.indexOf(topic._id.toString())
+ })
+ return topics;
+ }));
}));
Reply.getCountByAuthorId(user._id, proxy.done('pages', function (count) {
@@ -366,10 +371,24 @@ exports.deleteAll = function (req, res, next) {
res.json({status: 'success'});
});
// 删除主题
- TopicModel.update({author_id: user._id}, {$set: {deleted: true}}, {multi: true}, ep.done('del_topics'));
+ TopicModel.updateMany({author_id: user._id}, {$set: {deleted: true}}, ep.done('del_topics'));
// 删除评论
- ReplyModel.update({author_id: user._id}, {$set: {deleted: true}}, {multi: true}, ep.done('del_replys'));
+ ReplyModel.updateMany({author_id: user._id}, {$set: {deleted: true}}, ep.done('del_replys'));
// 点赞数也全部干掉
- ReplyModel.update({}, {$pull: {'ups': user._id}}, {multi: true}, ep.done('del_ups'));
+ ReplyModel.updateMany({}, {$pull: {'ups': user._id}}, ep.done('del_ups'));
}));
};
+
+exports.refreshToken = function (req, res, next) {
+ var user_id = req.session.user._id;
+
+ var ep = EventProxy.create();
+ ep.fail(next);
+
+ User.getUserById(user_id, ep.done(function (user) {
+ user.accessToken = uuid.v4();
+ user.save(ep.done(function () {
+ res.json({status: 'success', accessToken: user.accessToken});
+ }));
+ }));
+};
\ No newline at end of file
diff --git a/logs/.gitkeep b/logs/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/middlewares/auth.js b/middlewares/auth.js
index 1e9e06cacd..3615066859 100644
--- a/middlewares/auth.js
+++ b/middlewares/auth.js
@@ -24,7 +24,7 @@ exports.adminRequired = function (req, res, next) {
* 需要登录
*/
exports.userRequired = function (req, res, next) {
- if (!req.session || !req.session.user) {
+ if (!req.session || !req.session.user || !req.session.user._id) {
return res.status(403).send('forbidden!');
}
diff --git a/middlewares/limit.js b/middlewares/limit.js
index 0ebdd9cbc5..8556ef2273 100644
--- a/middlewares/limit.js
+++ b/middlewares/limit.js
@@ -5,7 +5,10 @@ var moment = require('moment');
var SEPARATOR = '^_^@T_T';
var makePerDayLimiter = function (identityName, identityFn) {
- return function (name, limitCount) {
+ return function (name, limitCount, options) {
+ /*
+ options.showJson = true 表示调用来自API并返回结构化数据;否则表示调用来自前段并渲染错误页面
+ */
return function (req, res, next) {
var identity = identityFn(req);
var YYYYMMDD = moment().format('YYYYMMDD');
@@ -23,7 +26,12 @@ var makePerDayLimiter = function (identityName, identityFn) {
res.set('X-RateLimit-Remaining', limitCount - count);
next();
} else {
- res.send('ratelimit forbidden. limit is ' + limitCount + ' per day.');
+ res.status(403);
+ if (options.showJson) {
+ res.send({success: false, error_msg: '频率限制:当前操作每天可以进行 ' + limitCount + ' 次'});
+ } else {
+ res.render('notify/notify', { error: '频率限制:当前操作每天可以进行 ' + limitCount + ' 次'});
+ }
}
});
};
@@ -35,5 +43,9 @@ exports.peruserperday = makePerDayLimiter('peruserperday', function (req) {
});
exports.peripperday = makePerDayLimiter('peripperday', function (req) {
- return req.ip;
+ var realIP = req.get('x-real-ip');
+ if (!realIP && !config.debug) {
+ throw new Error('should provide `x-real-ip` header')
+ }
+ return realIP;
});
diff --git a/middlewares/mongoose_log.js b/middlewares/mongoose_log.js
index 5717ddb567..67f5af8a6c 100644
--- a/middlewares/mongoose_log.js
+++ b/middlewares/mongoose_log.js
@@ -1,16 +1,21 @@
var mongoose = require('mongoose');
var logger = require('../common/logger');
+var config = require('../config');
-var traceMQuery = function (method, info, query) {
- return function (err, result, millis) {
- var infos = [];
- infos.push(query._collection.collection.name + "." + method.blue);
- infos.push(JSON.stringify(info));
- infos.push((millis + 'ms').green);
+if (config.debug) {
+ var traceMQuery = function (method, info, query) {
+ return function (err, result, millis) {
+ if (err) {
+ logger.error('traceMQuery error:', err)
+ }
+ var infos = [];
+ infos.push(query._collection.collection.name + "." + method.blue);
+ infos.push(JSON.stringify(info));
+ infos.push((millis + 'ms').green);
- // var duration = (new Date()) - t;
- logger.debug("MONGO".magenta, infos.join(' '));
+ logger.debug("MONGO".magenta, infos.join(' '));
+ };
};
-};
-mongoose.Mongoose.prototype.mquery.setGlobalTraceFunction(traceMQuery);
+ mongoose.Mongoose.prototype.mquery.setGlobalTraceFunction(traceMQuery);
+}
diff --git a/middlewares/proxy.js b/middlewares/proxy.js
index 26abfb3276..a2102d1005 100644
--- a/middlewares/proxy.js
+++ b/middlewares/proxy.js
@@ -1,5 +1,7 @@
var urllib = require('url');
var request = require('request');
+var logger = require('../common/logger')
+var _ = require('lodash')
var ALLOW_HOSTNAME = [
@@ -16,15 +18,13 @@ exports.proxy = function (req, res, next) {
request.get({
url: url,
- headers: {
- 'If-Modified-Since': req.header('If-Modified-Since') || ''
- }
+ headers: _.omit(req.headers, ['cookie', 'refer']),
})
.on('response', function (response) {
res.set(response.headers);
})
.on('error', function (err) {
- console.error(err);
+ logger.error(err);
})
.pipe(res);
};
diff --git a/middlewares/request_log.js b/middlewares/request_log.js
index 334327d40e..de551f27ff 100644
--- a/middlewares/request_log.js
+++ b/middlewares/request_log.js
@@ -1,22 +1,22 @@
var logger = require('../common/logger');
-module.exports = function (req, res, next) {
+var ignore = /^\/(public|agent)/;
+
+exports = module.exports = function (req, res, next) {
// Assets do not out log.
- if (exports.ignore.test(req.url)) {
+ if (ignore.test(req.url)) {
next();
return;
}
var t = new Date();
- logger.log('\n\nStarted', t.toISOString(), req.method, req.url, req.ip);
+ logger.info('\n\nStarted', t.toISOString(), req.method, req.url, req.ip);
res.on('finish', function () {
var duration = ((new Date()) - t);
- logger.log('Completed', res.statusCode, ('(' + duration + 'ms)').green);
+ logger.info('Completed', res.statusCode, ('(' + duration + 'ms)').green);
});
next();
};
-
-exports.ignore = /^\/(public|agent)/;
diff --git a/models/base_model.js b/models/base_model.js
index 7671c53a28..25e4caf0bd 100644
--- a/models/base_model.js
+++ b/models/base_model.js
@@ -9,7 +9,7 @@ module.exports = function (schema) {
return tools.formatDate(this.create_at, true);
};
- schema.methods.updated_at_ago = function () {
- return tools.formatDate(this.create_at, true);
+ schema.methods.update_at_ago = function () {
+ return tools.formatDate(this.update_at, true);
};
};
diff --git a/models/index.js b/models/index.js
index 8ea31cb675..b4c478eb6d 100644
--- a/models/index.js
+++ b/models/index.js
@@ -1,9 +1,14 @@
var mongoose = require('mongoose');
var config = require('../config');
+var logger = require('../common/logger')
-mongoose.connect(config.db, function (err) {
+mongoose.connect(config.db, {
+ poolSize: 20,
+ useCreateIndex: true,
+ useNewUrlParser: true
+}, function (err) {
if (err) {
- console.error('connect to %s error: ', config.db, err.message);
+ logger.error('connect to %s error: ', config.db, err.message);
process.exit(1);
}
});
@@ -20,4 +25,3 @@ exports.Topic = mongoose.model('Topic');
exports.Reply = mongoose.model('Reply');
exports.TopicCollect = mongoose.model('TopicCollect');
exports.Message = mongoose.model('Message');
-
diff --git a/models/topic.js b/models/topic.js
index 037b1bd3ee..baecc34840 100644
--- a/models/topic.js
+++ b/models/topic.js
@@ -27,7 +27,6 @@ var TopicSchema = new Schema({
TopicSchema.plugin(BaseModel);
TopicSchema.index({create_at: -1});
TopicSchema.index({top: -1, last_reply_at: -1});
-TopicSchema.index({last_reply_at: -1});
TopicSchema.index({author_id: 1, create_at: -1});
TopicSchema.virtual('tabName').get(function () {
diff --git a/models/topic_collect.js b/models/topic_collect.js
index 3d233ed346..0850dc050c 100644
--- a/models/topic_collect.js
+++ b/models/topic_collect.js
@@ -10,4 +10,6 @@ var TopicCollectSchema = new Schema({
});
TopicCollectSchema.plugin(BaseModel);
+TopicCollectSchema.index({user_id: 1, topic_id: 1}, {unique: true});
+
mongoose.model('TopicCollect', TopicCollectSchema);
diff --git a/models/user.js b/models/user.js
index c9a4ea8c16..c5c66db372 100644
--- a/models/user.js
+++ b/models/user.js
@@ -1,7 +1,9 @@
var mongoose = require('mongoose');
var BaseModel = require("./base_model");
+var renderHelper = require('../common/render_helper');
var Schema = mongoose.Schema;
var utility = require('utility');
+var _ = require('lodash');
var UserSchema = new Schema({
name: { type: String},
@@ -48,21 +50,18 @@ UserSchema.virtual('avatar_url').get(function () {
var url = this.avatar || ('https://gravatar.com/avatar/' + utility.md5(this.email.toLowerCase()) + '?size=48');
// www.gravatar.com 被墙
- // url = url.replace('//www.gravatar.com', '//gravatar.com');
+ url = url.replace('www.gravatar.com', 'gravatar.com');
// 让协议自适应 protocol,使用 `//` 开头
- // if (url.indexOf('http:') === 0) {
- // url = url.slice(5);
- // }
+ if (url.indexOf('http:') === 0) {
+ url = url.slice(5);
+ }
// 如果是 github 的头像,则限制大小
if (url.indexOf('githubusercontent') !== -1) {
url += '&s=120';
}
- // 通过服务器代理访问
- url = '/agent?url=' + encodeURIComponent(url);
-
return url;
});
@@ -77,4 +76,10 @@ UserSchema.index({score: -1});
UserSchema.index({githubId: 1});
UserSchema.index({accessToken: 1});
+UserSchema.pre('save', function(next){
+ var now = new Date();
+ this.update_at = now;
+ next();
+});
+
mongoose.model('User', UserSchema);
diff --git a/newrelic.js b/oneapm.js
similarity index 58%
rename from newrelic.js
rename to oneapm.js
index 83e3e0e87d..7471c3e1c7 100644
--- a/newrelic.js
+++ b/oneapm.js
@@ -1,25 +1,30 @@
-var config = require('./config');
/**
- * New Relic agent configuration.
+ * OneAPM agent configuration.
*
* See lib/config.defaults.js in the agent distribution for a more complete
* description of configuration variables and their potential values.
*/
+
+var config = require('./config');
+
exports.config = {
/**
* Array of application names.
*/
- app_name: [config.name],
+ app_name : [config.name],
/**
- * Your New Relic license key.
+ * Your OneAPM license key.
*/
- license_key: config.newrelic_key,
- logging: {
+ license_key : config.oneapm_key,
+ logging : {
/**
- * Level at which to log. 'trace' is most useful to New Relic when diagnosing
+ * Level at which to log. 'trace' is most useful to OneAPM when diagnosing
* issues with the agent, 'info' and higher will impose the least overhead on
* production applications.
*/
- level: 'info'
+ level : 'info'
+ },
+ transaction_events: {
+ enabled: true
}
};
diff --git a/package.json b/package.json
index 75f937a235..a7ab0e4f9e 100644
--- a/package.json
+++ b/package.json
@@ -1,72 +1,73 @@
{
"name": "nodeclub",
- "version": "2.0.0",
+ "version": "2.1.1",
"private": true,
"main": "app.js",
"description": "A Node.js bbs using MongoDB",
"repository": "https://github.com/cnodejs/nodeclub",
- "customHost": [
- "cnodejs.org",
- "club.cnodejs.org"
- ],
- "engines": {
- "node": "0.12.x"
- },
"dependencies": {
- "async": "0.9.0",
- "bcrypt": "0.8.3",
- "body-parser": "1.9.2",
- "compression": "1.2.0",
- "connect-busboy": "0.0.1",
- "connect-mongo": "0.4.1",
- "connect-redis": "2.2.0",
- "cookie-parser": "1.3.3",
- "cors": "2.5.0",
- "csurf": "1.6.2",
- "data2xml": "0.8.0",
- "ejs-mate": "2.0.0",
- "eventproxy": "0.3.1",
- "express": "4.9.5",
- "express-session": "1.9.1",
- "ioredis": "1.3.6",
- "jpush-sdk": "3.2.0",
- "loader": "0.1.4",
- "lodash": "3.6.0",
- "markdown-it": "3.0.3",
- "memory-cache": "0.0.5",
- "method-override": "1.0.2",
- "moment": "2.9.0",
- "mongoose": "4.0.3",
- "multiline": "1.0.1",
- "newrelic": "1.19.2",
- "node-uuid": "1.4.1",
- "nodemailer": "0.3.43",
- "passport": "0.1.18",
- "passport-github": "0.1.5",
- "pm2": "0.12.13",
- "qn": "1.0.1",
+ "async": "1.5.2",
+ "bcryptjs": "2.3.0",
+ "body-parser": "1.17.1",
+ "bytes": "^2.2.0",
+ "colors": "1.1.2",
+ "compression": "1.7.0",
+ "connect-busboy": "0.0.2",
+ "connect-redis": "3.0.2",
+ "cookie-parser": "1.4.1",
+ "cors": "2.7.1",
+ "csurf": "1.8.3",
+ "data2xml": "1.2.4",
+ "ejs-mate": "2.3.0",
+ "eventproxy": "1.0.0",
+ "express": "4.16.0",
+ "express-session": "1.12.1",
+ "helmet": "1.3.0",
+ "ioredis": "2.0.0",
+ "jpush-sdk": "3.3.2",
+ "loader-builder": "2.4.1",
+ "loader": "2.1.1",
+ "lodash": "4.17.21",
+ "log4js": "^0.6.29",
+ "markdown-it": "6.0.0",
+ "memory-cache": "0.1.4",
+ "method-override": "2.3.5",
+ "moment": "2.15.2",
+ "mongoose": "5.3.9",
+ "multiline": "1.0.2",
+ "node-uuid": "1.4.7",
+ "nodemailer": "2.3.0",
+ "nodemailer-smtp-transport": "2.4.0",
+ "oneapm": "1.2.20",
+ "passport": "0.3.2",
+ "passport-github": "1.1.0",
+ "pm2": "*",
+ "qn": "1.3.0",
"ready": "0.1.1",
- "request": "2.54.0",
- "response-time": "2.2.0",
- "superagent": "1.1.0",
- "utility": "1.0.0",
- "validator": "3.22.1",
- "xmlbuilder": "2.5.0",
- "xss": "0.1.15",
- "colors" : "1.1.0"
+ "request": "2.81.0",
+ "response-time": "2.3.1",
+ "superagent": "2.0.0",
+ "utility": "1.6.0",
+ "validator": "5.1.0",
+ "xmlbuilder": "7.0.0",
+ "xss": "0.2.10",
+ "snyk": "^1.88.0"
},
"devDependencies": {
- "coveralls": "2.11.2",
- "errorhandler": "1.2.2",
- "istanbul": "0.3.2",
- "mm": "0.2.1",
- "mocha": "2.0.1",
- "nock": "1.4.0",
+ "errorhandler": "1.4.3",
+ "istanbul": "0.4.2",
+ "loader-connect": "1.0.1",
+ "mm": "1.3.5",
+ "mocha": "2.4.5",
+ "nock": "7.5.0",
"pedding": "1.0.0",
- "should": "4.1.0",
- "supertest": "0.14.0"
+ "should": "8.3.0",
+ "supertest": "1.2.0"
},
"scripts": {
- "test": "make test"
- }
+ "test": "make test",
+ "snyk-protect": "snyk protect",
+ "prepare": "npm run snyk-protect"
+ },
+ "snyk": true
}
diff --git a/proxy/message.js b/proxy/message.js
index 84c8c51f0b..36c59e2bdb 100644
--- a/proxy/message.js
+++ b/proxy/message.js
@@ -17,7 +17,7 @@ var Reply = require('./reply');
* @param {Function} callback 获取消息数量
*/
exports.getMessagesCount = function (id, callback) {
- Message.count({master_id: id, has_read: false}, callback);
+ Message.countDocuments({master_id: id, has_read: false}, callback);
};
@@ -54,6 +54,8 @@ var getMessageRelations = exports.getMessageRelations = function (message, callb
User.getUserById(message.author_id, proxy.done('author'));
Topic.getTopicById(message.topic_id, proxy.done('topic'));
Reply.getReplyById(message.reply_id, proxy.done('reply'));
+ } else {
+ return callback(null, {is_invalid: true});
}
};
@@ -98,5 +100,18 @@ exports.updateMessagesToRead = function (userId, messages, callback) {
});
var query = { master_id: userId, _id: { $in: ids } };
- Message.update(query, { $set: { has_read: true } }, { multi: true }).exec(callback);
+ Message.updateMany(query, { $set: { has_read: true } }).exec(callback);
+};
+
+
+/**
+ * 将单个消息设置成已读
+ */
+exports.updateOneMessageToRead = function (msg_id, callback) {
+ callback = callback || _.noop;
+ if (!msg_id) {
+ return callback();
+ }
+ var query = { _id: msg_id };
+ Message.updateMany(query, { $set: { has_read: true } }).exec(callback);
};
diff --git a/proxy/reply.js b/proxy/reply.js
index 5e95d45411..ad2b6002a2 100644
--- a/proxy/reply.js
+++ b/proxy/reply.js
@@ -145,5 +145,5 @@ exports.getRepliesByAuthorId = function (authorId, opt, callback) {
// 通过 author_id 获取回复总数
exports.getCountByAuthorId = function (authorId, callback) {
- Reply.count({author_id: authorId}, callback);
+ Reply.countDocuments({author_id: authorId}, callback);
};
diff --git a/proxy/topic.js b/proxy/topic.js
index 2438c64b33..d1fc6675b5 100644
--- a/proxy/topic.js
+++ b/proxy/topic.js
@@ -58,7 +58,7 @@ exports.getTopicById = function (id, callback) {
* @param {Function} callback 回调函数
*/
exports.getCountByQuery = function (query, callback) {
- Topic.count(query, callback);
+ Topic.countDocuments(query, callback);
};
/**
@@ -133,7 +133,7 @@ exports.getFullTopic = function (id, callback) {
})
.fail(callback);
- Topic.findOne({_id: id}, proxy.done(function (topic) {
+ Topic.findOne({_id: id, deleted: false}, proxy.done(function (topic) {
if (!topic) {
proxy.unbind();
return callback(null, '此话题不存在或已被删除。');
diff --git a/proxy/topic_collect.js b/proxy/topic_collect.js
index 1d763242fd..cfa17d4d4d 100644
--- a/proxy/topic_collect.js
+++ b/proxy/topic_collect.js
@@ -1,11 +1,14 @@
var TopicCollect = require('../models').TopicCollect;
+var _ = require('lodash')
exports.getTopicCollect = function (userId, topicId, callback) {
TopicCollect.findOne({user_id: userId, topic_id: topicId}, callback);
};
-exports.getTopicCollectsByUserId = function (userId, callback) {
- TopicCollect.find({user_id: userId}, callback);
+exports.getTopicCollectsByUserId = function (userId, opt, callback) {
+ var defaultOpt = {sort: '-create_at'};
+ opt = _.assign(defaultOpt, opt)
+ TopicCollect.find({user_id: userId}, '', opt, callback);
};
exports.newAndSave = function (userId, topicId, callback) {
@@ -16,6 +19,6 @@ exports.newAndSave = function (userId, topicId, callback) {
};
exports.remove = function (userId, topicId, callback) {
- TopicCollect.remove({user_id: userId, topic_id: topicId}, callback);
+ TopicCollect.deleteOne({user_id: userId, topic_id: topicId}, callback);
};
diff --git a/proxy/user.js b/proxy/user.js
index d4ab0b9794..58b8a0a132 100644
--- a/proxy/user.js
+++ b/proxy/user.js
@@ -27,7 +27,7 @@ exports.getUsersByNames = function (names, callback) {
* @param {Function} callback 回调函数
*/
exports.getUserByLoginName = function (loginName, callback) {
- User.findOne({'loginname': loginName}, callback);
+ User.findOne({'loginname': new RegExp('^'+loginName+'$', "i")}, callback);
};
/**
@@ -39,6 +39,9 @@ exports.getUserByLoginName = function (loginName, callback) {
* @param {Function} callback 回调函数
*/
exports.getUserById = function (id, callback) {
+ if (!id) {
+ return callback();
+ }
User.findOne({_id: id}, callback);
};
diff --git a/public/javascripts/main.js b/public/javascripts/main.js
index a62d7da679..6abe2fc380 100644
--- a/public/javascripts/main.js
+++ b/public/javascripts/main.js
@@ -1,11 +1,21 @@
$(document).ready(function () {
+ var windowHeight = $(window).height();
var $backtotop = $('#backtotop');
- var top = $(window).height() - $backtotop.height() - 200;
+ var top = windowHeight - $backtotop.height() - 200;
+
function moveBacktotop() {
$backtotop.css({ top: top, right: 0});
}
+ function footerFixBottom() {
+ if($(document.body).height() < windowHeight){
+ $("#footer").addClass('fix-bottom');
+ }else{
+ $("#footer").removeClass('fix-bottom');
+ }
+ }
+
$backtotop.click(function () {
$('html,body').animate({ scrollTop: 0 });
return false;
@@ -20,7 +30,9 @@ $(document).ready(function () {
});
moveBacktotop();
+ footerFixBottom();
$(window).resize(moveBacktotop);
+ $(window).resize(footerFixBottom);
$('.topic_content a,.reply_content a').attr('target', '_blank');
diff --git a/public/javascripts/responsive.js b/public/javascripts/responsive.js
index 3a62769729..a5f60ba61f 100644
--- a/public/javascripts/responsive.js
+++ b/public/javascripts/responsive.js
@@ -21,6 +21,7 @@ $(document).ready(function () {
$main.height(sidebarHeight);
}
$sidebarMask[isShow ? 'fadeOut' : 'fadeIn']().height($('body').height());
+ $sidebar[isShow ? 'hide' : 'show']()
},
touchstart = function (e) {
var touchs = e.targetTouches;
@@ -67,4 +68,4 @@ $(document).ready(function () {
$responsiveBtn.trigger('click');
});
-});
\ No newline at end of file
+});
diff --git a/public/libs/editor/ext.js b/public/libs/editor/ext.js
index 38a1eb08cf..b0a94b033d 100644
--- a/public/libs/editor/ext.js
+++ b/public/libs/editor/ext.js
@@ -1,4 +1,57 @@
(function(Editor, markdownit, WebUploader){
+
+ function _replaceSelection(cm, active, start, end) {
+ var text;
+ var startPoint = cm.getCursor('start');
+ var endPoint = cm.getCursor('end');
+ var end = end || '';
+ if (active) {
+ text = cm.getLine(startPoint.line);
+ start = text.slice(0, startPoint.ch);
+ end = text.slice(startPoint.ch);
+ cm.setLine(startPoint.line, start + end);
+ } else {
+ text = cm.getSelection();
+ cm.replaceSelection(start + text + end);
+
+ startPoint.ch += start.length;
+ endPoint.ch += start.length;
+ }
+ cm.setSelection(startPoint, endPoint);
+ cm.focus();
+ }
+
+ /**
+ * The state of CodeMirror at the given position.
+ */
+ function getState(cm, pos) {
+ pos = pos || cm.getCursor('start');
+ var stat = cm.getTokenAt(pos);
+ if (!stat.type) return {};
+
+ var types = stat.type.split(' ');
+
+ var ret = {}, data, text;
+ for (var i = 0; i < types.length; i++) {
+ data = types[i];
+ if (data === 'strong') {
+ ret.bold = true;
+ } else if (data === 'variable-2') {
+ text = cm.getLine(pos.line);
+ if (/^\s*\d+\.\s/.test(text)) {
+ ret['ordered-list'] = true;
+ } else {
+ ret['unordered-list'] = true;
+ }
+ } else if (data === 'atom') {
+ ret.quote = true;
+ } else if (data === 'em') {
+ ret.italic = true;
+ }
+ }
+ return ret;
+ }
+
// Set default options
var md = new markdownit();
@@ -27,14 +80,14 @@
var $body = $('body');
- //添加连接工具
+ //添加链接工具
var ToolLink = function(){
var self = this;
this.$win = $([
'',
'',
- '',
+ '',
'',
'',
'',
@@ -66,7 +119,10 @@
var link = $el.find('[name=link]').val();
self.$win.modal('hide');
- self.editor.push(' ['+ title +']('+ link +')');
+
+ var cm = self.editor.codemirror;
+ var stat = getState(cm);
+ _replaceSelection(cm, stat.link, '['+ title +']('+ link +')');
$el.find('[name=title]').val('');
$el.find('[name=link]').val('http://');
@@ -124,7 +180,7 @@
paste: document.body,
dnd: this.$upload[0],
auto: true,
- fileSingleSizeLimit: 2 * 1024 * 1024,
+ fileSingleSizeLimit: 1 * 1024 * 1024,
//sendAsBinary: true,
// 只允许选择图片文件。
accept: {
@@ -149,7 +205,11 @@
this.uploader.on('uploadSuccess', function(file, res){
if(res.success){
self.$win.modal('hide');
- self.editor.push(' ');
+
+ var cm = self.editor.codemirror;
+ var stat = getState(cm);
+ _replaceSelection(cm, stat.image, '');
+
}
else{
self.removeFile();
@@ -167,7 +227,7 @@
switch(type){
case 'Q_EXCEED_SIZE_LIMIT':
case 'F_EXCEED_SIZE':
- self.showError('文件太大了, 不能超过2M');
+ self.showError('文件太大了, 不能超过1MB');
break;
case 'Q_TYPE_DENIED':
self.showError('只能上传图片');
diff --git a/public/stylesheets/common.css b/public/stylesheets/common.css
index 536a175d7e..b3a7a9c263 100644
--- a/public/stylesheets/common.css
+++ b/public/stylesheets/common.css
@@ -48,6 +48,9 @@ div pre.prettyprint {
margin: 20px -10px;
border-width: 1px 0px;
background: #f7f7f7;
+ -o-tab-size: 4;
+ -moz-tab-size: 4;
+ tab-size: 4;
}
form {
diff --git a/public/stylesheets/responsive.css b/public/stylesheets/responsive.css
index ae96fe3a81..1e4bf74574 100644
--- a/public/stylesheets/responsive.css
+++ b/public/stylesheets/responsive.css
@@ -57,7 +57,7 @@
}
#main {
- overflow: hidden;
+ /*overflow: hidden;*/
margin: 20px auto;
min-height: 0;
}
@@ -80,6 +80,7 @@
-ms-transition: .3s right;
-o-transition: .3s right;
transition: .3s right;
+ display: none;
}
#content .topic_title {
@@ -164,4 +165,3 @@
margin-top: 0;
}
}
-
diff --git a/public/stylesheets/style.less b/public/stylesheets/style.less
index f2436cbd16..f5deadcf09 100644
--- a/public/stylesheets/style.less
+++ b/public/stylesheets/style.less
@@ -6,7 +6,7 @@
/* base */
body {
background-color: @gray1;
- font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti !important;
+ font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", Tahoma, "Hiragino Sans GB", STHeiti, sans-serif !important;
}
#main {
@@ -54,6 +54,7 @@ body {
#content .changes {
font-size: 12px;
color: #838383;
+ overflow: hidden;
span:before {
content: "•"
@@ -97,6 +98,11 @@ body {
clear: both;
position: relative;
background: white;
+ &.fix-bottom{
+ position: fixed;
+ bottom: 0;
+ width:100%;
+ }
}
#footer_main {
@@ -545,6 +551,13 @@ a.user_avatar:hover {
font-weight: bold;
}
+.reply_by_author {
+ color: #fff;
+ background-color: #6ba44e;
+ padding: 2px;
+ font-size: 12px;
+}
+
.reply_time {
font-size: 11px;
}
@@ -568,10 +581,14 @@ a.user_avatar:hover {
float: right;
margin-left: 20px;
font-size: 15px;
-}
-.user_action a {
- text-decoration: none;
+ a {
+ text-decoration: none;
+ }
+
+ .up-count {
+ color: gray;
+ }
}
.reply_content {
@@ -617,7 +634,7 @@ a.topic_title {
white-space: nowrap;
overflow: hidden;
display: inline-block;
- vertical-align: bottom;
+ vertical-align: middle;
font-size: 16px;
line-height: 30px;
}
@@ -635,7 +652,7 @@ a.topic_title {
white-space: nowrap;
}
-.put_top {
+.put_top, .put_good {
background: @node_green;
padding: 2px 4px;
border-radius: 3px;
@@ -646,17 +663,6 @@ a.topic_title {
font-size: 12px;
}
-.put_good {
- background: @node_green;
- padding: 1px 2px;
- border-radius: 2px;
- -webkit-border-radius: 2px;
- -moz-border-radius: 2px;
- -o-border-radius: 2px;
- color: white;
- font-size: 13px;
-}
-
.star_name {
}
@@ -684,10 +690,6 @@ img.unread {
clear: left;
}
-#collect_btn {
-
-}
-
#backtotop {
width: 24px;
color: gray;
@@ -736,6 +738,22 @@ img.unread {
padding: 4px 6px;
}
+
+.markdown-text img {
+ cursor: pointer;
+}
+
+.markdown-text {
+ h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
+ font-size: inherit;
+ color: inherit;
+ }
+}
+
+.panel .markdown-text a {
+ color: #08c;
+}
+
.preview {
padding: 0.5em;
font-size: 15px;
@@ -809,7 +827,15 @@ textarea#title {
padding: 4px 0px 0px 20px;
width: 120px;
padding: 3px 20px;
- height: 28px;
+ height: 34px;
+ line-height: 34px;
+ text-shadow: none;
+ color: #cccccc;
+ font-weight: 700;
+
+ img {
+ vertical-align: initial;
+ }
}
.navbar .navbar-search {
@@ -1260,3 +1286,32 @@ textarea.editor {
.cnode-app-download {
text-align: center;
}
+
+// 图片预览
+#preview-modal {
+ // 左右两遍各预留 1%,因为一般屏幕比较宽,所以只留下了 1%
+ width: 98%;
+ margin-left: -49%;
+ top: 2%;
+ // 上面 2% 下面 2% 中间就剩下 96%
+ max-height: 96%;
+
+ text-align: center;
+ overflow-y: scroll;
+ display: none;
+
+ img {
+ // just for 增强好看度
+ box-shadow: 0 0 10px 5px grey;
+ cursor: pointer;
+ }
+}
+
+.about-friend-links img {
+ width: 250px;
+ height: 60px;
+}
+
+.sponsor_outlink:hover {
+ text-decoration: none;
+}
diff --git a/test/api/v1/message.test.js b/test/api/v1/message.test.js
index 7537162164..441d739769 100644
--- a/test/api/v1/message.test.js
+++ b/test/api/v1/message.test.js
@@ -7,7 +7,9 @@ var mm = require('mm');
var should = require('should');
describe('test/api/v1/message.test.js', function () {
+
var mockUser;
+
before(function (done) {
support.ready(function () {
support.createUser(function (err, user) {
@@ -30,7 +32,7 @@ describe('test/api/v1/message.test.js', function () {
should.not.exists(err);
request.get('/api/v1/messages')
.query({
- accesstoken: mockUser.accessToken,
+ accesstoken: mockUser.accessToken
})
.end(function (err, res) {
res.body.data.hasnot_read_messages.length.should.above(0);
@@ -45,7 +47,7 @@ describe('test/api/v1/message.test.js', function () {
});
request.get('/api/v1/message/count')
.query({
- accesstoken: mockUser.accessToken,
+ accesstoken: mockUser.accessToken
})
.end(function (err, res) {
res.body.data.should.equal(1);
@@ -56,14 +58,14 @@ describe('test/api/v1/message.test.js', function () {
it('should mark all messages read', function (done) {
request.post('/api/v1/message/mark_all')
.send({
- accesstoken: mockUser.accessToken,
+ accesstoken: mockUser.accessToken
})
.end(function (err, res) {
// 第一次查询有一个
res.body.marked_msgs.length.should.equal(1);
request.post('/api/v1/message/mark_all')
.send({
- accesstoken: mockUser.accessToken,
+ accesstoken: mockUser.accessToken
})
.end(function (err, res) {
// 第二次查询没了
@@ -72,4 +74,5 @@ describe('test/api/v1/message.test.js', function () {
});
});
});
+
});
diff --git a/test/api/v1/reply.test.js b/test/api/v1/reply.test.js
index f280eb6298..a6942f5674 100644
--- a/test/api/v1/reply.test.js
+++ b/test/api/v1/reply.test.js
@@ -5,8 +5,9 @@ var support = require('../../support/support');
var should = require('should');
describe('test/api/v1/reply.test.js', function () {
- var mockTopic;
- var mockReplyId;
+
+ var mockTopic, mockReplyId;
+
before(function (done) {
support.ready(function () {
support.createTopic(support.normalUser.id, function (err, topic) {
@@ -22,11 +23,11 @@ describe('test/api/v1/reply.test.js', function () {
request.post('/api/v1/topic/' + mockTopic.id + '/replies')
.send({
content: 'reply a topic from api',
- accesstoken: support.normalUser.accessToken,
+ accesstoken: support.normalUser.accessToken
})
.end(function (err, res) {
should.not.exists(err);
- res.body.success.should.true;
+ res.body.success.should.true();
mockReplyId = res.body.reply_id;
done();
});
@@ -37,27 +38,73 @@ describe('test/api/v1/reply.test.js', function () {
.send({
content: 'reply a topic from api',
accesstoken: support.normalUser.accessToken,
- repli_id: mockReplyId,
+ repli_id: mockReplyId
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ done();
+ });
+ });
+
+ it('should 401 when no accessToken', function (done) {
+ request.post('/api/v1/topic/' + mockTopic.id + 'not_valid' + '/replies')
+ .send({
+ content: 'reply a topic from api'
})
.end(function (err, res) {
should.not.exists(err);
- res.body.success.should.true;
+ res.status.should.equal(401);
+ res.body.success.should.false();
done();
});
});
- it('should fail when topic is not found', function (done) {
- request.post('/api/v1/topic/' + mockTopic.id + 'not_found' + '/replies')
+ it('should fail when topic_id is not valid', function (done) {
+ request.post('/api/v1/topic/' + mockTopic.id + 'not_valid' + '/replies')
.send({
content: 'reply a topic from api',
- accesstoken: support.normalUser.accessToken,
+ accesstoken: support.normalUser.accessToken
})
.end(function (err, res) {
should.not.exists(err);
- res.status.should.equal(500);
+ res.status.should.equal(400);
+ res.body.success.should.false();
done();
});
+ });
+ it('should fail when no content', function (done) {
+ request.post('/api/v1/topic/' + mockTopic.id + '/replies')
+ .send({
+ content: '',
+ accesstoken: support.normalUser.accessToken
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(400);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should fail when topic not found', function (done) {
+ var notFoundTopicId = mockTopic.id.split("").reverse().join("");
+ request.post('/api/v1/topic/' + notFoundTopicId + '/replies')
+ .send({
+ content: 'reply a topic from api',
+ accesstoken: support.normalUser.accessToken
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ if (mockTopic.id === notFoundTopicId) { // 小概率事件id反转之后还不变
+ res.body.success.should.true();
+ } else {
+ res.status.should.equal(404);
+ res.body.success.should.false();
+ }
+ done();
+ });
});
it('should fail when topic is locked', function (done) {
@@ -67,12 +114,12 @@ describe('test/api/v1/reply.test.js', function () {
request.post('/api/v1/topic/' + mockTopic.id + '/replies')
.send({
content: 'reply a topic from api',
- accesstoken: support.normalUser.accessToken,
+ accesstoken: support.normalUser.accessToken
})
- .expect(403)
.end(function (err, res) {
should.not.exists(err);
-
+ res.status.should.equal(403);
+ res.body.success.should.false();
// 解锁 topic
mockTopic.lock = !mockTopic.lock;
mockTopic.save(function () {
@@ -81,44 +128,78 @@ describe('test/api/v1/reply.test.js', function () {
});
});
});
- });
+ });
+
describe('create ups', function () {
+
it('should up', function (done) {
request.post('/api/v1/reply/' + mockReplyId + '/ups')
.send({
- accesstoken: support.normalUser.accessToken,
+ accesstoken: support.normalUser.accessToken
})
.end(function (err, res) {
should.not.exists(err);
- res.body.should.eql({"success": true, "action": "up"});
+ res.body.success.should.true();
+ res.body.action.should.equal("up");
done();
- })
+ });
});
it('should down', function (done) {
request.post('/api/v1/reply/' + mockReplyId + '/ups')
.send({
- accesstoken: support.normalUser.accessToken,
+ accesstoken: support.normalUser.accessToken
})
.end(function (err, res) {
should.not.exists(err);
- res.body.should.eql({"success": true, "action": "down"});
+ res.body.success.should.true();
+ res.body.action.should.equal("down");
done();
- })
+ });
});
- it('do nothing when replyid is not found', function (done) {
- request.post('/api/v1/reply/' + mockReplyId + 'not_found' + '/ups')
+ it('should 401 when no accessToken', function (done) {
+ request.post('/api/v1/reply/' + mockReplyId + '/ups')
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(401);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should fail when reply_id is not valid', function (done) {
+ request.post('/api/v1/reply/' + mockReplyId + 'not_valid' + '/ups')
.send({
- accesstoken: support.normalUser.accessToken,
+ accesstoken: support.normalUser.accessToken
})
.end(function (err, res) {
should.not.exists(err);
- res.status.should.equal(500);
+ res.status.should.equal(400);
+ res.body.success.should.false();
done();
+ });
+ });
+
+ it('should fail when reply_id is not found', function (done) {
+ var notFoundReplyId = mockReplyId.split("").reverse().join("");
+ request.post('/api/v1/reply/' + notFoundReplyId + '/ups')
+ .send({
+ accesstoken: support.normalUser.accessToken
})
+ .end(function (err, res) {
+ should.not.exists(err);
+ if (mockReplyId === notFoundReplyId) { // 小概率事件id反转之后还不变
+ res.body.success.should.true();
+ } else {
+ res.status.should.equal(404);
+ res.body.success.should.false();
+ }
+ done();
+ });
});
- })
+ });
+
});
diff --git a/test/api/v1/tools.test.js b/test/api/v1/tools.test.js
index d8b8f6b1e1..f5368e208d 100644
--- a/test/api/v1/tools.test.js
+++ b/test/api/v1/tools.test.js
@@ -1,18 +1,18 @@
-
var app = require('../../../app');
var request = require('supertest')(app);
var support = require('../../support/support');
var should = require('should');
-
describe('test/api/v1/tools.test.js', function () {
+
var mockUser;
+
before(function (done) {
support.createUser(function (err, user) {
mockUser = user;
done();
- })
- })
+ });
+ });
it('should response with loginname', function (done) {
request.post('/api/v1/accesstoken')
@@ -22,21 +22,24 @@ describe('test/api/v1/tools.test.js', function () {
.end(function (err, res) {
should.not.exists(err);
res.status.should.equal(200);
+ res.body.success.should.true();
res.body.loginname.should.equal(mockUser.loginname);
+ res.body.id.should.equal(mockUser.id);
done();
- })
- })
+ });
+ });
- it('should 403 when accessToken is wrong', function (done) {
+ it('should 401 when accessToken is wrong', function (done) {
request.post('/api/v1/accesstoken')
.send({
- accessToken: 'not_exists'
+ accesstoken: 'not_exists'
})
.end(function (err, res) {
should.not.exists(err);
- res.status.should.equal(403);
- res.body.error_msg.should.containEql('wrong accessToken');
+ res.status.should.equal(401);
+ res.body.success.should.false();
done();
- })
- })
-})
+ });
+ });
+
+});
diff --git a/test/api/v1/topic.test.js b/test/api/v1/topic.test.js
index a29b64bdb8..ef3b136d00 100644
--- a/test/api/v1/topic.test.js
+++ b/test/api/v1/topic.test.js
@@ -1,129 +1,229 @@
-
-
var app = require('../../../app');
var request = require('supertest')(app);
var should = require('should');
var support = require('../../support/support');
-
describe('test/api/v1/topic.test.js', function () {
+
var mockUser, mockTopic;
+
+ var createdTopicId = null;
+
before(function (done) {
support.createUser(function (err, user) {
mockUser = user;
support.createTopic(user.id, function (err, topic) {
mockTopic = topic;
- done();
- })
- })
- })
+ support.createReply(topic.id, user.id, function (err, reply) {
+ support.createSingleUp(reply.id, user.id, function (err, reply) {
+ done();
+ });
+ });
+ });
+ });
+ });
describe('get /api/v1/topics', function () {
+
it('should return topics', function (done) {
request.get('/api/v1/topics')
.end(function (err, res) {
should.not.exists(err);
+ res.body.success.should.true();
res.body.data.length.should.above(0);
done();
});
});
- it('should return topics', function (done) {
+ it('should return topics with limit 2', function (done) {
request.get('/api/v1/topics')
.query({
limit: 2
})
.end(function (err, res) {
should.not.exists(err);
+ res.body.success.should.true();
res.body.data.length.should.equal(2);
done();
});
});
+
});
describe('get /api/v1/topic/:topicid', function () {
- it('should return topic info', function (done) {
+ it('should return topic info', function (done) {
request.get('/api/v1/topic/' + mockTopic.id)
.end(function (err, res) {
should.not.exists(err);
+ res.body.success.should.true();
res.body.data.id.should.equal(mockTopic.id);
done();
+ });
+ });
+
+ it('should fail when topic_id is not valid', function (done) {
+ request.get('/api/v1/topic/' + mockTopic.id + 'not_valid')
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(400);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should fail when topic not found', function (done) {
+ var notFoundTopicId = mockTopic.id.split("").reverse().join("");
+ request.get('/api/v1/topic/' + notFoundTopicId)
+ .end(function (err, res) {
+ should.not.exists(err);
+ if (mockTopic.id === notFoundTopicId) { // 小概率事件id反转之后还不变
+ res.body.success.should.true();
+ res.body.data.id.should.equal(mockTopic.id);
+ } else {
+ res.status.should.equal(404);
+ res.body.success.should.false();
+ }
+ done();
+ });
+ });
+
+ it('should is_uped to be false without accesstoken', function (done) {
+ request.get('/api/v1/topic/' + mockTopic.id)
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.data.replies[0].is_uped.should.false();
+ done();
+ });
+ });
+
+ it('should is_uped to be false with wrong accesstoken', function (done) {
+ request.get('/api/v1/topic/' + mockTopic.id)
+ .query({
+ accesstoken: support.normalUser2.accesstoken
})
- })
- })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.data.replies[0].is_uped.should.false();
+ done();
+ });
+ });
+
+ it('should is_uped to be true with right accesstoken', function (done) {
+ request.get('/api/v1/topic/' + mockTopic.id)
+ .query({
+ accesstoken: mockUser.accessToken
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.data.replies[0].is_uped.should.true();
+ done();
+ });
+ });
+
+ });
describe('post /api/v1/topics', function () {
+
it('should create a topic', function (done) {
request.post('/api/v1/topics')
.send({
accesstoken: mockUser.accessToken,
- title: '我是 api 测试小助手',
+ title: '我是API测试标题',
tab: 'share',
- content: '我也是 api 测试小助手',
+ content: '我是API测试内容'
})
.end(function (err, res) {
should.not.exists(err);
- res.body.success.should.true;
- res.body.topic_id.should.be.String;
+ res.body.success.should.true();
+ res.body.topic_id.should.be.String();
+ createdTopicId = res.body.topic_id
done();
+ });
+ });
+
+ it('should 401 with no accessToken', function (done) {
+ request.post('/api/v1/topics')
+ .send({
+ title: '我是API测试标题',
+ tab: 'share',
+ content: '我是API测试内容'
})
- })
- })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(401);
+ res.body.success.should.false();
+ done();
+ });
+ });
- describe('post /api/v1/topic/collect', function () {
- it('should collect topic', function (done) {
- request.post('/api/v1/topic/collect')
+ it('should fail with no title', function (done) {
+ request.post('/api/v1/topics')
.send({
accesstoken: mockUser.accessToken,
- topic_id: mockTopic.id
+ title: '',
+ tab: 'share',
+ content: '我是API测试内容'
})
.end(function (err, res) {
should.not.exists(err);
- res.body.should.eql({"success": true});
+ res.status.should.equal(400);
+ res.body.success.should.false();
done();
- })
+ });
});
- it('do nothing when topic is not found', function (done) {
- request.post('/api/v1/topic/collect')
+ it('should fail with error tab', function (done) {
+ request.post('/api/v1/topics')
.send({
- accesstoken: support.normalUser.accessToken,
- topic_id: mockTopic.id + 'not_found'
+ accesstoken: mockUser.accessToken,
+ title: '我是API测试标题',
+ tab: '',
+ content: '我是API测试内容'
})
.end(function (err, res) {
should.not.exists(err);
- res.status.should.equal(500);
+ res.status.should.equal(400);
+ res.body.success.should.false();
done();
- })
+ });
});
- })
- describe('post /api/v1/topic/de_collect', function () {
- it('should de_collect topic', function (done) {
- request.post('/api/v1/topic/de_collect')
+ it('should fail with no content', function (done) {
+ request.post('/api/v1/topics')
.send({
accesstoken: mockUser.accessToken,
- topic_id: mockTopic.id
+ title: '我是API测试标题',
+ tab: 'share',
+ content: ''
})
.end(function (err, res) {
should.not.exists(err);
- res.body.should.eql({"success": true});
+ res.status.should.equal(400);
+ res.body.success.should.false();
done();
- })
+ });
});
- it('do nothing when topic is not found', function (done) {
- request.post('/api/v1/topic/de_collect')
+ });
+
+ describe('post /api/v1/topics/update', function () {
+ it('should update a topic', function (done) {
+ request.post('/api/v1/topics/update')
.send({
- accesstoken: support.normalUser.accessToken,
- topic_id: mockTopic.id + 'not_found'
+ accesstoken: mockUser.accessToken,
+ topic_id: createdTopicId,
+ title: '我是API测试标题',
+ tab: 'share',
+ content: '我是API测试内容 /api/v1/topics/update'
})
.end(function (err, res) {
should.not.exists(err);
- res.status.should.equal(500);
+ res.body.success.should.true();
+ res.body.topic_id.should.eql(createdTopicId);
done();
- })
- });
+ });
+ })
})
-})
+
+});
diff --git a/test/api/v1/topic_collect.test.js b/test/api/v1/topic_collect.test.js
new file mode 100644
index 0000000000..4b7e3bd5f9
--- /dev/null
+++ b/test/api/v1/topic_collect.test.js
@@ -0,0 +1,293 @@
+var app = require('../../../app');
+var request = require('supertest')(app);
+var should = require('should');
+var support = require('../../support/support');
+
+describe('test/api/v1/topic_collect.test.js', function () {
+
+ var mockUser, mockTopic;
+
+ before(function (done) {
+ support.createUser(function (err, user) {
+ mockUser = user;
+ support.createTopic(user.id, function (err, topic) {
+ mockTopic = topic;
+ done();
+ });
+ });
+ });
+
+ // 主题被收藏之前
+ describe('before collect topic', function () {
+
+ describe('get /topic_collect/:loginname', function () {
+
+ it('should list topic with length = 0', function (done) {
+ request.get('/api/v1/topic_collect/' + mockUser.loginname)
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ res.body.data.length.should.equal(0);
+ done();
+ });
+ });
+
+ });
+
+ describe('get /api/v1/topic/:topicid', function () {
+
+ it('should return topic info with is_collect = false', function (done) {
+ request.get('/api/v1/topic/' + mockTopic.id)
+ .query({
+ accesstoken: mockUser.accessToken
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ res.body.data.is_collect.should.false();
+ done();
+ });
+ });
+
+ });
+
+ });
+
+ // 收藏主题
+ describe('post /topic_collect/collect', function () {
+
+ it('should 401 with no accessToken', function (done) {
+ request.post('/api/v1/topic_collect/collect')
+ .send({
+ topic_id: mockTopic.id
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(401);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should collect topic with correct accessToken', function (done) {
+ request.post('/api/v1/topic_collect/collect')
+ .send({
+ accesstoken: mockUser.accessToken,
+ topic_id: mockTopic.id
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ done();
+ });
+ });
+
+ it('should not collect topic twice', function (done) {
+ request.post('/api/v1/topic_collect/collect')
+ .send({
+ accesstoken: mockUser.accessToken,
+ topic_id: mockTopic.id
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should fail when topic_id is not valid', function (done) {
+ request.post('/api/v1/topic_collect/collect')
+ .send({
+ accesstoken: mockUser.accessToken,
+ topic_id: mockTopic.id + "not_valid"
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(400);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should fail when topic not found', function (done) {
+ var notFoundTopicId = mockTopic.id.split("").reverse().join("");
+ request.post('/api/v1/topic_collect/collect')
+ .send({
+ accesstoken: mockUser.accessToken,
+ topic_id: notFoundTopicId
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ if (mockTopic.id === notFoundTopicId) { // 小概率事件id反转之后还不变
+ res.body.success.should.true();
+ } else {
+ res.status.should.equal(404);
+ res.body.success.should.false();
+ }
+ done();
+ });
+ });
+
+ });
+
+ // 主题被收藏之后
+ describe('after collect topic', function () {
+
+ describe('get /topic_collect/:loginname', function () {
+
+ it('should list topic with length = 1', function (done) {
+ request.get('/api/v1/topic_collect/' + mockUser.loginname)
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ res.body.data.length.should.equal(1);
+ res.body.data[0].id.should.equal(mockTopic.id);
+ done();
+ });
+ });
+
+ it('should fail when user not found', function (done) {
+ request.get('/api/v1/topic_collect/' + mockUser.loginname + 'not_found')
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(404);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ });
+
+ describe('get /api/v1/topic/:topicid', function () {
+
+ it('should return topic info with is_collect = true', function (done) {
+ request.get('/api/v1/topic/' + mockTopic.id)
+ .query({
+ accesstoken: mockUser.accessToken
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ res.body.data.is_collect.should.true();
+ done();
+ });
+ });
+
+ });
+
+ });
+
+ // 取消收藏主题
+ describe('post /topic_collect/de_collect', function () {
+
+ it('should 401 with no accessToken', function (done) {
+ request.post('/api/v1/topic_collect/de_collect')
+ .send({
+ topic_id: mockTopic.id
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(401);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should decollect topic with correct accessToken', function (done) {
+ request.post('/api/v1/topic_collect/de_collect')
+ .send({
+ accesstoken: mockUser.accessToken,
+ topic_id: mockTopic.id
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ done();
+ });
+ });
+
+ it('should not decollect topic twice', function (done) {
+ request.post('/api/v1/topic_collect/de_collect')
+ .send({
+ accesstoken: mockUser.accessToken,
+ topic_id: mockTopic.id
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should fail when topic_id is not valid', function (done) {
+ request.post('/api/v1/topic_collect/de_collect')
+ .send({
+ accesstoken: mockUser.accessToken,
+ topic_id: mockTopic.id + "not_valid"
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.status.should.equal(400);
+ res.body.success.should.false();
+ done();
+ });
+ });
+
+ it('should fail when topic not found', function (done) {
+ var notFoundTopicId = mockTopic.id.split("").reverse().join("");
+ request.post('/api/v1/topic_collect/de_collect')
+ .send({
+ accesstoken: mockUser.accessToken,
+ topic_id: notFoundTopicId
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ if (mockTopic.id === notFoundTopicId) { // 小概率事件id反转之后还不变
+ res.body.success.should.true();
+ } else {
+ res.status.should.equal(404);
+ res.body.success.should.false();
+ }
+ done();
+ });
+ });
+
+ });
+
+ // 主题被取消收藏之后
+ describe('after decollect topic', function () {
+
+ describe('get /topic_collect/:loginname', function () {
+
+ it('should list topic with length = 0', function (done) {
+ request.get('/api/v1/topic_collect/' + mockUser.loginname)
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ res.body.data.length.should.equal(0);
+ done();
+ });
+ });
+
+ });
+
+ describe('get /api/v1/topic/:topicid', function () {
+
+ it('should return topic info with is_collect = false', function (done) {
+ request.get('/api/v1/topic/' + mockTopic.id)
+ .query({
+ accesstoken: mockUser.accessToken
+ })
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ res.body.data.is_collect.should.false();
+ done();
+ });
+ });
+
+ });
+
+ });
+
+});
diff --git a/test/api/v1/user.test.js b/test/api/v1/user.test.js
index a15aa6ba18..947dfcfce7 100644
--- a/test/api/v1/user.test.js
+++ b/test/api/v1/user.test.js
@@ -1,20 +1,60 @@
-
-
var app = require('../../../app');
var request = require('supertest')(app);
var support = require('../../support/support');
var should = require('should');
+var async = require('async');
describe('test/api/v1/user.test.js', function () {
- it('should return user info', function (done) {
- support.createUser(function (err, user) {
- should.not.exists(err);
- request.get('/api/v1/user/' + user.loginname)
+
+ var mockUser;
+
+ before(function (done) {
+ async.auto({
+ create_user: function(callback){
+ support.createUser(function (err, user) {
+ mockUser = user;
+ callback(null, user);
+ });
+ },
+ create_topic: ['create_user', function(callback, result){
+ support.createTopic(result['create_user']._id, function(err, topic){
+ callback(null, topic);
+ });
+ }],
+ create_replies: ['create_topic', function(callback, result){
+ support.createReply(result['create_topic']._id, result['create_topic'].author_id, function(err, replay){
+ callback(null, replay);
+ });
+ }]
+ }, function(err, results){
+ done();
+ });
+ });
+
+ describe('get /api/v1/user/:loginname', function () {
+
+ it('should return user info', function (done) {
+ request.get('/api/v1/user/' + mockUser.loginname)
+ .end(function (err, res) {
+ should.not.exists(err);
+ res.body.success.should.true();
+ res.body.data.loginname.should.equal(mockUser.loginname);
+ should(res.body.data.recent_topics.length).be.exactly(1);
+ should(res.body.data.recent_replies.length).be.exactly(1);
+ done();
+ });
+ });
+
+ it('should fail when user is not found', function (done) {
+ request.get('/api/v1/user/' + mockUser.loginname + 'not_found')
.end(function (err, res) {
should.not.exists(err);
- res.body.data.loginname.should.equal(user.loginname);
+ res.status.should.equal(404);
+ res.body.success.should.false();
done();
});
});
+
});
+
});
diff --git a/test/common/at.test.js b/test/common/at.test.js
index e4362ad2ae..cfbcc6f550 100644
--- a/test/common/at.test.js
+++ b/test/common/at.test.js
@@ -37,6 +37,8 @@ describe('test/common/at.test.js', function () {
jysperm@gmail.com @alsotang
+ https://medium.com/@nodejs/announcing-a-new-experimental-modules-1be8d2d6c2ff
+
@alsotang2
@@ -74,6 +76,8 @@ describe('test/common/at.test.js', function () {
aldjf
@alsotang @tangzhanli
+ [@alsotang](/user/alsotang)
+
@liveinjs 没事儿,能力和热情更重要,北京雍和宫,想的就邮件给我i5ting@126.com
*/});
@@ -94,6 +98,8 @@ Text 中文[@begin_with_no_spaces](/user/begin_with_no_spaces)
jysperm@gmail.com [@alsotang](/user/alsotang)
+https://medium.com/@nodejs/announcing-a-new-experimental-modules-1be8d2d6c2ff
+
[@alsotang2](/user/alsotang2)
@@ -131,6 +137,8 @@ code: `@in_code`
aldjf
[@alsotang](/user/alsotang) [@tangzhanli](/user/tangzhanli)
+[@alsotang](/user/alsotang)
+
[@liveinjs](/user/liveinjs) 没事儿,能力和热情更重要,北京雍和宫,想的就邮件给我i5ting@126.com
*/});
diff --git a/test/common/render_helper.test.js b/test/common/render_helper.test.js
index e995249125..1b67846012 100644
--- a/test/common/render_helper.test.js
+++ b/test/common/render_helper.test.js
@@ -10,7 +10,18 @@ var renderHelper = require('../../common/render_helper');
describe('test/common/render_helper.test.js', function () {
describe('#markdown', function () {
- it('should render code', function () {
+ it('should render code inline', function () {
+ var text = multiline(function () {;
+ /*
+`var a = 1;`
+ */
+ });
+
+ var rendered = renderHelper.markdown(text);
+ rendered.should.equal('var a = 1;
\n');
+ });
+
+ it('should render fence', function () {
var text = multiline(function () {;
/*
```js
@@ -22,6 +33,17 @@ var a = 1;
var rendered = renderHelper.markdown(text);
rendered.should.equal('var a = 1;\n
');
});
+
+ it('should render code block', function () {
+ var text = multiline(function () {;
+/*
+ var a = 1;
+*/
+ });
+
+ var rendered = renderHelper.markdown(text);
+ rendered.should.equal('var a = 1;
');
+ });
});
describe('#escapeSignature', function () {
diff --git a/test/common/store_local.test.js b/test/common/store_local.test.js
index 451f7ce5ba..b83896bbdf 100644
--- a/test/common/store_local.test.js
+++ b/test/common/store_local.test.js
@@ -12,7 +12,7 @@ describe('test/common/store_local.test.js', function () {
var newFilePath = path.join(config.upload.path, newFilename);
setTimeout(function () {
fs.existsSync(newFilePath)
- .should.ok;
+ .should.ok();
fs.unlinkSync(newFilePath);
done(err);
}, 1 * 1000);
diff --git a/test/controllers/github.test.js b/test/controllers/github.test.js
index ab7e44dcc5..d4ff5a4913 100644
--- a/test/controllers/github.test.js
+++ b/test/controllers/github.test.js
@@ -109,7 +109,7 @@ describe('test/controllers/github.test.js', function () {
});
it('should create a new user', function (done) {
var userCount;
- User.count(function (err, count) {
+ User.countDocuments(function (err, count) {
userCount = count;
request.post('/auth/github/test_create')
.send({isnew: '1'})
@@ -119,7 +119,7 @@ describe('test/controllers/github.test.js', function () {
}
res.headers.should.have.property('location')
.with.endWith('/');
- User.count(function (err, count) {
+ User.countDocuments(function (err, count) {
count.should.equal(userCount + 1);
done();
});
diff --git a/test/controllers/sign.test.js b/test/controllers/sign.test.js
index eb3620e6dd..66d7324b14 100644
--- a/test/controllers/sign.test.js
+++ b/test/controllers/sign.test.js
@@ -60,7 +60,7 @@ describe('test/controllers/sign.test.js', function () {
res.text.should.containEql('欢迎加入');
UserProxy.getUserByLoginName(loginname, function (err, user) {
should.not.exists(err);
- user.should.ok;
+ user.should.ok();
done();
});
});
@@ -130,7 +130,7 @@ describe('test/controllers/sign.test.js', function () {
request.post('/signout')
.set('Cookie', config.auth_cookie_name + ':something;')
.expect(302, function (err, res) {
- res.headers['set-cookie'].should.eql([ 'node_club=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT' ]);
+ res.headers['set-cookie'].should.eql([ config.auth_cookie_name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT' ]);
done(err);
});
});
diff --git a/test/controllers/topic.test.js b/test/controllers/topic.test.js
index 9ccc57401f..270aa68f79 100644
--- a/test/controllers/topic.test.js
+++ b/test/controllers/topic.test.js
@@ -214,6 +214,18 @@ describe('test/controllers/topic.test.js', function () {
done(err);
})
})
+
+ it('should not collect a topic twice', function (done) {
+ request.post('/topic/collect')
+ .send({
+ topic_id: support.testTopic._id,
+ })
+ .set('Cookie', support.normalUser2Cookie)
+ .expect(200, function (err, res) {
+ res.body.should.eql({status: 'failed'});
+ done(err);
+ })
+ })
})
describe('#de_collect', function () {
@@ -228,6 +240,18 @@ describe('test/controllers/topic.test.js', function () {
done(err);
});
});
+
+ it('should not decollect a non-exist topic_collect', function (done) {
+ request.post('/topic/de_collect')
+ .send({
+ topic_id: support.testTopic._id,
+ })
+ .set('Cookie', support.normalUser2Cookie)
+ .expect(200, function (err, res) {
+ res.body.should.eql({status: 'failed'});
+ done(err);
+ });
+ });
});
describe('#upload', function () {
diff --git a/test/controllers/user.test.js b/test/controllers/user.test.js
index 1eb1576cd9..f4c603d767 100644
--- a/test/controllers/user.test.js
+++ b/test/controllers/user.test.js
@@ -156,7 +156,7 @@ describe('test/controllers/user.test.js', function () {
res.body.should.eql({status: 'success'});
UserProxy.getUserById(support.normalUser._id, function (err, user) {
- user.is_star.should.be.true;
+ user.is_star.should.be.true();
done(err);
});
});
@@ -172,7 +172,7 @@ describe('test/controllers/user.test.js', function () {
res.body.should.eql({status: 'success'});
UserProxy.getUserById(support.normalUser._id, function (err, user) {
- user.is_star.should.be.false;
+ user.is_star.should.be.false();
done(err);
});
});
@@ -230,7 +230,7 @@ describe('test/controllers/user.test.js', function () {
.expect(200, function (err, res) {
res.body.should.eql({status: 'success'});
UserProxy.getUserById(newuser._id, function (err, user) {
- user.is_block.should.be.true;
+ user.is_block.should.be.true();
done(err);
});
});
diff --git a/test/middlewares/limit.test.js b/test/middlewares/limit.test.js
index eac2d3b18c..9088871fb1 100644
--- a/test/middlewares/limit.test.js
+++ b/test/middlewares/limit.test.js
@@ -3,6 +3,7 @@ var app = require('../../app');
var supertest;
var support = require('../support/support');
var pedding = require('pedding');
+var visitor = 'visit' + Date.now();
describe('test/middlewares/limit.test.js', function () {
before(function (done) {
@@ -11,7 +12,7 @@ describe('test/middlewares/limit.test.js', function () {
before(function () {
app.get('/test_peripperday',
- limitMiddleware.peripperday('visit', 3), function (req, res) {
+ limitMiddleware.peripperday(visitor, 3, {showJson: true}), function (req, res) {
res.send('hello');
});
@@ -19,9 +20,9 @@ describe('test/middlewares/limit.test.js', function () {
});
describe('#peripperday', function () {
it('should visit', function (done) {
- supertest.get('/test_peripperday').end(function () {
- supertest.get('/test_peripperday').end(function () {
- supertest.get('/test_peripperday').end(function (err, res) {
+ supertest.get('/test_peripperday').set('x-real-ip', '127.0.0.1').end(function () {
+ supertest.get('/test_peripperday').set('x-real-ip', '127.0.0.1').end(function () {
+ supertest.get('/test_peripperday').set('x-real-ip', '127.0.0.1').end(function (err, res) {
res.text.should.eql('hello');
done();
});
@@ -30,8 +31,10 @@ describe('test/middlewares/limit.test.js', function () {
});
it('should not visit', function (done) {
supertest.get('/test_peripperday')
+ .set('x-real-ip', '127.0.0.1')
.end(function (err, res) {
- res.text.should.eql('ratelimit forbidden. limit is 3 per day.');
+ res.status.should.equal(403);
+ res.body.success.should.false();
done(err);
});
});
diff --git a/test/models/user.test.js b/test/models/user.test.js
index 1dc2e28967..beda7729da 100644
--- a/test/models/user.test.js
+++ b/test/models/user.test.js
@@ -3,6 +3,6 @@ var UserModel = require('../../models').User;
describe('test/models/user.test.js', function () {
it('should return proxy avatar url', function () {
var user = new UserModel({email: 'alsotang@gmail.com'});
- user.avatar_url.should.eql('/agent?url=https%3A%2F%2Fgravatar.com%2Favatar%2Feeb90e7b92f78e01cac07087165e3640%3Fsize%3D48');
+ user.avatar_url.should.eql('https://gravatar.com/avatar/eeb90e7b92f78e01cac07087165e3640?size=48');
});
});
diff --git a/test/support/support.js b/test/support/support.js
index 42a15c90ce..58c445e3c4 100644
--- a/test/support/support.js
+++ b/test/support/support.js
@@ -32,6 +32,16 @@ var createReply = exports.createReply = function (topicId, authorId, callback) {
Reply.newAndSave('I am content', topicId, authorId, callback);
};
+var createSingleUp = exports.createSingleUp = function (replyId, userId, callback) {
+ Reply.getReply(replyId, function (err, reply) {
+ reply.ups = [];
+ reply.ups.push(userId);
+ reply.save(function (err, reply) {
+ callback(err, reply);
+ });
+ });
+};
+
function mockUser(user) {
return 'mock_user=' + JSON.stringify(user) + ';';
}
diff --git a/views/_sponsors.html b/views/_sponsors.html
index 131871d3f6..4da6ee7fe4 100644
--- a/views/_sponsors.html
+++ b/views/_sponsors.html
@@ -1,4 +1,5 @@
+ CNode 社区为国内最专业的 Node.js 开源技术社区,致力于 Node.js 的技术研究。
服务器搭建在
@@ -12,4 +13,5 @@
alt="七牛云存储" width="115px"/>
+ 新手搭建 Node.js 服务器,推荐使用无需备案的 DigitalOcean(https://www.digitalocean.com/)
diff --git a/views/editor_sidebar.html b/views/editor_sidebar.html
index 35dc822ad3..7d102dbafb 100644
--- a/views/editor_sidebar.html
+++ b/views/editor_sidebar.html
@@ -12,7 +12,7 @@
[内容](链接)

- Markdown 文档
+ Markdown 文档
@@ -24,7 +24,6 @@
- 尽量把话题要点浓缩到标题里
- 代码含义和报错可在 SegmentFault 提问
- - 给话题选择合适的标签能增加浏览
diff --git a/views/includes/editor.html b/views/includes/editor.html
new file mode 100644
index 0000000000..6c0c397727
--- /dev/null
+++ b/views/includes/editor.html
@@ -0,0 +1,6 @@
+<%- Loader('/public/editor.min.js')
+.js('/public/libs/editor/editor.js')
+.js('/public/libs/webuploader/webuploader.withoutimage.js')
+.js('/public/libs/editor/ext.js')
+.done(assets, config.site_static_host, config.mini_assets)
+%>
diff --git a/views/layout.html b/views/layout.html
index 5a53574dd8..7dfbcef1d1 100644
--- a/views/layout.html
+++ b/views/layout.html
@@ -6,7 +6,6 @@
-
@@ -142,7 +141,7 @@
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
- })(window,document,'script',<%- proxy('https://www.google-analytics.com/analytics.js') %>,'ga');
+ })(window,document,'script', "<%- proxy('https://www.google-analytics.com/analytics.js') %>",'ga');
ga('create', '<%-config.google_tracker_id%>', 'auto');
ga('send', 'pageview');
diff --git a/views/reply/edit.html b/views/reply/edit.html
index fd965ba64e..0126b22e7c 100644
--- a/views/reply/edit.html
+++ b/views/reply/edit.html
@@ -46,12 +46,7 @@
-<%- Loader('/public/editor.min.js')
-.js('/public/libs/editor/editor.js')
-.js('/public/libs/webuploader/webuploader.withoutimage.js')
-.js('/public/libs/editor/ext.js')
-.done(assets, config.site_static_host, config.mini_assets)
-%>
+<%- partial('../includes/editor') %>
diff --git a/views/user/card.html b/views/user/card.html
index 6e2a86d597..41c2214890 100644
--- a/views/user/card.html
+++ b/views/user/card.html
@@ -1,7 +1,7 @@
-
+
<%= user.loginname %>
diff --git a/views/user/index.html b/views/user/index.html
index a3d9b55194..5d015568e1 100644
--- a/views/user/index.html
+++ b/views/user/index.html
@@ -9,7 +9,7 @@
-
+
<%= user.loginname %>
@@ -19,7 +19,7 @@
<% if (user.collect_topic_count) {%>
- <%= user.collect_topic_count %>话题收藏
+ <%= user.collect_topic_count %>个话题收藏
<%}%>
diff --git a/views/user/setting.html b/views/user/setting.html
index a7343ac7ae..463912de2c 100644
--- a/views/user/setting.html
+++ b/views/user/setting.html
@@ -124,9 +124,12 @@
Access Token
+
+
+
字符串:
- <%- accessToken %>
+ <%- accessToken %>
二维码:
@@ -137,12 +140,26 @@
diff --git a/views/user/top100_user.html b/views/user/top100_user.html
index 7535ec944b..f06558b769 100644
--- a/views/user/top100_user.html
+++ b/views/user/top100_user.html
@@ -2,7 +2,7 @@
<%= indexInCollection+1 %>
-
+
<%= user.loginname %>
diff --git a/views/user/user.html b/views/user/user.html
index 3607965522..6d0211a77c 100644
--- a/views/user/user.html
+++ b/views/user/user.html
@@ -1,7 +1,7 @@
-
+
<%= user.loginname %>
diff --git a/web_router.js b/web_router.js
index fd508e8234..278aaac460 100644
--- a/web_router.js
+++ b/web_router.js
@@ -8,24 +8,24 @@
* Module dependencies.
*/
-var express = require('express');
-var sign = require('./controllers/sign');
-var site = require('./controllers/site');
-var user = require('./controllers/user');
-var message = require('./controllers/message');
-var topic = require('./controllers/topic');
-var reply = require('./controllers/reply');
-var rss = require('./controllers/rss');
+var express = require('express');
+var sign = require('./controllers/sign');
+var site = require('./controllers/site');
+var user = require('./controllers/user');
+var message = require('./controllers/message');
+var topic = require('./controllers/topic');
+var reply = require('./controllers/reply');
+var rss = require('./controllers/rss');
var staticController = require('./controllers/static');
-var auth = require('./middlewares/auth');
-var limit = require('./middlewares/limit');
-var github = require('./controllers/github');
-var search = require('./controllers/search');
-var passport = require('passport');
+var auth = require('./middlewares/auth');
+var limit = require('./middlewares/limit');
+var github = require('./controllers/github');
+var search = require('./controllers/search');
+var passport = require('passport');
var configMiddleware = require('./middlewares/conf');
-var config = require('./config');
+var config = require('./config');
-var router = express.Router();
+var router = express.Router();
// home page
router.get('/', site.index);
@@ -39,7 +39,10 @@ if (config.allow_sign_up) {
router.get('/signup', sign.showSignup); // 跳转到注册页面
router.post('/signup', sign.signup); // 提交注册信息
} else {
- router.get('/signup', configMiddleware.github, passport.authenticate('github')); // 进行github验证
+ // 进行github验证
+ router.get('/signup', function (req, res, next) {
+ return res.redirect('/auth/github')
+ });
}
router.post('/signout', sign.signout); // 登出
router.get('/signin', sign.showLogin); // 进入登录页面
@@ -64,6 +67,7 @@ router.post('/user/set_star', auth.adminRequired, user.toggleStar); // 把某用
router.post('/user/cancel_star', auth.adminRequired, user.toggleStar); // 取消某用户的达人身份
router.post('/user/:name/block', auth.adminRequired, user.block); // 禁言某用户
router.post('/user/:name/delete_all', auth.adminRequired, user.deleteAll); // 删除某用户所有发言
+router.post('/user/refresh_token', auth.userRequired, user.refreshToken); // 刷新用户token
// message controler
router.get('/my/messages', auth.userRequired, message.index); // 用户个人的所有消息页
@@ -82,14 +86,14 @@ router.post('/topic/:tid/lock', auth.adminRequired, topic.lock); // 锁定主题
router.post('/topic/:tid/delete', auth.userRequired, topic.delete);
// 保存新建的文章
-router.post('/topic/create', auth.userRequired, limit.peruserperday('create_topic', config.create_post_per_day), topic.put);
+router.post('/topic/create', auth.userRequired, limit.peruserperday('create_topic', config.create_post_per_day, {showJson: false}), topic.put);
router.post('/topic/:tid/edit', auth.userRequired, topic.update);
router.post('/topic/collect', auth.userRequired, topic.collect); // 关注某话题
router.post('/topic/de_collect', auth.userRequired, topic.de_collect); // 取消关注某话题
// reply controller
-router.post('/:topic_id/reply', auth.userRequired, limit.peruserperday('create_reply', config.create_reply_per_day), reply.add); // 提交一级回复
+router.post('/:topic_id/reply', auth.userRequired, limit.peruserperday('create_reply', config.create_reply_per_day, {showJson: false}), reply.add); // 提交一级回复
router.get('/reply/:reply_id/edit', auth.userRequired, reply.showEdit); // 修改自己的评论页
router.post('/reply/:reply_id/edit', auth.userRequired, reply.update); // 修改某评论
router.post('/reply/:reply_id/delete', auth.userRequired, reply.delete); // 删除某评论
@@ -112,8 +116,15 @@ router.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/signin' }),
github.callback);
router.get('/auth/github/new', github.new);
-router.post('/auth/github/create', github.create);
+router.post('/auth/github/create', limit.peripperday('create_user_per_ip', config.create_user_per_ip, {showJson: false}), github.create);
router.get('/search', search.index);
+if (!config.debug) { // 这个兼容破坏了不少测试
+ router.get('/:name', function (req, res) {
+ res.redirect('/user/' + req.params.name)
+ })
+}
+
+
module.exports = router;