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(' !['+ file.name +']('+ res.url +')'); + + var cm = self.editor.codemirror; + var stat = getState(cm); + _replaceSelection(cm, stat.image, '!['+ file.name +']('+ res.url +')'); + } 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 的技术研究。

服务器搭建在

+

新手搭建 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 @@
    1. 尽量把话题要点浓缩到标题里
    2. 代码含义和报错可在 SegmentFault 提问
    3. -
    4. 给话题选择合适的标签能增加浏览
    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;